Skip to content

Commit 3b6cd71

Browse files
Add KicadLibraryConverter API with user/builtin separation for kicad-library directories and files (tscircuit#47)
* Add KicadLibraryConverter API with user/builtin separation for kicad-library directories and files * add test * update * Use object params * update * update * update * fix naming * naming * classify in stages * update naming * add test * update * rename * update
1 parent 6ba4fef commit 3b6cd71

20 files changed

Lines changed: 1039 additions & 9 deletions

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from "./schematic/CircuitJsonToKicadSchConverter"
22
export * from "./pcb/CircuitJsonToKicadPcbConverter"
33
export * from "./project/CircuitJsonToKicadProConverter"
44
export * from "./kicad-library/CircuitJsonToKicadLibraryConverter"
5+
export * from "./kicad-library/KicadLibraryConverter"
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { CircuitJsonToKicadLibraryConverter } from "./CircuitJsonToKicadLibraryConverter"
2+
import type {
3+
KicadLibraryConverterOptions,
4+
KicadLibraryConverterOutput,
5+
KicadLibraryConverterContext,
6+
BuiltTscircuitComponent,
7+
ExtractedKicadComponent,
8+
} from "./KicadLibraryConverterTypes"
9+
import { classifyKicadFootprints } from "./stages/ClassifyKicadFootprintsStage"
10+
import { classifyKicadSymbols } from "./stages/ClassifyKicadSymbolsStage"
11+
import { buildKicadLibraryFiles } from "./stages/BuildKicadLibraryFilesStage"
12+
13+
export type { KicadLibraryConverterOptions, KicadLibraryConverterOutput }
14+
15+
/**
16+
* Converts tscircuit component files to a KiCad library.
17+
*/
18+
export class KicadLibraryConverter {
19+
private options: KicadLibraryConverterOptions
20+
private output: KicadLibraryConverterOutput | null = null
21+
private ctx: KicadLibraryConverterContext
22+
23+
constructor(options: KicadLibraryConverterOptions) {
24+
this.options = options
25+
this.ctx = createKicadLibraryConverterContext({
26+
kicadLibraryName: options.kicadLibraryName ?? "tscircuit_library",
27+
includeBuiltins: options.includeBuiltins ?? true,
28+
})
29+
}
30+
31+
async run(): Promise<void> {
32+
// Stage 1: Build tscircuit components to circuit-json
33+
this.ctx.builtTscircuitComponents = await this.buildTscircuitComponents()
34+
35+
// Stage 2: Extract KiCad footprints and symbols from circuit-json
36+
this.ctx.extractedKicadComponents = this.extractKicadComponents()
37+
38+
// Stage 3: Classify footprints into user/builtin
39+
classifyKicadFootprints(this.ctx)
40+
41+
// Stage 4: Classify symbols into user/builtin
42+
classifyKicadSymbols(this.ctx)
43+
44+
// Stage 5: Build output files
45+
buildKicadLibraryFiles(this.ctx)
46+
47+
this.output = {
48+
kicadProjectFsMap: this.ctx.kicadProjectFsMap,
49+
model3dSourcePaths: this.ctx.model3dSourcePaths,
50+
}
51+
}
52+
53+
/**
54+
* Builds tscircuit components to circuit-json.
55+
*/
56+
private async buildTscircuitComponents(): Promise<BuiltTscircuitComponent[]> {
57+
const builtTscircuitComponents: BuiltTscircuitComponent[] = []
58+
const { entrypoint } = this.options
59+
60+
const exports = await this.options.getExportsFromTsxFile(entrypoint)
61+
const componentExports = exports.filter((name) => /^[A-Z]/.test(name))
62+
63+
for (const exportName of componentExports) {
64+
let componentPath = entrypoint
65+
if (this.options.resolveExportPath) {
66+
const resolved = await this.options.resolveExportPath(
67+
entrypoint,
68+
exportName,
69+
)
70+
if (resolved) componentPath = resolved
71+
}
72+
73+
const circuitJson = await this.options.buildFileToCircuitJson(
74+
componentPath,
75+
exportName,
76+
)
77+
if (
78+
circuitJson &&
79+
(!Array.isArray(circuitJson) || circuitJson.length > 0)
80+
) {
81+
builtTscircuitComponents.push({
82+
tscircuitComponentName: exportName,
83+
circuitJson,
84+
})
85+
}
86+
}
87+
88+
return builtTscircuitComponents
89+
}
90+
91+
/**
92+
* Extracts KiCad footprints and symbols from built tscircuit components.
93+
*/
94+
private extractKicadComponents(): ExtractedKicadComponent[] {
95+
const extractedKicadComponents: ExtractedKicadComponent[] = []
96+
97+
for (const builtTscircuitComponent of this.ctx.builtTscircuitComponents) {
98+
const { tscircuitComponentName, circuitJson } = builtTscircuitComponent
99+
100+
const libConverter = new CircuitJsonToKicadLibraryConverter(circuitJson, {
101+
libraryName: this.ctx.kicadLibraryName,
102+
footprintLibraryName: this.ctx.kicadLibraryName,
103+
})
104+
libConverter.runUntilFinished()
105+
const libOutput = libConverter.getOutput()
106+
107+
// Collect 3D model paths
108+
for (const path of libOutput.model3dSourcePaths) {
109+
if (!this.ctx.model3dSourcePaths.includes(path)) {
110+
this.ctx.model3dSourcePaths.push(path)
111+
}
112+
}
113+
114+
extractedKicadComponents.push({
115+
tscircuitComponentName,
116+
kicadFootprints: libOutput.footprints,
117+
kicadSymbols: libOutput.symbols,
118+
model3dSourcePaths: libOutput.model3dSourcePaths,
119+
})
120+
}
121+
122+
return extractedKicadComponents
123+
}
124+
125+
getOutput(): KicadLibraryConverterOutput {
126+
if (!this.output) {
127+
throw new Error(
128+
"Converter has not been run yet. Call run() before getOutput().",
129+
)
130+
}
131+
return this.output
132+
}
133+
}
134+
135+
/**
136+
* Creates an initialized context for the KiCad library converter.
137+
*/
138+
function createKicadLibraryConverterContext(params: {
139+
kicadLibraryName: string
140+
includeBuiltins: boolean
141+
}): KicadLibraryConverterContext {
142+
return {
143+
kicadLibraryName: params.kicadLibraryName,
144+
includeBuiltins: params.includeBuiltins,
145+
builtTscircuitComponents: [],
146+
extractedKicadComponents: [],
147+
userKicadFootprints: [],
148+
builtinKicadFootprints: [],
149+
userKicadSymbols: [],
150+
builtinKicadSymbols: [],
151+
model3dSourcePaths: [],
152+
kicadProjectFsMap: {},
153+
}
154+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { CircuitJson } from "circuit-json"
2+
import type { SymbolEntry, FootprintEntry } from "../types"
3+
4+
export interface KicadLibraryConverterOptions {
5+
/**
6+
* Name for the generated KiCad library (e.g., "my-library").
7+
* This will be used for the user library files.
8+
*/
9+
kicadLibraryName?: string
10+
11+
/**
12+
* The main entry point file for the library (e.g., "lib/my-library.ts").
13+
* This file's exports define the public API of the library.
14+
*/
15+
entrypoint: string
16+
17+
/**
18+
* Callback to build circuit JSON from a file path and export name.
19+
* Should handle both board components and symbol components:
20+
* - For board components: render inside a <board> element
21+
* - For symbol components: render inside a <chip> with the symbol prop
22+
* (Note: tscircuit symbols cannot render standalone - they must be
23+
* used as a prop on a <chip> component)
24+
* Return null if the export cannot be rendered.
25+
*/
26+
buildFileToCircuitJson: (
27+
filePath: string,
28+
componentName: string,
29+
) => Promise<CircuitJson | null>
30+
31+
/**
32+
* Callback to get all exports from a TSX/TS file.
33+
* Must evaluate the file (not just parse) to handle `export * from` patterns.
34+
*/
35+
getExportsFromTsxFile: (filePath: string) => Promise<string[]>
36+
37+
/**
38+
* Callback to resolve an export name to its file path.
39+
* Returns the file path where the component is defined, or null if not resolvable.
40+
*/
41+
resolveExportPath?: (
42+
entrypoint: string,
43+
exportName: string,
44+
) => Promise<string | null>
45+
46+
/**
47+
* Whether to include builtin footprints/symbols (like 0402, soic8).
48+
* Default: true
49+
*/
50+
includeBuiltins?: boolean
51+
}
52+
53+
export interface KicadLibraryConverterOutput {
54+
/**
55+
* Map of file paths to file contents for the generated KiCad library.
56+
*/
57+
kicadProjectFsMap: Record<string, string | Buffer>
58+
59+
/**
60+
* Source paths to 3D model files that need to be copied.
61+
*/
62+
model3dSourcePaths: string[]
63+
}
64+
65+
/**
66+
* A component that has been built to circuit JSON.
67+
*/
68+
export interface BuiltTscircuitComponent {
69+
tscircuitComponentName: string
70+
circuitJson: CircuitJson
71+
}
72+
73+
/**
74+
* A component with its extracted KiCad footprints and symbols.
75+
*/
76+
export interface ExtractedKicadComponent {
77+
tscircuitComponentName: string
78+
kicadFootprints: FootprintEntry[]
79+
kicadSymbols: SymbolEntry[]
80+
model3dSourcePaths: string[]
81+
}
82+
83+
/**
84+
* Context for the KiCad library converter stages.
85+
*/
86+
export interface KicadLibraryConverterContext {
87+
kicadLibraryName: string
88+
includeBuiltins: boolean
89+
90+
/** Tscircuit components built to circuit-json */
91+
builtTscircuitComponents: BuiltTscircuitComponent[]
92+
93+
/** KiCad footprints and symbols extracted from circuit-json */
94+
extractedKicadComponents: ExtractedKicadComponent[]
95+
96+
/** User-defined footprints (custom footprint={<footprint>...}) */
97+
userKicadFootprints: FootprintEntry[]
98+
99+
/** Builtin footprints (from footprinter like 0402, soic8) */
100+
builtinKicadFootprints: FootprintEntry[]
101+
102+
/** User-defined symbols (custom symbol={<symbol>...} or renamed for custom footprint) */
103+
userKicadSymbols: SymbolEntry[]
104+
105+
/** Builtin symbols (from schematic-symbols package) */
106+
builtinKicadSymbols: SymbolEntry[]
107+
108+
/** 3D model source paths to copy */
109+
model3dSourcePaths: string[]
110+
111+
/** Final output file map */
112+
kicadProjectFsMap: Record<string, string | Buffer>
113+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Generate fp-lib-table content for KiCad footprint library.
3+
*/
4+
export function generateFpLibTable(params: {
5+
kicadLibraryName: string
6+
includeBuiltin: boolean
7+
}): string {
8+
const { kicadLibraryName, includeBuiltin } = params
9+
let content = "(fp_lib_table\n"
10+
content += ` (lib (name "${kicadLibraryName}")(type "KiCad")(uri "\${KIPRJMOD}/footprints/${kicadLibraryName}.pretty")(options "")(descr ""))\n`
11+
if (includeBuiltin) {
12+
content += ` (lib (name "tscircuit_builtin")(type "KiCad")(uri "\${KIPRJMOD}/footprints/tscircuit_builtin.pretty")(options "")(descr ""))\n`
13+
}
14+
content += ")\n"
15+
return content
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Generate sym-lib-table content for KiCad symbol library.
3+
*/
4+
export function generateSymLibTable(params: {
5+
kicadLibraryName: string
6+
includeBuiltin: boolean
7+
}): string {
8+
const { kicadLibraryName, includeBuiltin } = params
9+
let content = "(sym_lib_table\n"
10+
content += ` (lib (name "${kicadLibraryName}")(type "KiCad")(uri "\${KIPRJMOD}/symbols/${kicadLibraryName}.kicad_sym")(options "")(descr ""))\n`
11+
if (includeBuiltin) {
12+
content += ` (lib (name "tscircuit_builtin")(type "KiCad")(uri "\${KIPRJMOD}/symbols/tscircuit_builtin.kicad_sym")(options "")(descr ""))\n`
13+
}
14+
content += ")\n"
15+
return content
16+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { FootprintEntry } from "../../types"
2+
import { parseKicadMod } from "kicadts"
3+
4+
/**
5+
* Rename a KiCad footprint entry to use a new name.
6+
*/
7+
export function renameKicadFootprint(params: {
8+
kicadFootprint: FootprintEntry
9+
newKicadFootprintName: string
10+
kicadLibraryName: string
11+
}): FootprintEntry {
12+
const { kicadFootprint, newKicadFootprintName, kicadLibraryName } = params
13+
14+
const footprint = parseKicadMod(kicadFootprint.kicadModString)
15+
16+
// Update the footprint name (libraryLink)
17+
footprint.libraryLink = newKicadFootprintName
18+
19+
// Update 3D model paths to use the correct library name
20+
for (const model of footprint.models) {
21+
const currentPath = model.path
22+
if (currentPath.includes("${KIPRJMOD}/")) {
23+
// Extract the filename from the path
24+
const filename = currentPath.split("/").pop() ?? ""
25+
model.path = `\${KIPRJMOD}/3dmodels/${kicadLibraryName}.3dshapes/${filename}`
26+
}
27+
}
28+
29+
return {
30+
footprintName: newKicadFootprintName,
31+
kicadModString: footprint.getString(),
32+
model3dSourcePaths: kicadFootprint.model3dSourcePaths,
33+
}
34+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { SymbolEntry } from "../../types"
2+
3+
/**
4+
* Rename a KiCad symbol entry to use a new name.
5+
* Updates child symbol units (subSymbols) to match the new name.
6+
*/
7+
export function renameKicadSymbol(params: {
8+
kicadSymbol: SymbolEntry
9+
newKicadSymbolName: string
10+
}): SymbolEntry {
11+
const { kicadSymbol, newKicadSymbolName } = params
12+
const symbol = kicadSymbol.symbol
13+
const oldName = symbol.libraryId
14+
15+
// Update main symbol name
16+
symbol.libraryId = newKicadSymbolName
17+
18+
// Update child symbol unit names (e.g., "OldName_0_1" -> "NewName_0_1")
19+
if (oldName && symbol.subSymbols) {
20+
for (const subSymbol of symbol.subSymbols) {
21+
if (subSymbol.libraryId?.startsWith(oldName)) {
22+
const suffix = subSymbol.libraryId.slice(oldName.length)
23+
subSymbol.libraryId = newKicadSymbolName + suffix
24+
}
25+
}
26+
}
27+
28+
return { symbolName: newKicadSymbolName, symbol }
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { SymbolEntry } from "../../types"
2+
3+
/**
4+
* Update a builtin KiCad symbol's footprint reference to point to tscircuit_builtin library.
5+
*/
6+
export function updateBuiltinKicadSymbolFootprint(
7+
kicadSymbol: SymbolEntry,
8+
): SymbolEntry {
9+
const symbol = kicadSymbol.symbol
10+
const properties = symbol.properties ?? []
11+
12+
for (const prop of properties) {
13+
if (prop.key === "Footprint" && prop.value) {
14+
const parts = prop.value.split(":")
15+
const footprintName = parts.length > 1 ? parts[1] : parts[0]
16+
prop.value = `tscircuit_builtin:${footprintName}`
17+
}
18+
}
19+
20+
return { symbolName: kicadSymbol.symbolName, symbol }
21+
}

0 commit comments

Comments
 (0)