Gerber export writes drill files in Excellon format, picked by a constant in GerberExporter, through gerber-writer 1.2.0; a Drill Hole no longer adds its outline and crosshair to the drill file

This commit is contained in:
Bane Stojkovic
2026-11-12 11:20:41 +01:00
parent 34490b50b2
commit 2f888a0090
11 changed files with 130 additions and 23 deletions
+2 -2
View File
@@ -97,10 +97,10 @@
</dependency>
<dependency>
<groupId>org.diylc.local</groupId>
<artifactId>gerber-writer-api-1.1.0</artifactId>
<artifactId>gerber-writer-api-1.2.0</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/../lib/gerber-writer-api-1.1.0.jar</systemPath>
<systemPath>${project.basedir}/../lib/gerber-writer-api-1.2.0.jar</systemPath>
</dependency>
<dependency>
<groupId>org.diylc.local</groupId>
@@ -15,6 +15,7 @@ import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import com.bancika.gerberwriter.DataLayer;
import com.bancika.gerberwriter.DrillFormat;
import org.diylc.common.PCBLayer;
import org.diylc.core.ComponentState;
@@ -32,6 +33,9 @@ public class GerberExporter {
public static final double OUTLINE_THICKNESS = new Size(0.3d, SizeUnit.mm).convertToPixels();
private static final Logger LOG = Logger.getLogger(GerberExporter.class);
// Excellon is the drill format every fab accepts; Gerber drill files are not read by all of them
private static final DrillFormat DRILL_FORMAT = DrillFormat.EXCELLON;
public static void exportGerber(String fileNameBase, Project currentProject, IView view,
Graphics2D graphics2d, String diylcVersion) {
@@ -137,6 +141,23 @@ public class GerberExporter {
return;
}
});
g2d.getDrillLayerMap().forEach((layer, drillLayer) -> {
try {
String fileName = layer.formatFileName(fileNameBase, boardComponent.getName(),
DRILL_FORMAT == DrillFormat.EXCELLON ? "drl" : "gbr");
LOG.info("Exporting layer: " + layer + " for board " + boardComponent.getName()
+ " to file: " + fileName);
drillLayer.dumpToFile(fileName, DRILL_FORMAT);
generatedFiles.add(fileName);
} catch (IOException e) {
LOG.error("Error writing drill file: " + e.getMessage());
view.showMessage(
LangUtil.translate(
"Failed to export the project to gerber. Please check the log for details"),
LangUtil.translate("Gerber Export"), IView.ERROR_MESSAGE);
}
});
});
view.showMessage(
String.format(
@@ -21,6 +21,7 @@ import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
@@ -32,6 +33,7 @@ import java.util.HashMap;
import java.util.Map;
import com.bancika.gerberwriter.DataLayer;
import com.bancika.gerberwriter.DrillLayer;
import com.bancika.gerberwriter.Point;
import com.bancika.gerberwriter.padmasters.Circle;
@@ -45,6 +47,7 @@ public class GerberG2DWrapper extends Graphics2D
private static final double CURVE_APPROXIMATION_TOLERANCE = 1d;
private Map<GerberLayer, DataLayer> layerMap;
private Map<GerberLayer, DrillLayer> drillLayerMap;
private boolean trackingGerber = false;
private Map<GerberLayer, GerberProperties> currentLayers =
new HashMap<GerberLayer, GerberProperties>();
@@ -65,6 +68,7 @@ public class GerberG2DWrapper extends Graphics2D
this.graphics2d = graphics2d;
this.boardRect = boardRect;
this.layerMap = new HashMap<GerberLayer, DataLayer>();
this.drillLayerMap = new HashMap<GerberLayer, DrillLayer>();
this.diylcVersion = diylcVersion;
}
@@ -115,7 +119,11 @@ public class GerberG2DWrapper extends Graphics2D
@Override
public void startGerberOutput(GerberLayer layer, String function, boolean negative) {
if (layer.isDrill()) {
drillLayerMap.computeIfAbsent(layer, k -> layer.buildDrillLayer(diylcVersion));
} else {
layerMap.computeIfAbsent(layer, k -> layer.buildLayer(diylcVersion));
}
currentLayers.put(layer, new GerberProperties(function, negative));
trackingGerber = true;
}
@@ -155,6 +163,10 @@ public class GerberG2DWrapper extends Graphics2D
return layerMap;
}
public Map<GerberLayer, DrillLayer> getDrillLayerMap() {
return drillLayerMap;
}
/**
* 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
@@ -168,6 +180,11 @@ public class GerberG2DWrapper extends Graphics2D
public void draw(Shape s) {
if (trackingGerber && !getColor().equals(Constants.TRANSPARENT_COLOR)) {
currentLayers.entrySet().forEach(entry -> {
// only round fills are holes; outlines, such as a drill hole marker's border and
// crosshair, are not drilled
if (entry.getKey().isDrill()) {
return;
}
double width = 1;
if (stroke != null && stroke instanceof BasicStroke) {
width = ((BasicStroke) stroke).getLineWidth();
@@ -222,6 +239,10 @@ public class GerberG2DWrapper extends Graphics2D
public void fillShape(Shape s) {
if (trackingGerber && !getColor().equals(Constants.TRANSPARENT_COLOR)) {
currentLayers.entrySet().forEach(entry -> {
// only round fills are holes
if (entry.getKey().isDrill()) {
return;
}
DataLayer dataLayer = layerMap.get(entry.getKey());
AffineTransform finalTx = getFinalTx(entry.getKey().isMirrored());
GerberPathRenderer.outputPathArea(s.getPathIterator(finalTx), dataLayer,
@@ -234,6 +255,10 @@ public class GerberG2DWrapper extends Graphics2D
private void fillFlash(Ellipse2D c) {
if (trackingGerber && !getColor().equals(Constants.TRANSPARENT_COLOR)) {
currentLayers.entrySet().forEach(entry -> {
if (entry.getKey().isDrill()) {
addHole(c, entry.getKey(), entry.getValue().function);
return;
}
DataLayer dataLayer = layerMap.get(entry.getKey());
AffineTransform finalTx = getFinalTx(entry.getKey().isMirrored());
if (Math.abs(c.getWidth() - c.getHeight()) < 0.1 && finalTx.getShearX() == 0
@@ -255,6 +280,17 @@ public class GerberG2DWrapper extends Graphics2D
}
}
private void addHole(Ellipse2D c, GerberLayer layer, String function) {
AffineTransform finalTx = getFinalTx(layer.isMirrored());
Point2D center = finalTx.transform(new Point2D.Double(c.getCenterX(), c.getCenterY()), null);
double factor = SizeUnit.px.getFactor();
// the drawing transform scales evenly, so the circle stays a circle, scaled by the square root
// of the determinant whether or not it is also rotated
double diameter = c.getWidth() * Math.sqrt(Math.abs(finalTx.getDeterminant())) * factor;
drillLayerMap.get(layer).addHole(new Point(center.getX() * factor, center.getY() * factor),
diameter, function);
}
private AffineTransform getFinalTx(boolean isMirrored) {
AffineTransform finalTx = new AffineTransform();
if (isMirrored) {
@@ -1,6 +1,7 @@
package org.diylc.core.gerber;
import com.bancika.gerberwriter.DataLayer;
import com.bancika.gerberwriter.DrillLayer;
import com.bancika.gerberwriter.GenerationSoftware;
public enum GerberLayer {
@@ -30,9 +31,18 @@ public enum GerberLayer {
}
public DataLayer buildLayer(String diylcVersion) {
GenerationSoftware genSoftware =
new GenerationSoftware("bancika", "DIY Layout Creator", diylcVersion);
return new DataLayer(function, negative, genSoftware);
return new DataLayer(function, negative, createGenerationSoftware(diylcVersion));
}
public DrillLayer buildDrillLayer(String diylcVersion) {
return new DrillLayer(this == DrillPlated, createGenerationSoftware(diylcVersion));
}
/**
* @return true for the drill layers, which hold holes rather than drawn copper or graphics
*/
public boolean isDrill() {
return this == DrillPlated || this == DrillNonPlated;
}
public String getExtension() {
@@ -56,7 +66,15 @@ public enum GerberLayer {
}
public String formatFileName(String fileNameBase, String boardName) {
return formatFileName(fileNameBase, boardName, getExtension());
}
public String formatFileName(String fileNameBase, String boardName, String extension) {
return fileNameBase + (fileNameBase.endsWith(".") ? "" : ".")
+ boardName + "-" + getSuffix() + "." + getExtension();
+ boardName + "-" + getSuffix() + "." + extension;
}
private static GenerationSoftware createGenerationSoftware(String diylcVersion) {
return new GenerationSoftware("bancika", "DIY Layout Creator", diylcVersion);
}
}
@@ -673,6 +673,10 @@
<changeType>IMPROVEMENT</changeType>
<description>Gerber export no longer cuts the holes of solder pads and vias out of the copper layers; the holes are defined by the drill files alone, as the Gerber specification recommends</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Gerber export writes the drill files in Excellon format, the drill format every PCB fab accepts, instead of Gerber format; a 'Drill Hole' no longer adds its outline and crosshair to the drill file</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Rotated labels in pasted components and building blocks were drawn in the wrong place and at the wrong angle while following the mouse, until the components were placed</description>
+2 -2
View File
@@ -73,10 +73,10 @@
</dependency>
<dependency>
<groupId>org.diylc.local</groupId>
<artifactId>gerber-writer-api-1.1.0</artifactId>
<artifactId>gerber-writer-api-1.2.0</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/../lib/gerber-writer-api-1.1.0.jar</systemPath>
<systemPath>${project.basedir}/../lib/gerber-writer-api-1.2.0.jar</systemPath>
</dependency>
<dependency>
<groupId>org.diylc.local</groupId>
@@ -71,9 +71,6 @@ public class Via extends AbstractTransparentComponent<Void>
public static Size HOLE_SIZE = new Size(0.4d, SizeUnit.mm);
public static Color COLOR = Color.black;
// gerber-writer has no constant for it
private static final String VIA_DRILL = "ViaDrill";
private Size size = SIZE;
private Size holeSize = HOLE_SIZE;
private Color color = COLOR;
@@ -112,7 +109,8 @@ public class Via extends AbstractTransparentComponent<Void>
if (gerberDrawingObserver != null) {
gerberDrawingObserver.stopGerberOutput(GerberLayer.CopperTop);
gerberDrawingObserver.stopGerberOutput(GerberLayer.CopperBot);
gerberDrawingObserver.startGerberOutput(GerberLayer.DrillPlated, VIA_DRILL, false);
gerberDrawingObserver.startGerberOutput(GerberLayer.DrillPlated, GerberFunctions.VIA_DRILL,
false);
}
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(point.getX() - holeDiameter / 2,
@@ -21,18 +21,20 @@
*/
package org.diylc.components.connectivity;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Map;
import java.util.EnumSet;
import java.util.Set;
import com.bancika.gerberwriter.DataLayer;
import com.bancika.gerberwriter.DrillFormat;
import org.diylc.common.PCBLayer;
import org.diylc.components.electromechanical.DrillHole;
import org.diylc.core.ComponentState;
import org.diylc.core.Project;
import org.diylc.core.gerber.GerberG2DWrapper;
@@ -79,7 +81,7 @@ public class SolderPadGerberTest {
@Test
public void testViaIsOnBothLayersAndTented() {
Via via = new Via();
Set<GerberLayer> layers = drawGerber(via).keySet();
Set<GerberLayer> layers = layersOf(drawGerber(via));
assertTrue(layers.contains(GerberLayer.CopperTop));
assertTrue(layers.contains(GerberLayer.CopperBot));
assertTrue(layers.contains(GerberLayer.DrillPlated));
@@ -95,11 +97,32 @@ public class SolderPadGerberTest {
assertNoCutOut(drawGerber(new Via()));
}
@Test
public void testViaIsDrilledOnceAsViaDrill() {
String excellon = drawGerber(new Via()).getDrillLayerMap().get(GerberLayer.DrillPlated)
.dumpToString(DrillFormat.EXCELLON);
assertTrue(excellon.contains("; #@! TA.AperFunction,Plated,PTH,ViaDrill\nT1C0.4\n"));
assertEquals(1, countHoles(excellon));
}
@Test
public void testDrillHoleMarkerIsDrilledOnce() {
// the marker's border and crosshair are drawn too, but they are not holes
String excellon = drawGerber(new DrillHole()).getDrillLayerMap()
.get(GerberLayer.DrillNonPlated).dumpToString(DrillFormat.EXCELLON);
assertTrue(excellon.contains("T1C3.0\n"));
assertEquals(1, countHoles(excellon));
}
private static long countHoles(String excellon) {
return excellon.lines().filter(line -> line.startsWith("X")).count();
}
// a cut-out is drawn with clear polarity, %LPC*%
private static void assertNoCutOut(Map<GerberLayer, DataLayer> layers) {
private static void assertNoCutOut(GerberG2DWrapper g2d) {
for (GerberLayer copperLayer : new GerberLayer[] {GerberLayer.CopperTop,
GerberLayer.CopperBot}) {
String gerber = layers.get(copperLayer).dumpGerberToString();
String gerber = g2d.getLayerMap().get(copperLayer).dumpGerberToString();
assertTrue(gerber.contains("D03*"));
assertFalse(gerber.contains("%LPC*%"));
}
@@ -113,10 +136,17 @@ public class SolderPadGerberTest {
SolderPad pad = new SolderPad();
pad.setLayer(layer);
pad.setHoleType(holeType);
return drawGerber(pad).keySet();
return layersOf(drawGerber(pad));
}
private static Map<GerberLayer, DataLayer> drawGerber(IGerberComponentCustom component) {
private static Set<GerberLayer> layersOf(GerberG2DWrapper g2d) {
Set<GerberLayer> layers = EnumSet.noneOf(GerberLayer.class);
layers.addAll(g2d.getLayerMap().keySet());
layers.addAll(g2d.getDrillLayerMap().keySet());
return layers;
}
private static GerberG2DWrapper drawGerber(IGerberComponentCustom component) {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics();
GerberG2DWrapper g2d =
@@ -124,6 +154,6 @@ public class SolderPadGerberTest {
g2d.startedDrawingComponent();
component.draw(g2d, ComponentState.NORMAL, false, new Project(), g2d, g2d);
graphics.dispose();
return g2d.getLayerMap();
return g2d;
}
}
+2 -2
View File
@@ -140,10 +140,10 @@
</dependency>
<dependency>
<groupId>org.diylc.local</groupId>
<artifactId>gerber-writer-api-1.1.0</artifactId>
<artifactId>gerber-writer-api-1.2.0</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/../lib/gerber-writer-api-1.1.0.jar</systemPath>
<systemPath>${project.basedir}/../lib/gerber-writer-api-1.2.0.jar</systemPath>
</dependency>
<dependency>
<groupId>org.diylc.local</groupId>
Binary file not shown.
Binary file not shown.