Specctra round trip: N copper layers, copper zones as planes, back-side components, custom rules handed to Freerouting

Open
#430 5 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
12/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
rust
Domain
devtools

Research direction

This is a design proposal, not a task to start: the author asks for agreement on the split and order before any code. Read the four places named in the body: crates/copperpour-core/src/specctra.rs (copper_layers, SupportedProfile), crates/copperpour-core/src/specctra_ses.rs (validate_manifest), and crates/copperpour-core/src/tools/pcb_export.rs. Done for a newcomer means the open questions in step 3a are answered by a maintainer and the scope is agreed, not a patch.

Written by the indexing model from the issue text.

Description

enhancement

Problem

The Specctra round trip (export_specctra_dsn → route_specctra_dsn → apply_specctra_ses) only accepts a two-layer board with all components on the front, no copper zones and no custom rules. A dense 4-layer board, with ground and power planes on the inner layers, components on both sides and a project .kicad_dru, is refused at the first step:

unsupported first routing profile: expected exactly F.Cu and B.Cu, got [F.Cu, In1.Cu, In2.Cu, B.Cu]

Freerouting is the only autorouting path, and Copperpour reaches it only through Specctra. On such a board, nothing can be autorouted.

The limit is not a single check. On main (1f2f0eb0) it sits in four places that have to move together:

  • export, crates/copperpour-core/src/specctra.rs: copper_layers() requires exactly [F.Cu, B.Cu]; any zone or rule area is refused; locked vias must span [F.Cu, B.Cu]; padstacks are written for F.Cu/B.Cu; component side is hard-coded to "front";
  • manifest, same file: SupportedProfile stores copper_layers: 2, component_side: "front", copper_zones: false and custom_rules: false as constants, not as values read from the board (MANIFEST_SCHEMA_VERSION = 2);
  • import, crates/copperpour-core/src/specctra_ses.rs: validate_manifest() re-checks the same four constants, "manifest must contain exactly two copper layer mappings", and component.side != "front";
  • tool, crates/copperpour-core/src/tools/pcb_export.rs: refusal on the mere presence of <board>.kicad_dru, and capabilities.supported_profile is the fixed string two_layer_front_side_…_no_existing_routing_or_zones.

Existing issues

  • #37 is this limit, imported from Konnect, with its acceptance list (ordered copper stack in DSN and manifest, SES validated on inner layers, atomic apply, real multilayer end-to-end evidence, fail closed on anything not representable). This plan follows that list. If you agree, the series closes #37.
  • #75 holds the .kicad_dru half. Step 4 below proposes a different way to lift that refusal; if you agree, it closes #75 too. Its increment 1 (a truthful refusal message) stays independent and is not touched here.
  • #79 is the multilayer tracker. This series does not take it over: it only covers its point 5 (multilayer autorouting) and would be linked there.
  • #72 (export check order, native bridge), assigned to mn89h: not touched. The series widens only the checks that the decision on #72 calls import-owned (layers, side, zones, rules), which run in every mode whatever order #72 settles on.
  • #81 (validated two-layer rule areas) and #71 (stale zone fills after copper edits) are close to step 3a; see the question there.
  • #243 (rounded outline) is out of scope.

On the export path

The Konnect review on #37 asked not to make a Rust reimplementation of KiCad's multilayer exporter the primary solution. Two facts from the code shape this plan:

  • the reverse manifest that apply_specctra_ses validates against is built by the Rust export in every mode; adopt_native_dsn only correlates the native DSN against it, and correlate_layers refuses a layer-count mismatch. A multilayer manifest therefore has to come from the Rust side whichever writer produces the DSN;
  • the native bridge is the KiCad 10 ActionPlugin, so KiCad 11 stays on the Rust path (decision on #72).

So step 2 extends the Rust side to N copper layers, bounded to what the native exporter writes for the same board, and each step proves it against KiCad's own DSN for the same board.

Plan: one PR per step

Each PR is one result, built from main, with the PR template, the reliability table, scripts/control.sh runs for every new guard ("Guard broken | Tests failing") and a note in docs/api-migrations/. Fixtures come from KiCad, never hand-written.

  1. This issue. No code before the approach is agreed here.

  2. N copper layers (N ≥ 2, even, inner layers included) in DSN export, manifest and SES import. One PR, because an export without the matching import is not a usable result.

    • copper_layers() returns the ordered list of enabled copper layers (F.Cu, In*.Cu, B.Cu) and refuses an empty, gapped or out-of-order list. The DSN structure lists every layer as signal; through-hole padstacks and locked through vias span every layer.
    • The manifest stores the layer count read from the board and one mapping per layer. MANIFEST_SCHEMA_VERSION goes to 3 if the shape changes; schema 1 and 2 manifests are still read (test).
    • validate_manifest() and the layer-mapping check accept N, and refuse a SES that names a layer absent from the manifest.
    • KiCad 10 native bridge: the native DSN of the same 4-layer board passes the existing comparison, with a test that the Rust and native paths agree on it.
    • capabilities.supported_profile and the tool description state the new profile, derived from the code rather than a fixed string.
    • Evidence: a KiCad-produced multilayer fixture (for example crates/copperpour-core/tests/fixtures/six_layer_power_planes_kicad10.kicad_pcb, already in the repository), and a full round trip on a public 4-layer board (KiCad's demos/video, unlocked tracks and vias removed): export, route_specctra_dsn, apply_specctra_ses, DRC, with routed nets, vias and tracks per inner layer in the PR.
    • Still refused: blind, buried and micro vias, arcs, non-rectilinear outlines (unchanged), zones, back-side components and custom rules (steps 3 and 4).
  3. Two PRs after step 2, each from main, independent of each other.

    • 3a, copper zones. A filled copper zone becomes a plane in the DSN, as KiCad's native exporter writes it. The manifest inventories zones per layer and per net, read from the board. apply_specctra_ses never creates, modifies or deletes a zone, checks that the inventory has not changed, and says in its response that a zone refill is needed. Evidence: the same public board with its planes kept, DRC after refill.
      Questions for you: should apply_specctra_ses only report the needed refill, or run refill_zones itself (#71 is about the same staleness)? And do rule areas (keep-outs) belong here, or do they stay refused and remain with #81?
    • 3b, back-side components. (place REF x y back rot) and the mirrored image, as KiCad writes them; the real side in the manifest; the placement invariance check on import also compares the side. Evidence: a KiCad fixture with a component flipped by flip_component, round trip, DRC, comparison with the native DSN.
  4. Custom rules: lift the .kicad_dru refusal and hand the file to Freerouting. This step waits for an option in Freerouting itself that reads KiCad custom rules; it is being proposed upstream there, and I will link it here once it is public. Copperpour would then not translate any rule: no .kicad_dru parser on this side.

    • export_specctra_dsn stops refusing a board with <board>.kicad_dru; the manifest records custom_rules: true and the file's sha256; apply_specctra_ses refuses if the file changed since export.
    • route_specctra_dsn passes the file to Freerouting unchanged, and refuses with a clear message (minimum version, test) when the Freerouting it finds does not support the option.
    • Evidence: a public board with a .kicad_dru that changes the routing (a wider clearance on one net), before and after.
    • A possible later direction, not part of this plan: KiCad 11's IPC API exposes custom rules (GetCustomDesignRules, on KiCad master, empty in 10.0.x), so the file could be written for Freerouting from IPC when the board has none on disk.

Steps 2 and 3 do not depend on step 4, and step 4 does not start before the Freerouting option is released.

Does this split and order work for you, and should anything be scoped differently before I start on step 2?

Dominant language
Rust
Stars
34
Forks
9
Avg merge
9h 26m
Merged PRs (30d)
226

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Copperpour/Copperpour

All issues in Copperpour/Copperpour

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.