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) {
layerMap.computeIfAbsent(layer, k -> layer.buildLayer(diylcVersion));
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>