Summary
convertSoupToGerberCommands only emits F_Cu and B_Cu copper gerbers, even when the input circuit_json has pcb_trace segments on inner1 / inner2 and a pcb_board.num_layers === 4. Inner-layer trace data is silently dropped from the gerber zip — a fab building from these files produces a 2-layer board with broken connectivity, with no error or warning surfaced to the user.
Repro
Build any 4-layer tscircuit board (<board layers={4}>) where the autorouter places traces on inner1 / inner2. Confirm via circuit.json:
const innerLayers = new Set();
for (const t of circuit_json) if (t.type === 'pcb_trace')
for (const r of t.route) if (r.layer) innerLayers.add(r.layer);
// → Set { 'top', 'inner1', 'inner2', 'bottom' }
Run bunx tsci export <file> --format gerbers --output ../board.zip and inspect the zip:
F_Cu.gbr F_SilkScreen.gbr F_Mask.gbr F_Paste.gbr
B_Cu.gbr B_SilkScreen.gbr B_Mask.gbr B_Paste.gbr
Edge_Cuts.gbr drill.drl drill_npth.drl bom.csv pick_and_place.csv
No In1_Cu.gbr / In2_Cu.gbr. The two inner copper layers' traces are absent from the output.
Where it's missing
Across the package, the type system, layer maps, and rendering loops all hardcode top / bottom:
src/gerber/convert-soup-to-gerber-commands/GerberLayerName.ts — LayerToGerberCommandsMap only declares F_Cu / B_Cu / etc.
src/gerber/convert-soup-to-gerber-commands/getGerberLayerName.ts — layerRefToGerberPrefix has only { top: "F_", bottom: "B_" }.
src/gerber/convert-soup-to-gerber-commands/getCommandHeaders.ts — has the // TODO inner layers comment alongside an inner1 | inner2 | inner3 | inner4 accepted-layer union, but the corresponding layerAndTypeToFileFunction entries are missing.
src/gerber/convert-soup-to-gerber-commands/getAllTraceWidths.ts — return shape is { top, bottom } only.
src/gerber/convert-soup-to-gerber-commands/index.ts — glayers object, the aperture-defining loop (~line 79), and two trace/via render loops (~lines 456 and 594) are hardcoded to top/bottom.
src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts — glayer_name.startsWith("F_") ? "top" : "bottom" collapses inner layers into the bottom bucket; getAllApertureTemplateConfigsForLayer only accepts "top" | "bottom".
Proposed fix
I have a working patch on kcknox/circuit-json-to-gerber branch feature/inner-layer-gerbers that:
- Adds
In1_Cu / In2_Cu to LayerToGerberCommandsMap.
- Adds
inner1: "In1_" / inner2: "In2_" to layerRefToGerberPrefix.
- Adds
inner1-copper: "Copper,L2,Inr" / inner2-copper: "Copper,L3,Inr" to layerAndTypeToFileFunction (Gerber X2 file-function attribute).
- Adds
inner1 / inner2 keys to getAllTraceWidths return shape.
- Adds
In1_Cu / In2_Cu headers to the glayers object and includes them in the aperture-defining loop.
- Generalises the two render loops to iterate
["top", "inner1", "inner2", "bottom"] (and + "edgecut" for the second), with early-continue guards so handlers for pcb_smtpad / pcb_solder_paste / pcb_silkscreen_* / pcb_hole skip inner layers (they only carry copper traces, vias, plated holes, and copper text).
- Generalises
getApertureConfigsForLayer and defineAperturesForLayer's layer-bucket lookup to handle inner layers.
After the patch, exporting a 4-layer board produces:
F_Cu.gbr In1_Cu.gbr In2_Cu.gbr B_Cu.gbr ...
26/27 existing tests pass (the 1 failing test, generate-gerber-macrokeypad, was already failing on main before my changes — looks like a pre-existing 20s timeout / snapshot drift issue unrelated to this).
Questions before opening a PR
A few design questions I'd want maintainer input on before sending the diff:
- N inner layers vs just 2? I implemented
inner1 + inner2 because that covers 4-layer (the common case for medium-density designs) and the existing getCommandHeaders already accepts the inner1..4 union. Should it generalise to inner1..6 (12-layer support) on day one, or is incremental fine?
.FileFunction Lk numbering. I left bottom-copper as Copper,L2,Bot rather than rewriting it to L4,Bot for 4-layer boards, since the layer count isn't always known at gerber-emit time. Most fabs identify stack position from the filename, not this attribute. OK?
- Inner-layer file extension. Many fabs (JLCPCB, PCBWay, Eurocircuits) reject
In1_Cu.gbr / In2_Cu.gbr and expect Protel-style .g1 / .g2. Should circuit-json-to-gerber keep emitting .gbr (current convention) and let tscircuit/cli's zip-write step handle the rename, or should this package own the extension mapping?
Happy to open the PR once there's a thumbs-up on direction. Branch is ready at https://github.com/kcknox/circuit-json-to-gerber/tree/feature/inner-layer-gerbers — no PR opened yet to avoid blind-PRing.
Summary
convertSoupToGerberCommandsonly emits F_Cu and B_Cu copper gerbers, even when the inputcircuit_jsonhaspcb_tracesegments oninner1/inner2and apcb_board.num_layers === 4. Inner-layer trace data is silently dropped from the gerber zip — a fab building from these files produces a 2-layer board with broken connectivity, with no error or warning surfaced to the user.Repro
Build any 4-layer tscircuit board (
<board layers={4}>) where the autorouter places traces oninner1/inner2. Confirm viacircuit.json:Run
bunx tsci export <file> --format gerbers --output ../board.zipand inspect the zip:No
In1_Cu.gbr/In2_Cu.gbr. The two inner copper layers' traces are absent from the output.Where it's missing
Across the package, the type system, layer maps, and rendering loops all hardcode
top/bottom:src/gerber/convert-soup-to-gerber-commands/GerberLayerName.ts—LayerToGerberCommandsMaponly declares F_Cu / B_Cu / etc.src/gerber/convert-soup-to-gerber-commands/getGerberLayerName.ts—layerRefToGerberPrefixhas only{ top: "F_", bottom: "B_" }.src/gerber/convert-soup-to-gerber-commands/getCommandHeaders.ts— has the// TODO inner layerscomment alongside aninner1 | inner2 | inner3 | inner4accepted-layer union, but the correspondinglayerAndTypeToFileFunctionentries are missing.src/gerber/convert-soup-to-gerber-commands/getAllTraceWidths.ts— return shape is{ top, bottom }only.src/gerber/convert-soup-to-gerber-commands/index.ts—glayersobject, the aperture-defining loop (~line 79), and two trace/via render loops (~lines 456 and 594) are hardcoded to top/bottom.src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts—glayer_name.startsWith("F_") ? "top" : "bottom"collapses inner layers into the bottom bucket;getAllApertureTemplateConfigsForLayeronly accepts"top" | "bottom".Proposed fix
I have a working patch on
kcknox/circuit-json-to-gerberbranchfeature/inner-layer-gerbersthat:In1_Cu/In2_CutoLayerToGerberCommandsMap.inner1: "In1_"/inner2: "In2_"tolayerRefToGerberPrefix.inner1-copper: "Copper,L2,Inr"/inner2-copper: "Copper,L3,Inr"tolayerAndTypeToFileFunction(Gerber X2 file-function attribute).inner1/inner2keys togetAllTraceWidthsreturn shape.In1_Cu/In2_Cuheaders to theglayersobject and includes them in the aperture-defining loop.["top", "inner1", "inner2", "bottom"](and+ "edgecut"for the second), with early-continueguards so handlers forpcb_smtpad/pcb_solder_paste/pcb_silkscreen_*/pcb_holeskip inner layers (they only carry copper traces, vias, plated holes, and copper text).getApertureConfigsForLayeranddefineAperturesForLayer's layer-bucket lookup to handle inner layers.After the patch, exporting a 4-layer board produces:
26/27 existing tests pass (the 1 failing test,
generate-gerber-macrokeypad, was already failing onmainbefore my changes — looks like a pre-existing 20s timeout / snapshot drift issue unrelated to this).Questions before opening a PR
A few design questions I'd want maintainer input on before sending the diff:
inner1+inner2because that covers 4-layer (the common case for medium-density designs) and the existinggetCommandHeadersalready accepts theinner1..4union. Should it generalise toinner1..6(12-layer support) on day one, or is incremental fine?.FileFunctionLknumbering. I leftbottom-copperasCopper,L2,Botrather than rewriting it toL4,Botfor 4-layer boards, since the layer count isn't always known at gerber-emit time. Most fabs identify stack position from the filename, not this attribute. OK?In1_Cu.gbr/In2_Cu.gbrand expect Protel-style.g1/.g2. Shouldcircuit-json-to-gerberkeep emitting.gbr(current convention) and lettscircuit/cli's zip-write step handle the rename, or should this package own the extension mapping?Happy to open the PR once there's a thumbs-up on direction. Branch is ready at https://github.com/kcknox/circuit-json-to-gerber/tree/feature/inner-layer-gerbers — no PR opened yet to avoid blind-PRing.