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 org.diylc.common.PCBLayer;
import org.diylc.core.ComponentState;
import org.diylc.core.IBoard;
import org.diylc.core.IBoardSideComponent;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IView;
import org.diylc.core.Project;
@@ -158,8 +160,14 @@ public class GerberExporter {
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());
GerberLayer layer = gerberComponentSimple.getGerberLayer();
// 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);
g2d.stopGerberOutput();
}
@@ -29,7 +29,9 @@ import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.EnumSet;
import org.diylc.common.PCBLayer;
import org.diylc.core.ComponentState;
import org.diylc.core.IBoardSideComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.testcomponents.MockDIYComponent;
@@ -51,6 +53,27 @@ public class GerberExporterTest {
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) {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics();
@@ -85,4 +108,22 @@ public class GerberExporterTest {
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
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
next steps below remain.
Status: the core Gerber side is done, with tests: `IBoardSideComponent` exists and the export
sends a bottom-side part to the bottom layers. No component implements it yet; the next steps below
remain.
## 1. Current state
@@ -20,10 +21,12 @@ next steps below remain.
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.
- 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.
- One method decides the layer for a simple component: `GerberExporter.drawComponent`. It is
package-private so that `GerberExporterTest` can call it. It uses `getGerberLayer()`, flipped
with `GerberLayer.flip()` when the component is an `IBoardSideComponent` on `_1` (Bottom).
- **`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
(`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
- **`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`,
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.
@@ -133,8 +131,8 @@ order.
## 5. Verification
- `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components;
extend it with a stub `IBoardSideComponent` (top, bottom and `null`).
- `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components,
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
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
@@ -148,15 +146,14 @@ order.
## 6. Next steps
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
1. Add the field to the library components, putting it on shared bases where they exist. Settle
the silkscreen-text question while doing so.
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
2. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`.
3. Add the Flip to Other Side action.
4. Split the silkscreen print-out by side.
5. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`.
6. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-4 and can ship
early.
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.
7. Mirror stage 2: dragging through the mirror.
8. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
9. Run the regression suite.