diff --git a/src/convert-kicad-json-to-tscircuit-soup.ts b/src/convert-kicad-json-to-tscircuit-soup.ts index 422dbfb..86b28dd 100644 --- a/src/convert-kicad-json-to-tscircuit-soup.ts +++ b/src/convert-kicad-json-to-tscircuit-soup.ts @@ -1,4 +1,5 @@ -import type { KicadModJson } from "./kicad-zod" +import type { KicadModJson, KicadSymJson } from "./kicad-zod" +import { convertKicadSymToTscircuitSchematic } from "./convert-kicad-sym-to-tscircuit-schematic" import type { AnyCircuitElement } from "circuit-json" import Debug from "debug" import { generateArcPath, getArcLength } from "./math/arc-utils" @@ -82,6 +83,7 @@ export const convertKicadLayerToTscircuitLayer = (kicadLayer: string) => { export const convertKicadJsonToTsCircuitSoup = async ( kicadJson: KicadModJson, + kicadSymJson?: KicadSymJson, ): Promise => { const { fp_lines, @@ -102,14 +104,33 @@ export const convertKicadJsonToTsCircuitSoup = async ( supplier_part_numbers: {}, } as any) - circuitJson.push({ + const schematicComponent: any = { type: "schematic_component", schematic_component_id: "schematic_component_0", source_component_id: "source_component_0", center: { x: 0, y: 0 }, rotation: 0, size: { width: 0, height: 0 }, - } as any) + } + + if (kicadSymJson) { + const schematicInfo = convertKicadSymToTscircuitSchematic(kicadSymJson) + schematicComponent.port_arrangement = schematicInfo.port_arrangement + schematicComponent.port_labels = schematicInfo.port_labels + + const pin_spacing = 2.54 // 0.1 inch + const left_pins = + schematicInfo.port_arrangement?.left_side?.pins.length ?? 0 + const right_pins = + schematicInfo.port_arrangement?.right_side?.pins.length ?? 0 + + const height = Math.max(left_pins, right_pins) * pin_spacing + const width = pin_spacing * 4 + + schematicComponent.size = { width, height } + } + + circuitJson.push(schematicComponent) // Collect all unique port names from pads and holes const portNames = new Set() @@ -135,12 +156,42 @@ export const convertKicadJsonToTsCircuitSoup = async ( name: portName, port_hints: [portName], }) + + const schematic_port_id = `schematic_port_${sourcePortId++}` + + let center = { x: 0, y: 0 } + if (schematicComponent.port_arrangement) { + const { width, height } = schematicComponent.size + const pin_spacing = 2.54 + const portNumber = parseInt(portName, 10) + + const left_pins = + schematicComponent.port_arrangement.left_side?.pins ?? [] + const right_pins = + schematicComponent.port_arrangement.right_side?.pins ?? [] + + if (left_pins.includes(portNumber)) { + const pinIndex = left_pins.indexOf(portNumber) + center = { + x: -width / 2, + y: (left_pins.length / 2 - pinIndex - 0.5) * pin_spacing, + } + } + if (right_pins.includes(portNumber)) { + const pinIndex = right_pins.indexOf(portNumber) + center = { + x: width / 2, + y: (right_pins.length / 2 - pinIndex - 0.5) * pin_spacing, + } + } + } + circuitJson.push({ type: "schematic_port", - schematic_port_id: `schematic_port_${sourcePortId++}`, + schematic_port_id, source_port_id, schematic_component_id: "schematic_component_0", - center: { x: 0, y: 0 }, + center, }) } diff --git a/src/convert-kicad-sym-to-tscircuit-schematic.ts b/src/convert-kicad-sym-to-tscircuit-schematic.ts new file mode 100644 index 0000000..0ff0b40 --- /dev/null +++ b/src/convert-kicad-sym-to-tscircuit-schematic.ts @@ -0,0 +1,111 @@ +import type { KicadSymJson, Pin } from "./kicad-zod" + +interface SchematicComponent { + type: "schematic_component" + rotation: number + size: { width: number; height: number } + center: { x: number; y: number } + source_component_id: string + schematic_component_id: string + pin_spacing?: number + pin_styles?: Record< + string, + { + left_margin?: number + right_margin?: number + top_margin?: number + bottom_margin?: number + } + > + box_width?: number + symbol_name?: string + port_arrangement?: + | { + left_size: number + right_size: number + top_size?: number + bottom_size?: number + } + | { + left_side?: { + pins: number[] + direction?: "top-to-bottom" | "bottom-to-top" + } + right_side?: { + pins: number[] + direction?: "top-to-bottom" | "bottom-to-top" + } + top_side?: { + pins: number[] + direction?: "left-to-right" | "right-to-left" + } + bottom_side?: { + pins: number[] + direction?: "left-to-right" | "right-to-left" + } + } + port_labels?: Record +} + +export const convertKicadSymToTscircuitSchematic = ( + kicadSymJson: KicadSymJson, +): Pick => { + const { pins } = kicadSymJson + + const port_labels: Record = {} + for (const pin of pins) { + port_labels[pin.num] = pin.name + } + + const left_pins: Pin[] = [] + const right_pins: Pin[] = [] + const top_pins: Pin[] = [] + const bottom_pins: Pin[] = [] + + for (const pin of pins) { + const angle = pin.at[2] + if (angle === 0) { + right_pins.push(pin) + } else if (angle === 90) { + top_pins.push(pin) + } else if (angle === 180) { + left_pins.push(pin) + } else if (angle === 270) { + bottom_pins.push(pin) + } + } + + // Sort pins by position + left_pins.sort((a, b) => a.at[1] - b.at[1]) // sort by y + right_pins.sort((a, b) => a.at[1] - b.at[1]) // sort by y + top_pins.sort((a, b) => a.at[0] - b.at[0]) // sort by x + bottom_pins.sort((a, b) => a.at[0] - b.at[0]) // sort by x + + const port_arrangement: SchematicComponent["port_arrangement"] = {} + + if (left_pins.length > 0) { + port_arrangement.left_side = { + pins: left_pins.map((p) => parseInt(p.num, 10)), + } + } + if (right_pins.length > 0) { + port_arrangement.right_side = { + pins: right_pins.map((p) => parseInt(p.num, 10)), + } + } + if (top_pins.length > 0) { + port_arrangement.top_side = { + pins: top_pins.map((p) => parseInt(p.num, 10)), + } + } + if (bottom_pins.length > 0) { + port_arrangement.bottom_side = { + pins: bottom_pins.map((p) => parseInt(p.num, 10)), + } + } + + return { + port_arrangement, + port_labels, + } +} diff --git a/src/index.ts b/src/index.ts index aa31af1..96b4562 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ -export { parseKicadModToKicadJson } from "./parse-kicad-mod-to-kicad-json" -export { parseKicadModToCircuitJson } from "./parse-kicad-mod-to-circuit-json" export { convertKicadJsonToTsCircuitSoup } from "./convert-kicad-json-to-tscircuit-soup" +export { convertKicadSymToTscircuitSchematic } from "./convert-kicad-sym-to-tscircuit-schematic" +export { parseKicadModToCircuitJson } from "./parse-kicad-mod-to-circuit-json" +export { parseKicadModToKicadJson } from "./parse-kicad-mod-to-kicad-json" +export { parseKicadSymToKicadJson } from "./parse-kicad-sym-to-kicad-json" diff --git a/src/kicad-zod.ts b/src/kicad-zod.ts index 4cc6394..f632d04 100644 --- a/src/kicad-zod.ts +++ b/src/kicad-zod.ts @@ -260,3 +260,21 @@ export type FpArc = z.infer export type FpCircle = z.infer export type FpPoly = z.infer export type KicadModJson = z.infer + +export const pin_def = z.object({ + num: z.string(), + name: z.string(), + type: z.string(), + shape: z.string().optional(), + at: point3, + length: z.number(), +}) + +export const kicad_sym_json_def = z.object({ + symbol_name: z.string(), + properties: z.array(property_def), + pins: z.array(pin_def), +}) + +export type Pin = z.infer +export type KicadSymJson = z.infer diff --git a/src/parse-kicad-mod-to-circuit-json.ts b/src/parse-kicad-mod-to-circuit-json.ts index 58edbc6..cfa5c40 100644 --- a/src/parse-kicad-mod-to-circuit-json.ts +++ b/src/parse-kicad-mod-to-circuit-json.ts @@ -1,12 +1,18 @@ import type { AnyCircuitElement } from "circuit-json" import { parseKicadModToKicadJson } from "./parse-kicad-mod-to-kicad-json" +import { parseKicadSymToKicadJson } from "./parse-kicad-sym-to-kicad-json" import { convertKicadJsonToTsCircuitSoup as convertKicadJsonToCircuitJson } from "./convert-kicad-json-to-tscircuit-soup" export const parseKicadModToCircuitJson = async ( kicadMod: string, + kicadSym?: string, ): Promise => { const kicadJson = parseKicadModToKicadJson(kicadMod) + const kicadSymJson = kicadSym ? parseKicadSymToKicadJson(kicadSym) : undefined - const circuitJson = await convertKicadJsonToCircuitJson(kicadJson) + const circuitJson = await convertKicadJsonToCircuitJson( + kicadJson, + kicadSymJson, + ) return circuitJson as any } diff --git a/src/parse-kicad-sym-to-kicad-json.ts b/src/parse-kicad-sym-to-kicad-json.ts new file mode 100644 index 0000000..035328d --- /dev/null +++ b/src/parse-kicad-sym-to-kicad-json.ts @@ -0,0 +1,90 @@ +import parseSExpression from "s-expression" +import { kicad_sym_json_def, property_def, pin_def } from "./kicad-zod" +import type { KicadSymJson, Property, Pin } from "./kicad-zod" +import { formatAttr, getAttr } from "./get-attr" +import Debug from "debug" + +const debug = Debug("kicad-sym-converter") + +export const parseKicadSymToKicadJson = (fileContent: string): KicadSymJson => { + const kicadSExpr = parseSExpression(fileContent) + + if (kicadSExpr[0].valueOf() !== "kicad_symbol_lib") { + throw new Error("Not a kicad_symbol_lib file") + } + + const symbol = kicadSExpr.find( + (item: any) => Array.isArray(item) && item[0] === "symbol", + ) + if (!symbol) { + throw new Error("No symbol found in kicad_sym file") + } + + const symbolName = symbol[1].valueOf() + + const properties = symbol + .slice(2) + .filter((row: any[]) => Array.isArray(row) && row[0] === "property") + .map((row: any) => { + const key = row[1].valueOf() + const val = row[2].valueOf() + const attributes = row.slice(3).reduce((acc: any, attrAr: any[]) => { + const attrKey = attrAr[0].valueOf() + acc[attrKey] = formatAttr(attrAr.slice(1), attrKey) + return acc + }, {} as any) + + return { + key, + val, + attributes, + } as Property + }) + + const symbolGraphics = symbol.find( + (item: any) => Array.isArray(item) && item[0] === "symbol", + ) + + const pins: Array = [] + if (symbolGraphics) { + const pinRows = symbolGraphics + .slice(1) + .filter((row: any[]) => Array.isArray(row) && row[0] === "pin") + + for (const row of pinRows) { + const at = getAttr(row, "at") + const length = getAttr(row, "length") + const num = getAttr(row, "num") + const name = getAttr(row, "name") + const type = getAttr(row, "type") + const shape = getAttr(row, "shape") + + if ( + num === undefined || + name === undefined || + type === undefined || + at === undefined || + length === undefined + ) { + debug(`Skipping invalid pin: ${JSON.stringify(row)}`) + continue + } + + const pinRaw = { + num: String(num), + name: String(name), + type: String(type), + shape: shape ? String(shape) : undefined, + at, + length, + } + pins.push(pin_def.parse(pinRaw)) + } + } + + return kicad_sym_json_def.parse({ + symbol_name: symbolName, + properties, + pins, + }) +} diff --git a/src/site/App.tsx b/src/site/App.tsx index 64758dd..ac0b2d6 100644 --- a/src/site/App.tsx +++ b/src/site/App.tsx @@ -26,7 +26,10 @@ export const App = () => { setError(null) let circuitJson: any try { - circuitJson = await parseKicadModToCircuitJson(filesAdded.kicad_mod) + circuitJson = await parseKicadModToCircuitJson( + filesAdded.kicad_mod, + filesAdded.kicad_sym, + ) updateCircuitJson(circuitJson as any) } catch (err: any) { setError(`Error parsing KiCad Mod file: ${err.toString()}`) diff --git a/tests/__snapshots__/schematic-from-sym.snap.svg b/tests/__snapshots__/schematic-from-sym.snap.svg new file mode 100644 index 0000000..cfd7147 --- /dev/null +++ b/tests/__snapshots__/schematic-from-sym.snap.svg @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/tests/data/resistor.kicad_mod b/tests/data/resistor.kicad_mod new file mode 100644 index 0000000..c739446 --- /dev/null +++ b/tests/data/resistor.kicad_mod @@ -0,0 +1,12 @@ +(module Resistor_SMD:R_0603_1608Metric (layer F.Cu) (tedit 5F8A27C4) + (fp_text reference R (at 0 -1.5) (layer F.SilkS) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text value R (at 0 1.5) (layer F.Fab) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_line (start -0.8 0.4) (end 0.8 0.4) (layer F.SilkS) (width 0.12)) + (fp_line (start -0.8 -0.4) (end 0.8 -0.4) (layer F.SilkS) (width 0.12)) + (pad 1 smd rect (at -0.8 0) (size 0.8 0.9) (layers F.Cu)) + (pad 2 smd rect (at 0.8 0) (size 0.8 0.9) (layers F.Cu)) +) diff --git a/tests/data/resistor.kicad_sym b/tests/data/resistor.kicad_sym new file mode 100644 index 0000000..bda1e7b --- /dev/null +++ b/tests/data/resistor.kicad_sym @@ -0,0 +1,14 @@ +(kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor) + (symbol "Resistor" (in_bom yes) (on_board yes) + (property "Reference" "R" (at 0 2.54 0) + (effects (font (size 1.27 1.27)) (justify left bottom)) + ) + (property "Value" "R" (at 0 -2.54 0) + (effects (font (size 1.27 1.27)) (justify left top)) + ) + (symbol "Resistor_0_1" + (pin (num 1) (name "A") (type passive) (shape line) (at -5.08 0 180) (length 2.54)) + (pin (num 2) (name "B") (type passive) (shape line) (at 5.08 0 0) (length 2.54)) + ) + ) +) diff --git a/tests/schematic-from-sym.test.ts b/tests/schematic-from-sym.test.ts new file mode 100644 index 0000000..78b21ee --- /dev/null +++ b/tests/schematic-from-sym.test.ts @@ -0,0 +1,28 @@ +import { test, expect } from "bun:test" +import { readFileSync } from "node:fs" +import { resolve } from "node:path" +import { parseKicadModToCircuitJson } from "../src/parse-kicad-mod-to-circuit-json" + +import { convertCircuitJsonToSchematicSvg } from "circuit-to-svg" + +test("schematic from sym", async () => { + const kicad_mod = readFileSync( + resolve(__dirname, "./data/resistor.kicad_mod"), + "utf-8", + ) + const kicad_sym = readFileSync( + resolve(__dirname, "./data/resistor.kicad_sym"), + "utf-8", + ) + + const circuitJson = await parseKicadModToCircuitJson(kicad_mod, kicad_sym) + + const schematicComponent = circuitJson.find( + (c) => c.type === "schematic_component", + ) + + expect(schematicComponent).toBeTruthy() + + const schematicSvg = convertCircuitJsonToSchematicSvg(circuitJson as any) + expect(schematicSvg).toMatchSvgSnapshot(import.meta.path) +})