Below is a practical, implementation‑oriented plan to build the kicad‑to‑circuit‑json solver. I’ve split it into (1) a punch‑list you can work through, (2) a proposed architecture (mirroring the working circuit‑json‑to‑kicad pipeline you already have), and (3) concrete recommendations and gotchas.
A. Repository & scaffolding
- Reuse the existing
KicadToCircuitJsonConvertershell and turn it into a staged pipeline (symmetry with your CJ→KiCad design). - Bring over the generic
ConverterStage<Input, Output>base pattern from your CJ→KiCad repo (abstract class withstep(),runUntilFinished(),finishedflag, etc.) to replace the emptyConverterStageinterface on the KiCad→CJ side. This keeps both directions consistent.
B. Inputs & parsing
- Accept a map of file paths → contents (already supported by
addFile()), find exactly one.kicad_schand one.kicad_pcb(or allow either one). - Parse with
kicadts(parseKicadSch,parseKicadPcb) and create acju([])database in your converter context.
C. Coordinate systems & transforms
- Define KiCad→Circuit JSON transforms for schematic and PCB. Your CJ→KiCad schematic used
scale(15, -15)plus centering; the inverse isscale(1/15, -1/15)plus inverse translations. PCB usedscale(1, -1); inverse isscale(1, -1)again with appropriate translation. Keep these matrices in context ask2cMatSch,k2cMatPcb.
D. Schematic pipeline (KiCadSch → circuit-json)
- InitializeSchematicContext (paper size, UUID, transform). (mirror of
InitializeSchematicStage) - ExtractLibrarySymbols → translate lib symbols and instance symbols to CJ
schematic_component+schematic_portdata. (mirror ofAddLibrarySymbolsStage+AddSchematicSymbolsStage) - ExtractNetLabels → map power/ground symbols and regular labels into CJ
schematic_net_labelwithanchor_position,anchor_side, etc. (mirror ofAddSchematicNetLabelsStage) - ExtractTraces → convert KiCad wires & junctions to CJ
schematic_trace.edgesandjunctions. (mirror ofAddSchematicTracesStage) - FinalizeSchematic → compute bounds/center for CJ using the inverse of your existing helper logic. (mirror of
getSchematicBoundsAndCenter)
E. PCB pipeline (KicadPcb → circuit-json)
- InitializePcbContext (layers, general, setup ignored for CJ; keep transform). (mirror of
InitializePcbStage) - ExtractNets → translate KiCad nets into CJ naming (you used
Net-<trace_id>going to KiCad; invert that on the way back, preferring meaningful names when available). (mirror ofAddNetsStage) - ExtractFootprints → each KiCad
Footprintbecomes apcb_component+ associated pads/holes/text. You already have one‑way utilities for pads/text—use them as shape/field guides for the reverse direction. (mirror ofAddFootprintsStageandCreate*FromCircuitJsonhelpers) - ExtractTraces & Vias → segments →
pcb_trace.route[]; vias →pcb_viawith proper net mapping. (mirror ofAddTracesStage,AddViasStage) - ExtractGraphics & Board Outline → convert
Edge.Cutsandgr_text/gr_lineintopcb_board.outlineandpcb_silkscreen_*. (mirror ofAddGraphicsStage)
F. Integration & output
- Compose a valid CircuitJson object from your
cjudb and return it. - Provide a
getOutput()andgetOutputString()like the CJ→KiCad converters.
G. Tests & visual diffs
- Use the existing snapshot harness: export KiCad to SVG/PNG with
kicad-cli(yourtake-kicad-snapshot.ts) and export your reconstructed CircuitJson viacircuit-to-svg(yourtake-circuit-json-snapshot.ts), then stack/compare PNGs with the tolerant matcher you already wrote. - Build specimen tests: flat‑hierarchy schematic, simple 2‑layer PCB, text/labels, edge cuts, vias, SMD/TH pads, power symbols, etc.
- Use your PNG diff matcher (tolerance, CI knobs) for robust comparisons.
You already have a clean, staged design for CJ→KiCad with:
- A context that stores the
cjuDB, KiCad AST objects, and transformation matrices. - A pipeline of small, deterministic stages (
Initialize*,Add*Symbols,Add*Traces,Add*Graphics, …). - Deterministic coordinate transforms: Circuit JSON ↔ KiCad via
transformation-matrix.
Replicate that architecture for KiCad→CJ:
-
Context (
ConverterContext) should hold:db: cju([])(write target),- parsed
kicadSch?,kicadPcb?, k2cMatSch?,k2cMatPcb?,- shared maps (e.g.,
netNumToName,footprintUuid→pcb_component_id), mirroring the fields used on the CJ→KiCad side (c2kMatSch,c2kMatPcb, net maps, etc.).
A good mirror for your schematic stages is:
-
InitializeSchematicContextStage
- Build
k2cMatSch(inverse of CJ→KiCad:scale(1/15, -1/15)and matching translations so the schematic content centers in CJ space). - Seed any
pinPositions/wireConnectionsmaps you want to accumulate. (Reference how CJ→KiCad selected paper & centered content inCircuitJsonToKicadSchConverter.)
- Build
-
CollectLibrarySymbolsStage (reverse of
AddLibrarySymbolsStage)- From
lib_symbolsand placed symbol instances, create CJsource_component(withftypeheuristics) andschematic_component. - Ports: generate
schematic_portby reading pins and projecting to CJ coordinates (k2cMatSch).
- From
-
CollectSchematicSymbolsStage (reverse of
AddSchematicSymbolsStage)- Place components (
schematic_component.center), setname(Reference) &value. - Record chip geometry (approximate
size.width/height) from symbol primitives if available, otherwise infer from pin extents.
- Place components (
-
CollectNetLabelsStage (reverse of
AddSchematicNetLabelsStage)- Map power symbols (library
Custom:*likevcc_up,ground_down) to CJschematic_net_labelwithsymbol_name,text,anchor_position,anchor_side. - Regular labels become
schematic_net_labelwithoutsymbol_name.
- Map power symbols (library
-
CollectSchematicTracesStage (reverse of
AddSchematicTracesStage)- Convert wires to
schematic_trace.edgesusingk2cMatSch. - Junctions →
schematic_trace.junctions.
- Convert wires to
-
FinalizeSchematicStage
- Compute CJ bounds/center (inverse of
getSchematicBoundsAndCenter) for any downstream consumers.
- Compute CJ bounds/center (inverse of
Mirror your PCB stages:
-
InitializePcbContextStage
- Build
k2cMatPcb(inverse of your CJ→KiCad PCB transform: you usedtranslate(100,100)andscale(1,-1), so invert appropriately).
- Build
-
CollectNetsStage (reverse of
AddNetsStage)- From KiCad
nets, construct a mapping to CJ names. Prefer KiCad’s actual names; fall back to deterministicNet-<n>if empty. Store map for traces/vias.
- From KiCad
-
CollectFootprintsStage (reverse of
AddFootprintsStage)-
Each
Footprint→ CJpcb_componentwithcenterandrotation. -
fp_text→ CJpcb_silkscreen_textattached to the component; use inverse of yourcreateFpTextFromCircuitJsonlogic to recoveranchor_position,layer,ccw_rotation, etc. -
fp_pads:- SMD pads →
pcb_smtpad(shape,width/heightorradius,layertop/bottom). - TH pads →
pcb_plated_holewithshape(circle,pill, …) and outer diameters; map drill (PadDrill) to CJhole_*fields (mirror ofCreateThruHolePadFromCircuitJson). - NPTH pads →
pcb_hole(mirror ofCreateNpthPadFromCircuitJson).
- SMD pads →
-
-
CollectTracesStage (reverse of
AddTracesStage)- KiCad
Segment→ CJpcb_tracewith aroutearray (transform each segment endpoint throughk2cMatPcb; group adjacent segments by net/layer).
- KiCad
-
CollectViasStage (reverse of
AddViasStage)- KiCad
Via→ CJpcb_via(x,y,outer_diameter,hole_diameter,net_namefrom map).
- KiCad
-
CollectGraphicsStage (reverse of
AddGraphicsStage)gr_textnot bound to footprints →pcb_silkscreen_text(standalone).gr_lineonEdge.Cuts→pcb_board.outlinepolyline.gr_lineon silk layers →pcb_silkscreen_path.
-
FinalizePcbStage
- If outline absent, derive a rectangular board from
Edge.Cutsextents.
- If outline absent, derive a rectangular board from
On the KiCad→CJ side you currently have only an empty ConverterStage interface and a skeletal KicadToCircuitJsonConverter with a pipeline?: ConverterStage[]. Port the abstract base class used by CJ→KiCad (with step(), runUntilFinished(), finished, MAX_ITERATIONS) so both directions share the same ergonomics. It will make your converters composable and testable in the same way.
| Domain | CJ→KiCad (existing) | KiCad→CJ (implement) |
|---|---|---|
| Schematic | c2kMatSch = translate(KICAD_CENTER) ∘ scale(15, -15) ∘ translate(-center) |
k2cMatSch ≈ inverse(c2kMatSch) → translate(center) ∘ scale(1/15, -1/15) ∘ translate(-KICAD_CENTER) (apply actual numeric paper center used by KiCad file) |
| PCB | c2kMatPcb = translate(100,100) ∘ scale(1, -1) |
k2cMatPcb ≈ translate(-100,-100) ∘ scale(1, -1) (or compute exact inverse from c2kMatPcb) |
Use the same transformation-matrix library you already use on CJ→KiCad. Keep the Y inversion consistent, because you relied on scale(_, -_) throughout.
- Silk:
F.SilkS↔top,B.SilkS↔bottom. - Copper:
F.Cu↔top,B.Cu↔bottom. - Preserve unknown layers as raw strings in CJ (forward‑compat). You already map these the other direction; reuse the same tables reversed.
Use your “create‑*FromCircuitJson” functions as a spec for what CJ expects:
- SMD pad: recover
shape(circlevsrect),radiusORwidth/height, and component‑relative position (undo component rotation and center). That’s precisely the inverse of your SMD utility. - TH pad / plated hole: reconstitute the correct variant (
circle,oval/pill,*_with_rect_pad,rotated_*) and its rotation. Your forward mapping handles all of these—mirror it back. - NPTH: map
PadDrill(oval vs circle) and size back to CJ’spcb_hole.
Tip: For component‑local coordinates, you already rotate/translate to KiCad using per‑component rotation matrices. Build the inverse to express pads/holes/text back relative to the component center in CJ.
- Segments: stitch collinear, contiguous segments into a single CJ
routepolyline perpcb_traceif you want a cleaner model; otherwise 1‑segment‑per‑edge also works for MVP. Net comes from yournetmapping. - Vias:
ViaNet(number)→net_namevia reverse lookup. Sizes map 1:1. - Net names: Your forward pass created
Net-<trace_id>defaults; on reverse, prefer KiCad’s explicit names (GND, VCC, etc.). Ensure net 0 handling (“no net”) is treated as unconnected or “GND” only when KiCad indicates so (your forward stage always inserted “GND”; consider being stricter on reverse to avoid inventing ground).
- Instances: From each
SchematicSymbolinstance, create aschematic_componentatk2cMatSch(at.x, at.y). Determine chip vs passive from library id (Device:U_*vsDevice:R_*,Device:C_*, etc.)—that’s how your forward path derived library ids and reference prefixes. - Pins: Convert KiCad pin geometry to
schematic_portpositions relative to the component center. Your forward logic snaps pins to box edges and orients them; reverse by measuring pin endpoints vs symbol box. If symbol primitives aren’t available, infer box from min/max pin extents. - Text: The forward path positioned
ReferenceandValuebased on template fields (and sometimes hidValuefor chips). On reverse, read those properties to populatename/valuein CJ (hide flags don’t exist in CJ; just capture the text content).
- Board: Convert
Edge.Cutsclosed loops into a CJpcb_board.outline(array of points). If multiple loops exist, treat the largest as the main outline (MVP). - Silk text/paths:
gr_text/gr_lineon silk layers →pcb_silkscreen_text/pcb_silkscreen_pathwith stroke widths andanchor_alignmentwhen detectable (you already encodeanchor_alignmenton the forward path; mirror reasonable defaults).
-
Visual parity:
- KiCad input → (your solver) → Circuit JSON → PNG (
circuit-to-svg). - KiCad input →
kicad-cli ... export svg→ PNG. - Stack/compare with your tolerant matcher + optional diff images.
You already have
take-kicad-snapshot.ts,take-circuit-json-snapshot.ts,stackPngsVertically.ts, and the cross‑platformtoMatchPngSnapshotmatcher. Use those directly.
- KiCad input → (your solver) → Circuit JSON → PNG (
-
Fixtures to cover:
- Single‑sheet schematic (R‑C‑U) with net labels (VCC/GND, arrows on left/right).
- PCB with one SMD footprint, one TH connector, vias, silk text top/bottom, edge cuts rectangle and non‑rectangular outline.
- Rotated components, rotated oval pads, NPTH mounting holes.
- Multi‑segment tracks on both layers.
- Emit warnings (collect them in context) for: unknown pad shapes, missing footprints, library id not recognized, segments without nets, symbols without pins.
- Include a stats object (counts of components, pads, vias, traces, labels) to help tests assert completeness.
- Both directions are linear in entities; keep transformations streaming within each stage. Avoid repeated matrix inversions—precompute
k2cMat*.
// lib/KicadToCircuitJsonConverter.ts
import { cju } from "@tscircuit/circuit-json-util"
import { parseKicadPcb, parseKicadSch } from "kicadts"
import { compose, scale, translate, inverse } from "transformation-matrix"
export class KicadToCircuitJsonConverter {
fsMap: Record<string, string> = {}
ctx!: {
db: ReturnType<typeof cju>
kicadPcb?: ReturnType<typeof parseKicadPcb>
kicadSch?: ReturnType<typeof parseKicadSch>
k2cMatSch?: any
k2cMatPcb?: any
// ...net maps, id maps, warnings, stats
}
pipeline: ConverterStage<any, any>[] = []
currentStageIndex = 0
addFile(path: string, content: string) {
this.fsMap[path] = content
}
initializePipeline() {
const pcbFile = this._findFileWithExtension(".kicad_pcb")
const schFile = this._findFileWithExtension(".kicad_sch")
this.ctx = {
db: cju([]),
kicadPcb: pcbFile ? parseKicadPcb(this.fsMap[pcbFile]!) : undefined,
kicadSch: schFile ? parseKicadSch(this.fsMap[schFile]!) : undefined,
// set k2cMat* after inspecting paper/centers if needed
}
this.pipeline = [
new InitializeSchematicContextStage(this.ctx),
new CollectLibrarySymbolsStage(this.ctx),
new CollectSchematicSymbolsStage(this.ctx),
new CollectNetLabelsStage(this.ctx),
new CollectSchematicTracesStage(this.ctx),
new InitializePcbContextStage(this.ctx),
new CollectNetsStage(this.ctx),
new CollectFootprintsStage(this.ctx),
new CollectTracesStage(this.ctx),
new CollectViasStage(this.ctx),
new CollectGraphicsStage(this.ctx),
new FinalizeOutputStage(this.ctx),
]
}
runUntilFinished() {
/* identical to CJ→KiCad loop */
}
getOutput() {
return this.ctx.db.toJSON()
}
getOutputString() {
return JSON.stringify(this.getOutput(), null, 2)
}
}This mirrors the structure you use on the other direction; you can even share the stage base class.
- Y‑axis inversion drift: If an element looks vertically mirrored when you render CJ→SVG, your inverse transform is off. Validate with 1–2 known anchor points per stage.
- Angles: KiCad angles are degrees CCW. CJ uses
ccw_rotation. When converting component‑relative items (pads, silkscreen text), undo the component rotation first, then write CJ coords. - Net 0/GND confusion: Don’t invent ground; read KiCad’s net table. Your forward “always add GND” convenience shouldn’t be mirrored blindly in reverse.
- Pads without X/Y: Your forward utilities explicitly “throw on polygon pads”. For reverse, start with circular/rectangular/oval and warn for polygons until you add support.
- Symbol geometry: If library geometry is missing, derive a minimal size box from pin extents so pin snapping is stable.
-
The KiCad→CJ repo already parses KiCad with
kicadtsand sets up a converter context; it only needs the staged pipeline and inverse transforms added. -
The CJ→KiCad repo is a complete reference for:
- what your context should carry,
- how to map symbols, text, pads, vias, traces, nets, outlines, and
- how to do all coordinate math consistently. Use it as a “ground truth spec” for the reverse direction.
KiCad → Circuit JSON (schematic)
SchematicSymbol@at→schematic_component.center(throughk2cMatSch).SymbolProperty{Reference}→source_component.nameorschematic_component.name.pins[]→schematic_port[]with positions relative to component.- Power symbols (
Custom:*) →schematic_net_label{symbol_name}. Wire,Junction→schematic_trace.edges,junctions.
KiCad → Circuit JSON (pcb)
Footprint@at→pcb_component.center/rotation.FpText(attached) →pcb_silkscreen_textwithanchor_position,layer,ccw_rotation.FootprintPad(SMD) →pcb_smtpad(shape + size). TH/NPTH pads →pcb_plated_hole/pcb_hole.Segment→pcb_trace.route[](grouped per net/layer).Via→pcb_via.GrText/GrLine(F.SilkS/B.SilkS) →pcb_silkscreen_*;Edge.Cutslines →pcb_board.outline.
If you’d like, I can sketch one of the extraction stages (e.g., CollectFootprintsStage) in actual TypeScript next—using the same matrix utilities and field names you already rely on.