Resistors, capacitors, diodes, ICs, transistors and other PCB parts can be placed on the bottom of the board with the new 'Board Side' property; Gerber export puts their silkscreen on the bottom layer with the text mirrored

This commit is contained in:
Bane Stojkovic
2026-11-11 15:15:19 +01:00
parent 16f54fa479
commit df873df04a
36 changed files with 498 additions and 54 deletions
@@ -162,13 +162,16 @@ public class GerberExporter {
IGerberComponentSimple gerberComponentSimple = (IGerberComponentSimple) gerberComponent;
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) {
boolean bottomSide = c instanceof IBoardSideComponent
&& ((IBoardSideComponent) c).getBoardSide() == PCBLayer._1;
if (bottomSide) {
layer = layer.flip();
}
g2d.startGerberOutput(layer, gerberComponentSimple.getGerberFunction(),
gerberComponentSimple.isGerberNegative());
g2d.setMirrorText(bottomSide);
c.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d);
g2d.setMirrorText(false);
g2d.stopGerberOutput();
}
}
@@ -58,6 +58,7 @@ public class GerberG2DWrapper extends Graphics2D
private double tolerance;
private Rectangle2D boardRect;
private boolean mirrorText = false;
public GerberG2DWrapper(Graphics2D graphics2d, String diylcVersion, Rectangle2D boardRect) {
super();
@@ -154,6 +155,15 @@ public class GerberG2DWrapper extends Graphics2D
return layerMap;
}
/**
* Sets whether text is written mirrored, each string about its own centre. The output is in top
* view, so text on the bottom side has to be mirrored to read correctly once the board is turned
* over.
*/
public void setMirrorText(boolean mirrorText) {
this.mirrorText = mirrorText;
}
@Override
public void draw(Shape s) {
if (trackingGerber && !getColor().equals(Constants.TRANSPARENT_COLOR)) {
@@ -181,6 +191,10 @@ public class GerberG2DWrapper extends Graphics2D
GlyphVector gv = font.createGlyphVector(graphics2d.getFontRenderContext(), str);
AffineTransform translateInstance = AffineTransform.getTranslateInstance(x, y);
if (mirrorText) {
translateInstance.translate(gv.getLogicalBounds().getWidth(), 0);
translateInstance.scale(-1, 1);
}
for (int i = 0; i < gv.getNumGlyphs(); i++) {
Shape glyphShape = gv.getGlyphOutline(i);
@@ -22,12 +22,17 @@
package org.diylc.core.gerber;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import org.diylc.common.PCBLayer;
import org.diylc.core.ComponentState;
@@ -74,6 +79,49 @@ public class GerberExporterTest {
assertEquals(EnumSet.of(GerberLayer.SilkscreenTop), EnumSet.copyOf(g2d.getLayerMap().keySet()));
}
@Test
public void testTopSideTextReadsLeftToRight() {
List<Rectangle2D> glyphs = drawText(PCBLayer._2);
assertTrue(glyphs.get(0).getMinX() < glyphs.get(1).getMinX());
}
@Test
public void testBottomSideTextIsMirrored() {
List<Rectangle2D> top = drawText(PCBLayer._2);
List<Rectangle2D> bottom = drawText(PCBLayer._1);
// seen from the top, the first letter of a bottom-side label is on the right
assertTrue(bottom.get(0).getMinX() > bottom.get(1).getMinX());
// mirrored about the centre of its layout box, so the text stays where it was; the glyphs'
// uneven side bearings move their drawn bounds by a pixel or so
assertEquals(union(top).getCenterX(), union(bottom).getCenterX(), 2);
}
private static List<Rectangle2D> drawText(PCBLayer side) {
List<Rectangle2D> glyphs = new ArrayList<Rectangle2D>();
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)) {
@Override
public void fillShape(Shape s) {
glyphs.add(s.getBounds2D());
}
};
GerberExporter.drawComponent(new Project(), g2d, new TextGerberComponent(side), false);
graphics.dispose();
return glyphs;
}
private static Rectangle2D union(List<Rectangle2D> rects) {
Rectangle2D union = (Rectangle2D) rects.get(0).clone();
for (Rectangle2D rect : rects) {
union.add(rect);
}
return union;
}
private static GerberG2DWrapper draw(SimpleGerberComponent component) {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics();
@@ -126,4 +174,21 @@ public class GerberExporterTest {
return side;
}
}
public static class TextGerberComponent extends SidedGerberComponent {
private static final long serialVersionUID = 1L;
public TextGerberComponent(PCBLayer side) {
super(side);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
g2d.setColor(Color.black);
g2d.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 20));
g2d.drawString("AB", 10, 50);
}
}
}