diff --git a/diylc/diylc-core/pom.xml b/diylc/diylc-core/pom.xml index f924d9382..db468fc76 100644 --- a/diylc/diylc-core/pom.xml +++ b/diylc/diylc-core/pom.xml @@ -97,10 +97,10 @@ org.diylc.local - gerber-writer-api-1.1.0 + gerber-writer-api-1.2.0 1.0 system - ${project.basedir}/../lib/gerber-writer-api-1.1.0.jar + ${project.basedir}/../lib/gerber-writer-api-1.2.0.jar org.diylc.local diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java index 40bb8e614..340bf2dbf 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java @@ -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( diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberG2DWrapper.java b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberG2DWrapper.java index 2ad489d79..6bc7ebb4b 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberG2DWrapper.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberG2DWrapper.java @@ -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 layerMap; + private Map drillLayerMap; private boolean trackingGerber = false; private Map currentLayers = new HashMap(); @@ -65,6 +68,7 @@ public class GerberG2DWrapper extends Graphics2D this.graphics2d = graphics2d; this.boardRect = boardRect; this.layerMap = new HashMap(); + this.drillLayerMap = new HashMap(); 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 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) { diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberLayer.java b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberLayer.java index 5dabcdf23..70bc9b7a8 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberLayer.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberLayer.java @@ -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); } } diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index 78f693609..b7a8ed9c7 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -673,6 +673,10 @@ IMPROVEMENT 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 + + IMPROVEMENT + 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 + BUG_FIX 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 diff --git a/diylc/diylc-library/pom.xml b/diylc/diylc-library/pom.xml index ca8b072b5..aae9cd5c0 100644 --- a/diylc/diylc-library/pom.xml +++ b/diylc/diylc-library/pom.xml @@ -73,10 +73,10 @@ org.diylc.local - gerber-writer-api-1.1.0 + gerber-writer-api-1.2.0 1.0 system - ${project.basedir}/../lib/gerber-writer-api-1.1.0.jar + ${project.basedir}/../lib/gerber-writer-api-1.2.0.jar org.diylc.local diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java index 01261a6e4..79c9b15f0 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java @@ -71,9 +71,6 @@ public class Via extends AbstractTransparentComponent 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 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, diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/connectivity/SolderPadGerberTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/connectivity/SolderPadGerberTest.java index 89b7361f7..ea50b3f3b 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/connectivity/SolderPadGerberTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/connectivity/SolderPadGerberTest.java @@ -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 layers = drawGerber(via).keySet(); + Set 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 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 drawGerber(IGerberComponentCustom component) { + private static Set layersOf(GerberG2DWrapper g2d) { + Set 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; } } diff --git a/diylc/diylc-swing/pom.xml b/diylc/diylc-swing/pom.xml index 58833ef53..e81c992c1 100644 --- a/diylc/diylc-swing/pom.xml +++ b/diylc/diylc-swing/pom.xml @@ -140,10 +140,10 @@ org.diylc.local - gerber-writer-api-1.1.0 + gerber-writer-api-1.2.0 1.0 system - ${project.basedir}/../lib/gerber-writer-api-1.1.0.jar + ${project.basedir}/../lib/gerber-writer-api-1.2.0.jar org.diylc.local diff --git a/diylc/lib/gerber-writer-api-1.1.0.jar b/diylc/lib/gerber-writer-api-1.1.0.jar deleted file mode 100644 index 502ca7919..000000000 Binary files a/diylc/lib/gerber-writer-api-1.1.0.jar and /dev/null differ diff --git a/diylc/lib/gerber-writer-api-1.2.0.jar b/diylc/lib/gerber-writer-api-1.2.0.jar new file mode 100644 index 000000000..a69a45c2e Binary files /dev/null and b/diylc/lib/gerber-writer-api-1.2.0.jar differ