From 324cb206340a1dcc03a06022e161eab36a082ffa Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Wed, 11 Nov 2026 14:46:02 +0100 Subject: [PATCH] Gerber export decides each component's layer in one place, and Gerber layers can be mapped to the other side of the board --- .../org/diylc/core/gerber/GerberExporter.java | 8 +- .../org/diylc/core/gerber/GerberLayer.java | 23 +++++ .../diylc/core/gerber/GerberPathRenderer.java | 19 ---- .../diylc/core/gerber/GerberExporterTest.java | 88 +++++++++++++++++++ .../diylc/core/gerber/GerberLayerTest.java | 53 +++++++++++ diylc/docs/plans/bottom-side-components.md | 43 +++++---- 6 files changed, 190 insertions(+), 44 deletions(-) create mode 100644 diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java create mode 100644 diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberLayerTest.java diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java index e041e9e29..a19ddffa0 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java @@ -145,8 +145,12 @@ public class GerberExporter { 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; g2d.startedDrawingComponent(); if (gerberComponent instanceof IGerberComponentCustom) { diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberLayer.java b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberLayer.java index 6a7c4c110..e4aca640b 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberLayer.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberLayer.java @@ -54,6 +54,29 @@ public enum GerberLayer { public boolean isMirrored() { 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) { return fileNameBase + (fileNameBase.endsWith(".") ? "" : ".") diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberPathRenderer.java b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberPathRenderer.java index bc47380ef..325dc9dcf 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberPathRenderer.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberPathRenderer.java @@ -25,29 +25,10 @@ import com.bancika.gerberwriter.Point; import com.bancika.gerberwriter.path.MoveTo; 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; 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, boolean isNegative, String function) { List outputPath = diff --git a/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java b/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java new file mode 100644 index 000000000..e76abe62f --- /dev/null +++ b/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java @@ -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 . + +*/ +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)); + } + } +} diff --git a/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberLayerTest.java b/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberLayerTest.java new file mode 100644 index 000000000..a3b5da1b5 --- /dev/null +++ b/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberLayerTest.java @@ -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 . + +*/ +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()); + } + } +} diff --git a/diylc/docs/plans/bottom-side-components.md b/diylc/docs/plans/bottom-side-components.md index deef5be37..907a6c7db 100644 --- a/diylc/docs/plans/bottom-side-components.md +++ b/diylc/docs/plans/bottom-side-components.md @@ -3,7 +3,8 @@ 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. -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 @@ -19,9 +20,10 @@ Status: not started. 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. Their pads are separate `SolderPad` components, which already have a layer. -- Two copies of the same dispatch decide the layer for a simple component: - `GerberExporter.drawComponent` and `GerberPathRenderer.drawComponent`. Both call - `g2d.startGerberOutput(getGerberLayer(), ...)`. +- One method decides the layer for a simple component: `GerberExporter.drawComponent`, which + calls `g2d.startGerberOutput(getGerberLayer(), ...)`. It is package-private so that + `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 (`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 `@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. -- **`GerberLayer.flip()`**: maps `CopperTop`/`CopperBot`, `SilkscreenTop`/`SilkscreenBot` and - `SolderMaskTop`/`SolderMaskBot` to each other. `Outline` and the drill layers map to themselves. -- **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. +- **Gerber dispatch**: in the simple-component branch of `GerberExporter.drawComponent`, use + `getGerberLayer().flip()` when the component is an `IBoardSideComponent` on `_1`. - **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 view and solid in the Underside Display mirror, the same as bottom copper. @@ -135,8 +133,8 @@ order. ## 5. Verification -- Unit tests in `diylc-core` for `GerberLayer.flip()` and for the merged dispatch method, using a - stub `IBoardSideComponent` (top, bottom and `null`). +- `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components; + 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 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 @@ -150,16 +148,15 @@ order. ## 6. Next steps -1. Merge the two `drawComponent` copies in `diylc-core` and add `GerberLayer.flip()` with tests. -2. Add `IBoardSideComponent` and swap the Gerber layer in the dispatch. -3. Add the field to the library components, putting it on shared bases where they exist. Settle +1. 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 the silkscreen-text question while doing so. -4. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. -5. Add the Flip to Other Side action. -6. Split the silkscreen print-out by side. -7. 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 +3. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. +4. Add the Flip to Other Side action. +5. Split the silkscreen print-out by side. +6. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`. +7. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-5 and can ship early. -9. Mirror stage 2: dragging through the mirror. -10. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. -11. Run the regression suite. +8. Mirror stage 2: dragging through the mirror. +9. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. +10. Run the regression suite.