Gerber export decides each component's layer in one place, and Gerber layers can be mapped to the other side of the board

This commit is contained in:
Bane Stojkovic
2026-11-11 14:46:02 +01:00
parent 39c7743c30
commit 324cb20634
6 changed files with 190 additions and 44 deletions
@@ -145,8 +145,12 @@ public class GerberExporter {
LOG.info("Completed export to gerber"); LOG.info("Completed export to gerber");
} }
private static void drawComponent(Project currentProject, GerberG2DWrapper g2d, /**
IDIYComponent<?> c, boolean outlineMode) { * Draws the specified component into the Gerber layers it belongs to. This is the only place that
* decides which layer a component's output goes to.
*/
static void drawComponent(Project currentProject, GerberG2DWrapper g2d,
IDIYComponent<?> c, boolean outlineMode) {
IGerberComponent gerberComponent = (IGerberComponent) c; IGerberComponent gerberComponent = (IGerberComponent) c;
g2d.startedDrawingComponent(); g2d.startedDrawingComponent();
if (gerberComponent instanceof IGerberComponentCustom) { if (gerberComponent instanceof IGerberComponentCustom) {
@@ -55,6 +55,29 @@ public enum GerberLayer {
return mirrored; return mirrored;
} }
/**
* @return the same layer on the other side of the board; the outline and the drill layers go
* through the whole board and are returned unchanged
*/
public GerberLayer flip() {
switch (this) {
case CopperBot:
return CopperTop;
case CopperTop:
return CopperBot;
case SilkscreenBot:
return SilkscreenTop;
case SilkscreenTop:
return SilkscreenBot;
case SolderMaskBot:
return SolderMaskTop;
case SolderMaskTop:
return SolderMaskBot;
default:
return this;
}
}
public String formatFileName(String fileNameBase, String boardName) { public String formatFileName(String fileNameBase, String boardName) {
return fileNameBase + (fileNameBase.endsWith(".") ? "" : ".") return fileNameBase + (fileNameBase.endsWith(".") ? "" : ".")
+ boardName + "-" + getSuffix() + "." + getExtension(); + boardName + "-" + getSuffix() + "." + getExtension();
@@ -25,29 +25,10 @@ import com.bancika.gerberwriter.Point;
import com.bancika.gerberwriter.path.MoveTo; import com.bancika.gerberwriter.path.MoveTo;
import com.bancika.gerberwriter.path.Path; import com.bancika.gerberwriter.path.Path;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.core.measures.SizeUnit; import org.diylc.core.measures.SizeUnit;
public class GerberPathRenderer { public class GerberPathRenderer {
public static void drawComponent(Project currentProject, GerberG2DWrapper g2d, IDIYComponent<?> c,
boolean outlineMode) {
IGerberComponent gerberComponent = (IGerberComponent) c;
g2d.startedDrawingComponent();
if (gerberComponent instanceof IGerberComponentCustom) {
IGerberComponentCustom gerberComponentCustom = (IGerberComponentCustom) gerberComponent;
gerberComponentCustom.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d, g2d);
} else if (gerberComponent instanceof IGerberComponentSimple) {
IGerberComponentSimple gerberComponentSimple = (IGerberComponentSimple) gerberComponent;
g2d.startGerberOutput(gerberComponentSimple.getGerberLayer(),
gerberComponentSimple.getGerberFunction(), gerberComponentSimple.isGerberNegative());
c.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d);
g2d.stopGerberOutput();
}
}
public static void outputPathArea(PathIterator pathIterator, DataLayer dataLayer, double d, public static void outputPathArea(PathIterator pathIterator, DataLayer dataLayer, double d,
boolean isNegative, String function) { boolean isNegative, String function) {
List<PathWithNegative> outputPath = List<PathWithNegative> outputPath =
@@ -0,0 +1,88 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DIYLC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.core.gerber;
import static org.junit.Assert.assertEquals;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.EnumSet;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.testcomponents.MockDIYComponent;
import org.junit.Test;
public class GerberExporterTest {
@Test
public void testSimpleComponentGoesToSilkscreenTopByDefault() {
GerberG2DWrapper g2d = draw(new SimpleGerberComponent(null));
assertEquals(EnumSet.of(GerberLayer.SilkscreenTop), EnumSet.copyOf(g2d.getLayerMap().keySet()));
}
@Test
public void testSimpleComponentGoesToItsOwnLayer() {
GerberG2DWrapper g2d = draw(new SimpleGerberComponent(GerberLayer.CopperBot));
assertEquals(EnumSet.of(GerberLayer.CopperBot), EnumSet.copyOf(g2d.getLayerMap().keySet()));
}
private static GerberG2DWrapper draw(SimpleGerberComponent component) {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics();
GerberG2DWrapper g2d =
new GerberG2DWrapper(graphics, "test", new Rectangle2D.Double(0, 0, 100, 100));
GerberExporter.drawComponent(new Project(), g2d, component, false);
graphics.dispose();
return g2d;
}
public static class SimpleGerberComponent extends MockDIYComponent
implements IGerberComponentSimple {
private static final long serialVersionUID = 1L;
// null uses the interface default
private GerberLayer layer;
public SimpleGerberComponent(GerberLayer layer) {
this.layer = layer;
}
@Override
public GerberLayer getGerberLayer() {
return layer == null ? IGerberComponentSimple.super.getGerberLayer() : layer;
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
g2d.setColor(Color.black);
g2d.fill(new Rectangle2D.Double(10, 10, 20, 20));
}
}
}
@@ -0,0 +1,53 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DIYLC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.core.gerber;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class GerberLayerTest {
@Test
public void testFlipSwapsSides() {
assertEquals(GerberLayer.CopperBot, GerberLayer.CopperTop.flip());
assertEquals(GerberLayer.CopperTop, GerberLayer.CopperBot.flip());
assertEquals(GerberLayer.SilkscreenBot, GerberLayer.SilkscreenTop.flip());
assertEquals(GerberLayer.SilkscreenTop, GerberLayer.SilkscreenBot.flip());
assertEquals(GerberLayer.SolderMaskBot, GerberLayer.SolderMaskTop.flip());
assertEquals(GerberLayer.SolderMaskTop, GerberLayer.SolderMaskBot.flip());
}
@Test
public void testFlipKeepsLayersThroughTheBoard() {
assertEquals(GerberLayer.Outline, GerberLayer.Outline.flip());
assertEquals(GerberLayer.DrillPlated, GerberLayer.DrillPlated.flip());
assertEquals(GerberLayer.DrillNonPlated, GerberLayer.DrillNonPlated.flip());
}
@Test
public void testFlipTwiceReturnsLayer() {
for (GerberLayer layer : GerberLayer.values()) {
assertEquals(layer, layer.flip().flip());
}
}
}
+20 -23
View File
@@ -3,7 +3,8 @@
Let a component be mounted on the bottom of a PCB, so that its silkscreen goes to the bottom Let a component be mounted on the bottom of a PCB, so that its silkscreen goes to the bottom
Gerber layers and the canvas shows it as a part on the far side of the board. Gerber layers and the canvas shows it as a part on the far side of the board.
Status: not started. Status: the Gerber dispatch is a single method and `GerberLayer.flip()` exists, with tests; the
next steps below remain.
## 1. Current state ## 1. Current state
@@ -19,9 +20,10 @@ Status: not started.
default (resistors, film and radial capacitors, diodes, LEDs, transistor packages, DIL/SIL ICs, default (resistors, film and radial capacitors, diodes, LEDs, transistor packages, DIL/SIL ICs,
SMD passives, switches, jacks, relays, transformers, tube sockets, ...). None of them emit copper. SMD passives, switches, jacks, relays, transformers, tube sockets, ...). None of them emit copper.
Their pads are separate `SolderPad` components, which already have a layer. Their pads are separate `SolderPad` components, which already have a layer.
- Two copies of the same dispatch decide the layer for a simple component: - One method decides the layer for a simple component: `GerberExporter.drawComponent`, which
`GerberExporter.drawComponent` and `GerberPathRenderer.drawComponent`. Both call calls `g2d.startGerberOutput(getGerberLayer(), ...)`. It is package-private so that
`g2d.startGerberOutput(getGerberLayer(), ...)`. `GerberExporterTest` can call it. `GerberLayer.flip()` maps a layer to the other side of the
board.
- Z-order is paint order only. A trace on the bottom layer already paints above the board - Z-order is paint order only. A trace on the bottom layer already paints above the board
(`zOrder = TRACE`), so the codebase already treats the physical side as separate from z-order. (`zOrder = TRACE`), so the codebase already treats the physical side as separate from z-order.
@@ -44,12 +46,8 @@ Status: not started.
- **`org.diylc.core.IBoardSideComponent`**: `PCBLayer getBoardSide();` with Javadoc and - **`org.diylc.core.IBoardSideComponent`**: `PCBLayer getBoardSide();` with Javadoc and
`@author`, in the same shape as `ILayeredComponent` but deliberately separate from it (see `@author`, in the same shape as `ILayeredComponent` but deliberately separate from it (see
Decisions). It returns `_1` or `_2` only. Core knows the side only through this interface. Decisions). It returns `_1` or `_2` only. Core knows the side only through this interface.
- **`GerberLayer.flip()`**: maps `CopperTop`/`CopperBot`, `SilkscreenTop`/`SilkscreenBot` and - **Gerber dispatch**: in the simple-component branch of `GerberExporter.drawComponent`, use
`SolderMaskTop`/`SolderMaskBot` to each other. `Outline` and the drill layers map to themselves. `getGerberLayer().flip()` when the component is an `IBoardSideComponent` on `_1`.
- **Gerber dispatch**: in the simple-component branch, use
`getGerberLayer().flip()` when the component is an `IBoardSideComponent` on `_1`. Merge
`GerberExporter.drawComponent` and `GerberPathRenderer.drawComponent` into one method first, so
the rule is written once.
- **Canvas rendering**: bottom-side components use the far-side fade from `copper-side-view.md`, - **Canvas rendering**: bottom-side components use the far-side fade from `copper-side-view.md`,
wrapping their painting in `startDrawingSide(getBoardSide())`. They are then faded in the main wrapping their painting in `startDrawingSide(getBoardSide())`. They are then faded in the main
view and solid in the Underside Display mirror, the same as bottom copper. view and solid in the Underside Display mirror, the same as bottom copper.
@@ -135,8 +133,8 @@ order.
## 5. Verification ## 5. Verification
- Unit tests in `diylc-core` for `GerberLayer.flip()` and for the merged dispatch method, using a - `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components;
stub `IBoardSideComponent` (top, bottom and `null`). extend it with a stub `IBoardSideComponent` (top, bottom and `null`).
- Load an old `.diy` with PCB parts and confirm every component reports `_2` (Top) and that the Gerber - Load an old `.diy` with PCB parts and confirm every component reports `_2` (Top) and that the Gerber
output is byte-identical to before. output is byte-identical to before.
- Export a board with one part on each side and open the files in a Gerber viewer: the bottom part - Export a board with one part on each side and open the files in a Gerber viewer: the bottom part
@@ -150,16 +148,15 @@ order.
## 6. Next steps ## 6. Next steps
1. Merge the two `drawComponent` copies in `diylc-core` and add `GerberLayer.flip()` with tests. 1. Add `IBoardSideComponent` and swap the Gerber layer in the dispatch.
2. Add `IBoardSideComponent` and swap the Gerber layer in the dispatch. 2. Add the field to the library components, putting it on shared bases where they exist. Settle
3. Add the field to the library components, putting it on shared bases where they exist. Settle
the silkscreen-text question while doing so. the silkscreen-text question while doing so.
4. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. 3. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`.
5. Add the Flip to Other Side action. 4. Add the Flip to Other Side action.
6. Split the silkscreen print-out by side. 5. Split the silkscreen print-out by side.
7. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`. 6. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`.
8. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-6 and can ship 7. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-5 and can ship
early. early.
9. Mirror stage 2: dragging through the mirror. 8. Mirror stage 2: dragging through the mirror.
10. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. 9. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
11. Run the regression suite. 10. Run the regression suite.