A component can report which side of the board it is mounted on, and Gerber export puts the silkscreen of a part mounted underneath on the bottom layer

This commit is contained in:
Bane Stojkovic
2026-11-11 14:48:57 +01:00
parent 324cb20634
commit 3506c97403
4 changed files with 113 additions and 25 deletions
@@ -0,0 +1,42 @@
/*
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;
import org.diylc.common.PCBLayer;
/**
* Interface for components mounted on one side of a board, such as a resistor or an IC. The side
* says where the part sits physically; it is separate from {@link ILayeredComponent}, which drives
* continuity, because the leads of a through-hole part connect to copper on both sides whichever
* side the part is mounted on. Components whose copper already has a layer (traces, pads, ground
* fills) do not implement it.
*
* @author Branislav Stojkovic
*/
public interface IBoardSideComponent {
/**
* @return {@link PCBLayer#_2} for the top of the board or {@link PCBLayer#_1} for the bottom,
* never an inner layer or null
*/
PCBLayer getBoardSide();
}
@@ -16,8 +16,10 @@ import org.apache.log4j.Logger;
import com.bancika.gerberwriter.DataLayer; import com.bancika.gerberwriter.DataLayer;
import org.diylc.common.PCBLayer;
import org.diylc.core.ComponentState; import org.diylc.core.ComponentState;
import org.diylc.core.IBoard; import org.diylc.core.IBoard;
import org.diylc.core.IBoardSideComponent;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.core.IView; import org.diylc.core.IView;
import org.diylc.core.Project; import org.diylc.core.Project;
@@ -158,8 +160,14 @@ public class GerberExporter {
gerberComponentCustom.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d, g2d); gerberComponentCustom.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d, g2d);
} else if (gerberComponent instanceof IGerberComponentSimple) { } else if (gerberComponent instanceof IGerberComponentSimple) {
IGerberComponentSimple gerberComponentSimple = (IGerberComponentSimple) gerberComponent; IGerberComponentSimple gerberComponentSimple = (IGerberComponentSimple) gerberComponent;
g2d.startGerberOutput(gerberComponentSimple.getGerberLayer(), GerberLayer layer = gerberComponentSimple.getGerberLayer();
gerberComponentSimple.getGerberFunction(), gerberComponentSimple.isGerberNegative()); // components emit their top-side layer; a part mounted underneath goes to the bottom one
if (c instanceof IBoardSideComponent
&& ((IBoardSideComponent) c).getBoardSide() == PCBLayer._1) {
layer = layer.flip();
}
g2d.startGerberOutput(layer, gerberComponentSimple.getGerberFunction(),
gerberComponentSimple.isGerberNegative());
c.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d); c.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d);
g2d.stopGerberOutput(); g2d.stopGerberOutput();
} }
@@ -29,7 +29,9 @@ import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.EnumSet; import java.util.EnumSet;
import org.diylc.common.PCBLayer;
import org.diylc.core.ComponentState; import org.diylc.core.ComponentState;
import org.diylc.core.IBoardSideComponent;
import org.diylc.core.IDrawingObserver; import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project; import org.diylc.core.Project;
import org.diylc.testcomponents.MockDIYComponent; import org.diylc.testcomponents.MockDIYComponent;
@@ -51,6 +53,27 @@ public class GerberExporterTest {
assertEquals(EnumSet.of(GerberLayer.CopperBot), EnumSet.copyOf(g2d.getLayerMap().keySet())); assertEquals(EnumSet.of(GerberLayer.CopperBot), EnumSet.copyOf(g2d.getLayerMap().keySet()));
} }
@Test
public void testTopSideComponentKeepsItsLayer() {
GerberG2DWrapper g2d = draw(new SidedGerberComponent(PCBLayer._2));
assertEquals(EnumSet.of(GerberLayer.SilkscreenTop), EnumSet.copyOf(g2d.getLayerMap().keySet()));
}
@Test
public void testBottomSideComponentGoesToTheOtherSide() {
GerberG2DWrapper g2d = draw(new SidedGerberComponent(PCBLayer._1));
assertEquals(EnumSet.of(GerberLayer.SilkscreenBot), EnumSet.copyOf(g2d.getLayerMap().keySet()));
}
@Test
public void testComponentWithoutSideStaysOnTop() {
GerberG2DWrapper g2d = draw(new SidedGerberComponent(null));
assertEquals(EnumSet.of(GerberLayer.SilkscreenTop), EnumSet.copyOf(g2d.getLayerMap().keySet()));
}
private static GerberG2DWrapper draw(SimpleGerberComponent component) { private static GerberG2DWrapper draw(SimpleGerberComponent component) {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics(); Graphics2D graphics = img.createGraphics();
@@ -85,4 +108,22 @@ public class GerberExporterTest {
g2d.fill(new Rectangle2D.Double(10, 10, 20, 20)); g2d.fill(new Rectangle2D.Double(10, 10, 20, 20));
} }
} }
public static class SidedGerberComponent extends SimpleGerberComponent
implements IBoardSideComponent {
private static final long serialVersionUID = 1L;
private PCBLayer side;
public SidedGerberComponent(PCBLayer side) {
super(null);
this.side = side;
}
@Override
public PCBLayer getBoardSide() {
return side;
}
}
} }
+20 -23
View File
@@ -3,8 +3,9 @@
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: the Gerber dispatch is a single method and `GerberLayer.flip()` exists, with tests; the Status: the core Gerber side is done, with tests: `IBoardSideComponent` exists and the export
next steps below remain. sends a bottom-side part to the bottom layers. No component implements it yet; the next steps below
remain.
## 1. Current state ## 1. Current state
@@ -20,10 +21,12 @@ next steps below remain.
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.
- One method decides the layer for a simple component: `GerberExporter.drawComponent`, which - One method decides the layer for a simple component: `GerberExporter.drawComponent`. It is
calls `g2d.startGerberOutput(getGerberLayer(), ...)`. It is package-private so that package-private so that `GerberExporterTest` can call it. It uses `getGerberLayer()`, flipped
`GerberExporterTest` can call it. `GerberLayer.flip()` maps a layer to the other side of the with `GerberLayer.flip()` when the component is an `IBoardSideComponent` on `_1` (Bottom).
board. - **`org.diylc.core.IBoardSideComponent`**: `PCBLayer getBoardSide()`, returning `_1` or `_2`
only. It is deliberately separate from `ILayeredComponent` (see Decisions). Core knows a part's
side only through this interface.
- 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.
@@ -43,11 +46,6 @@ next steps below remain.
### diylc-core ### diylc-core
- **`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.
- **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`, - **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.
@@ -133,8 +131,8 @@ order.
## 5. Verification ## 5. Verification
- `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components; - `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components,
extend it with a stub `IBoardSideComponent` (top, bottom and `null`). including a stub `IBoardSideComponent` on 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
@@ -148,15 +146,14 @@ order.
## 6. Next steps ## 6. Next steps
1. Add `IBoardSideComponent` and swap the Gerber layer in the dispatch. 1. Add the field to the library components, putting it on shared bases where they exist. Settle
2. 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.
3. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. 2. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`.
4. Add the Flip to Other Side action. 3. Add the Flip to Other Side action.
5. Split the silkscreen print-out by side. 4. Split the silkscreen print-out by side.
6. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`. 5. 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 6. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-4 and can ship
early. early.
8. Mirror stage 2: dragging through the mirror. 7. Mirror stage 2: dragging through the mirror.
9. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. 8. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
10. Run the regression suite. 9. Run the regression suite.