Skip to content

feat(pcb): translate pcb_copper_pour records to KiCad (zone …) blocks#285

Open
gsdali wants to merge 2 commits intotscircuit:mainfrom
gsdali:add-copperpour-zone-export
Open

feat(pcb): translate pcb_copper_pour records to KiCad (zone …) blocks#285
gsdali wants to merge 2 commits intotscircuit:mainfrom
gsdali:add-copperpour-zone-export

Conversation

@gsdali
Copy link
Copy Markdown

@gsdali gsdali commented May 6, 2026

Summary

Closes #284.

The PCB exporter previously emitted no (zone) blocks regardless of how many pcb_copper_pour records were in the input circuit JSON. Pours rendered correctly in gerber export (G36/G37 region commands in B_Cu.gbr/F_Cu.gbr) but were silently dropped on the KiCad path — so opening the resulting .kicad_pcb in pcbnew showed pads + traces + silkscreen but no ground plane.

Fix

New AddCopperPoursStage slotted into the PCB pipeline after AddViasStage. For each pcb_copper_pour record:

  • Resolve the KiCad net via source_net_id → source_net's subcircuit_connectivity_map_keypcbNetMap (same lookup pattern as AddViasStage).
  • Map layer to the KiCad layer name via the existing getKicadLayer helper (bottomB.Cu, inner1In1.Cu, etc.).
  • Transform brep_shape.outer_ring.vertices through the existing c2kMatPcb matrix (origin shift + Y-flip) to emit (polygon (pts (xy …))).
  • Preserve brep_shape.inner_rings as additional (polygon …) children so KiCad subtracts them when refilling.
  • Assign a deterministic UUID via the same helper used by AddViasStage.

Build the Zone via parseKicadSexpr round-trip on a sexpr-string template, since kicadts exposes Zone primarily through that path (the class itself is sparse — rawChildren only).

Test plan

  • 3 new tests in tests/pcb/copperpour-zone-export.test.ts:
    • basic pour with resolvable net → 1 zone, correct (net 1) (net_name GND) (layer B.Cu)
    • pour without source_net_id → falls back to (net 0)
    • pour with inner_rings → emits both outline and cutout polygons
  • All previously-passing tests still pass: 89/97
    • The 8 failing tests fail on main too (pre-existing unrelated issues with the KiCad-CLI snapshot fixtures — verified via git stash && bun test <failing> && git stash pop round-trip)
  • bun run typecheck: clean
  • biome format: clean

Smoke test against a real board

Tested against an InsightSiP ISP3080-UX daughter board (circuit.json with 4 pcb_copper_pour fragments, all on net.GND, bottom layer). Output .kicad_pcb contains 4 (zone) blocks each with (net 1) (net_name GND) (layer B.Cu) and correctly transformed polygon vertices:

(zone
    (net 1)
    (net_name GND)
    (layer B.Cu)
    (uuid 56720008-153e-7238-7f11-1b881360a948)
    (hatch edge 0.5)
    (connect_pads (clearance 0.2))
    (min_thickness 0.25)
    (filled_areas_thickness no)
    (fill yes (thermal_gap 0.5) (thermal_bridge_width 0.5))
    (polygon (pts (xy 88.3 89.3) (xy 111.7 89.3) ...))
)

What's not in this PR

  • KiCad will recompute the actual filled copper at refill time from outline + clearance / min_thickness. We don't pre-fill the polygon — that would require running the same constraint-solver KiCad does internally, and KiCad's refill is reliable.
  • Inner rings are emitted as separate (polygon) children rather than as parts of a (filled_polygon). This works for the "outline as input to refill" model but the visual representation in pcbnew before refill may show outline-only. After Edit → Fill All Zones (B), the actual fill matches the source pour intent.

🤖 Generated with Claude Code

Closes tscircuit#284.

The PCB exporter previously emitted no `(zone)` blocks regardless of
how many `pcb_copper_pour` records were in the input circuit JSON.
Copper pours rendered correctly in gerber export (G36/G37 region
commands) but were silently dropped on the KiCad path, so KiCad's
pcbnew showed pads + traces + silkscreen but no ground plane.

Add `AddCopperPoursStage`, slotted into the PCB pipeline after
`AddViasStage`. For each `pcb_copper_pour`:

- resolve the KiCad net via `source_net_id` → source_net's
  subcircuit_connectivity_map_key → pcbNetMap (same lookup pattern
  as AddViasStage)
- map `layer` to the KiCad layer name via the existing
  layerMapping helper
- transform `brep_shape.outer_ring.vertices` through the
  `c2kMatPcb` matrix to emit `(polygon (pts (xy …)))`
- preserve `brep_shape.inner_rings` as additional `(polygon …)`
  children so KiCad subtracts them when refilling
- assign a deterministic UUID

Build the zone via `parseKicadSexpr` round-trip on a sexpr-string
template, since `kicadts` exposes Zone primarily through that path.

Tests:
- 3 new tests in `tests/pcb/copperpour-zone-export.test.ts`:
  - basic pour with resolvable net → 1 zone, correct net id/name/layer
  - pour without source_net_id → falls back to net 0
  - pour with inner_rings → emits both outline and cutout polygons
- All previously-passing tests still pass (89/97; the 8 failing
  tests fail on `main` too — pre-existing unrelated issues with
  the KiCad-CLI snapshot fixtures)

Smoke-tested against an InsightSiP ISP3080-UX daughter board
(`circuit.json` with 4 pcb_copper_pour fragments, all on net.GND
bottom layer): output `.kicad_pcb` contains 4 `(zone)` blocks each
with `(net 1) (net_name GND) (layer B.Cu)` and the correct
transformed polygon vertices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
circuit-json-to-kicad Ready Ready Preview, Comment May 11, 2026 0:12am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KiCad export drops <copperpour> — emits 0 (zone) blocks despite pcb_copper_pour records in circuit.json

1 participant