diff --git a/diylc/catalog_full.json b/diylc/catalog_full.json
index b55f97ce8..0dfa62df2 100644
--- a/diylc/catalog_full.json
+++ b/diylc/catalog_full.json
@@ -1691,7 +1691,7 @@
"namePrefix" : "Pad",
"description" : "Copper solder pad, round or square",
"creationMethod" : "SINGLE_CLICK",
- "notes" : [ "Use on Blank Board and PCB layouts; Perf Board, Vero Board, TriPad Board and the proto boards already have their own pads" ],
+ "notes" : [ "Use on Blank Board and PCB layouts; Perf Board, Vero Board, TriPad Board and the proto boards already have their own pads; a pad with Hole Type 'Plated Through Hole' is on both copper layers and joins them, so its Layer is ignored" ],
"properties" : [ {
"name" : "Alpha",
"type" : "Percentage",
@@ -1931,6 +1931,44 @@
} ]
} ]
}
+ }, {
+ "name" : "Via",
+ "className" : "connectivity.Via",
+ "category" : "Connectivity",
+ "namePrefix" : "Via",
+ "description" : "Plated hole joining the copper on the top and bottom of a PCB",
+ "creationMethod" : "SINGLE_CLICK",
+ "notes" : [ "Use on Blank Board and PCB layouts to take a trace from one copper layer to the other", "Place the control point where a Top trace and a Bottom trace meet; the via joins the copper of both layers there. It is not a component pad: leads go into a Solder Pad" ],
+ "properties" : [ {
+ "name" : "Alpha",
+ "type" : "Percentage",
+ "format" : "number without the % sign, e.g. 100",
+ "defaultValue" : "100"
+ }, {
+ "name" : "Color",
+ "type" : "Color",
+ "format" : "hex",
+ "defaultValue" : "#000000"
+ }, {
+ "name" : "Hole",
+ "type" : "Size",
+ "format" : "(value)(unit)",
+ "possibleUnits" : [ "px", "mm", "cm", "m", "in", "ft", "yd" ],
+ "defaultValue" : "0.4mm"
+ }, {
+ "name" : "Name",
+ "type" : "String"
+ }, {
+ "name" : "Size",
+ "type" : "Size",
+ "format" : "(value)(unit)",
+ "possibleUnits" : [ "px", "mm", "cm", "m", "in", "ft", "yd" ],
+ "defaultValue" : "0.8mm"
+ } ],
+ "pinout" : {
+ "pinCount" : 1,
+ "labels" : "sequential"
+ }
}, {
"name" : "Arduino Mega 2560",
"className" : "micro.ArduinoMega",
diff --git a/diylc/catalog_index.json b/diylc/catalog_index.json
index 90e25d3ab..f68ddc62f 100644
--- a/diylc/catalog_index.json
+++ b/diylc/catalog_index.json
@@ -85,7 +85,7 @@
}, {
"name" : "Solder Pad",
"className" : "connectivity.SolderPad",
- "notes" : [ "Use on Blank Board and PCB layouts; Perf Board, Vero Board, TriPad Board and the proto boards already have their own pads" ]
+ "notes" : [ "Use on Blank Board and PCB layouts; Perf Board, Vero Board, TriPad Board and the proto boards already have their own pads; a pad with Hole Type 'Plated Through Hole' is on both copper layers and joins them, so its Layer is ignored" ]
}, {
"name" : "Trace Cut",
"className" : "connectivity.TraceCut"
@@ -96,6 +96,10 @@
"name" : "Twisted Leads",
"className" : "connectivity.TwistedWire",
"notes" : [ "Electrically a single conductor between its two ends, exactly like Hookup Wire, even though two twisted leads are drawn; use it only for the look of a twisted pair, and where the circuit must be electrically correct and analyzable, draw each conductor as its own Hookup Wire or Right Angle Wire instead" ]
+ }, {
+ "name" : "Via",
+ "className" : "connectivity.Via",
+ "notes" : [ "Use on Blank Board and PCB layouts to take a trace from one copper layer to the other" ]
} ],
"Controllers" : [ {
"name" : "Arduino Mega 2560",
diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml
index 87e200598..a9a78cc96 100644
--- a/diylc/diylc-core/src/main/resources/update.xml
+++ b/diylc/diylc-core/src/main/resources/update.xml
@@ -645,6 +645,10 @@
NEW_FEATURE
Resistors, capacitors, diodes, ICs, transistors, switches, jacks, tube sockets and other PCB parts have a new 'Board Side' property to place them on the bottom of the board; bottom parts are drawn faded on the canvas and solid in the board's Underside Display, and Gerber export puts their silkscreen on the bottom layer with the text mirrored, and the silkscreen print-out has a separate page for the bottom; a 'Dot' used as a solder joint can be put on the bottom as well
+
+ NEW_FEATURE
+ Added 'Via' to the Connectivity category, a plated hole that takes a trace from one copper layer to the other: it joins the top and bottom copper in netlists and 'Highlight Connected Areas', and Gerber export writes it to both copper layers and the plated drill file, covered by the solder mask
+
IMPROVEMENT
Boards are now drawn the way they really are, so it is clear at a glance which side you are looking at: the main view shows the component side, with strips, pads, traces and other copper on the bottom of the board showing through it faded, and the Underside Display shows the solder side, with the copper solid and the parts mounted on top faded behind the board. Holes and trace cuts go through the board and stay clear from both sides
@@ -661,6 +665,10 @@
IMPROVEMENT
PCB text on the bottom layer is now printed on the bottom page of the silkscreen print-out, not the top one
+
+ IMPROVEMENT
+ A 'Solder Pad' with a plated through hole now has copper on both layers, like the real thing, and joins top and bottom copper in netlists and 'Highlight Connected Areas'; Gerber export writes it to both copper and both solder mask layers
+
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/src/main/java/org/diylc/components/connectivity/SolderPad.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/SolderPad.java
index 179a8a95a..713f92c21 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/SolderPad.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/SolderPad.java
@@ -54,7 +54,7 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
-@AiNote(value = "Use on Blank Board and PCB layouts; Perf Board, Vero Board, TriPad Board and the proto boards already have their own pads", scope = Scope.SELECTION)
+@AiNote(value = "Use on Blank Board and PCB layouts; Perf Board, Vero Board, TriPad Board and the proto boards already have their own pads; a pad with Hole Type 'Plated Through Hole' is on both copper layers and joins them, so its Layer is ignored", scope = Scope.SELECTION)
@ComponentDescriptor(name = "Solder Pad", category = "Connectivity", author = "Branislav Stojkovic",
description = "Copper solder pad, round or square", instanceNamePrefix = "Pad",
zOrder = IDIYComponent.TRACE + 0.1, bomPolicy = BomPolicy.NEVER_SHOW,
@@ -92,16 +92,22 @@ public class SolderPad extends AbstractTransparentComponent
? SELECTION_COLOR
: color);
drawingObserver.startTrackingContinuityArea(true);
- GerberLayer gerberCopperLayer = this.getLayer().toGerberCopperLayer();
- // the mask is opened on the side the pad is on
- GerberLayer gerberMaskLayer = gerberCopperLayer == GerberLayer.CopperTop
- ? GerberLayer.SolderMaskTop : GerberLayer.SolderMaskBot;
+ GerberLayer[] gerberCopperLayers = isOnBothLayers()
+ ? new GerberLayer[] {GerberLayer.CopperTop, GerberLayer.CopperBot}
+ : new GerberLayer[] {getLayer().toGerberCopperLayer()};
if (gerberDrawingObserver != null) {
- gerberDrawingObserver.startGerberOutput(gerberCopperLayer, GerberFunctions.CONNECTOR_PAD, false);
- gerberDrawingObserver.startGerberOutput(gerberMaskLayer, "Material", false);
+ for (GerberLayer gerberCopperLayer : gerberCopperLayers) {
+ gerberDrawingObserver.startGerberOutput(gerberCopperLayer, GerberFunctions.CONNECTOR_PAD,
+ false);
+ gerberDrawingObserver.startGerberOutput(getMaskLayer(gerberCopperLayer), "Material", false);
+ }
}
Composite oldComposite = applyAlpha(g2d, componentState);
- drawingObserver.startDrawingSide(getLayer());
+ if (isOnBothLayers()) {
+ drawingObserver.startDrawingBothSides();
+ } else {
+ drawingObserver.startDrawingSide(getLayer());
+ }
if (type == Type.ROUND) {
g2d.fill(new Ellipse2D.Double(point.getX() - diameter / 2, point.getY() - diameter / 2,
@@ -120,11 +126,15 @@ public class SolderPad extends AbstractTransparentComponent
drawingObserver.stopTracking();
drawingObserver.stopTrackingContinuityArea();
if (gerberDrawingObserver != null) {
- gerberDrawingObserver.stopGerberOutput(gerberMaskLayer);
+ for (GerberLayer gerberCopperLayer : gerberCopperLayers) {
+ gerberDrawingObserver.stopGerberOutput(getMaskLayer(gerberCopperLayer));
+ }
}
if (getHoleSize().getValue() > 0 && !HoleType.NONE.equals(getHoleType())) {
if (gerberDrawingObserver != null) {
- gerberDrawingObserver.setGerberNegative(gerberCopperLayer, true);
+ for (GerberLayer gerberCopperLayer : gerberCopperLayers) {
+ gerberDrawingObserver.setGerberNegative(gerberCopperLayer, true);
+ }
GerberLayer holeLayer = HoleType.PTH.equals(getHoleType()) ? GerberLayer.DrillPlated : GerberLayer.DrillNonPlated;
gerberDrawingObserver.startGerberOutput(holeLayer, GerberFunctions.COMPONENT_DRILL, false);
}
@@ -204,9 +214,22 @@ public class SolderPad extends AbstractTransparentComponent
this.layer = layer;
}
+ // layer id 0 is copper that connects to copper on every layer
@Override
public int getLayerId() {
- return getLayer().getId();
+ return isOnBothLayers() ? 0 : getLayer().getId();
+ }
+
+ // a plated hole is a barrel with a copper ring on each side of the board, so the pad is on both
+ // layers and joins them, whatever its 'Layer' says
+ private boolean isOnBothLayers() {
+ return HoleType.PTH.equals(getHoleType());
+ }
+
+ // the mask is opened on the side the copper is on
+ private static GerberLayer getMaskLayer(GerberLayer gerberCopperLayer) {
+ return gerberCopperLayer == GerberLayer.CopperTop ? GerberLayer.SolderMaskTop
+ : GerberLayer.SolderMaskBot;
}
@Override
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
new file mode 100644
index 000000000..5981267ac
--- /dev/null
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java
@@ -0,0 +1,216 @@
+/*
+
+ 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 .
+
+*/
+package org.diylc.components.connectivity;
+
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Graphics2D;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+import com.bancika.gerberwriter.GerberFunctions;
+
+import org.diylc.components.AbstractTransparentComponent;
+import org.diylc.components.transform.SimpleComponentTransformer;
+import org.diylc.core.ComponentState;
+import org.diylc.core.IConnective;
+import org.diylc.core.IDIYComponent;
+import org.diylc.core.IDrawingObserver;
+import org.diylc.core.Project;
+import org.diylc.core.VisibilityPolicy;
+import org.diylc.core.annotations.AiNote;
+import org.diylc.core.annotations.AiNote.Scope;
+import org.diylc.core.annotations.BomPolicy;
+import org.diylc.core.annotations.ComponentDescriptor;
+import org.diylc.core.annotations.EditableProperty;
+import org.diylc.core.annotations.KeywordPolicy;
+import org.diylc.core.annotations.PositiveNonZeroMeasureValidator;
+import org.diylc.core.gerber.GerberLayer;
+import org.diylc.core.gerber.IGerberComponentCustom;
+import org.diylc.core.gerber.IGerberDrawingObserver;
+import org.diylc.core.measures.Size;
+import org.diylc.core.measures.SizeUnit;
+import org.diylc.utils.Constants;
+
+@AiNote(value = "Use on Blank Board and PCB layouts to take a trace from one copper layer to the "
+ + "other", scope = Scope.SELECTION)
+@AiNote("Place the control point where a Top trace and a Bottom trace meet; the via joins the "
+ + "copper of both layers there. It is not a component pad: leads go into a Solder Pad")
+@ComponentDescriptor(name = "Via", category = "Connectivity", author = "Branislav Stojkovic",
+ description = "Plated hole joining the copper on the top and bottom of a PCB",
+ instanceNamePrefix = "Via", zOrder = IDIYComponent.TRACE + 0.1,
+ bomPolicy = BomPolicy.NEVER_SHOW, keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB",
+ transformer = SimpleComponentTransformer.class, enableCache = true)
+public class Via extends AbstractTransparentComponent
+ implements IConnective, IGerberComponentCustom {
+
+ private static final long serialVersionUID = 1L;
+
+ // the defaults KiCad gives a new board
+ public static Size SIZE = new Size(0.8d, SizeUnit.mm);
+ 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;
+ private Point2D.Double point = new Point2D.Double(0, 0);
+
+ @Override
+ public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
+ Project project, IDrawingObserver drawingObserver,
+ IGerberDrawingObserver gerberDrawingObserver) {
+ if (checkPointsClipped(g2d.getClip())) {
+ return;
+ }
+ double diameter = getSize().convertToPixels();
+ double holeDiameter = getHoleSize().convertToPixels();
+ g2d.setColor(
+ componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
+ ? SELECTION_COLOR
+ : color);
+ // the via is tented: the solder mask covers it, so no mask opening is written
+ if (gerberDrawingObserver != null) {
+ gerberDrawingObserver.startGerberOutput(GerberLayer.CopperTop, GerberFunctions.VIA_PAD,
+ false);
+ gerberDrawingObserver.startGerberOutput(GerberLayer.CopperBot, GerberFunctions.VIA_PAD,
+ false);
+ }
+ Composite oldComposite = applyAlpha(g2d, componentState);
+ // the copper is not on a layer of its own, so it connects to the copper of every layer
+ drawingObserver.startTrackingContinuityArea(true);
+ drawingObserver.startDrawingBothSides();
+ g2d.fill(new Ellipse2D.Double(point.getX() - diameter / 2, point.getY() - diameter / 2,
+ diameter, diameter));
+ drawingObserver.stopTracking();
+ drawingObserver.stopTrackingContinuityArea();
+ if (gerberDrawingObserver != null) {
+ gerberDrawingObserver.setGerberNegative(GerberLayer.CopperTop, true);
+ gerberDrawingObserver.setGerberNegative(GerberLayer.CopperBot, true);
+ gerberDrawingObserver.startGerberOutput(GerberLayer.DrillPlated, VIA_DRILL, false);
+ }
+ g2d.setColor(Constants.CANVAS_COLOR);
+ g2d.fill(new Ellipse2D.Double(point.getX() - holeDiameter / 2,
+ point.getY() - holeDiameter / 2, holeDiameter, holeDiameter));
+ drawingObserver.stopDrawingSide();
+ g2d.setComposite(oldComposite);
+ if (gerberDrawingObserver != null) {
+ gerberDrawingObserver.stopGerberOutput();
+ }
+ }
+
+ @Override
+ public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
+ Project project, IDrawingObserver drawingObserver) {
+ this.draw(g2d, componentState, outlineMode, project, drawingObserver, null);
+ }
+
+ @Override
+ public void drawIcon(Graphics2D g2d, int width, int height) {
+ int diameter = getClosestOdd(width / 3);
+ int holeDiameter = getClosestOdd(width / 7);
+ g2d.setColor(COLOR);
+ g2d.fillOval((width - diameter) / 2, (height - diameter) / 2, diameter, diameter);
+ g2d.setColor(Constants.CANVAS_COLOR);
+ g2d.fillOval((width - holeDiameter) / 2, (height - holeDiameter) / 2, holeDiameter,
+ holeDiameter);
+ }
+
+ @EditableProperty(validatorClass = PositiveNonZeroMeasureValidator.class)
+ public Size getSize() {
+ if (size == null) {
+ size = SIZE;
+ }
+ return size;
+ }
+
+ public void setSize(Size size) {
+ this.size = size;
+ }
+
+ @EditableProperty(name = "Hole", validatorClass = PositiveNonZeroMeasureValidator.class)
+ public Size getHoleSize() {
+ if (holeSize == null) {
+ holeSize = HOLE_SIZE;
+ }
+ return holeSize;
+ }
+
+ public void setHoleSize(Size holeSize) {
+ this.holeSize = holeSize;
+ }
+
+ @EditableProperty
+ public Color getColor() {
+ if (color == null) {
+ color = COLOR;
+ }
+ return color;
+ }
+
+ public void setColor(Color color) {
+ this.color = color;
+ }
+
+ @Override
+ public int getControlPointCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean isControlPointSticky(int index) {
+ return true;
+ }
+
+ @Override
+ public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
+ return VisibilityPolicy.NEVER;
+ }
+
+ @Override
+ public Point2D getControlPoint(int index) {
+ return point;
+ }
+
+ @Override
+ public void setControlPoint(Point2D point, int index) {
+ this.point.setLocation(point);
+ }
+
+ @Override
+ public Void getValue() {
+ return null;
+ }
+
+ @Override
+ public void setValue(Void value) {}
+
+ @Override
+ public Rectangle2D getCachingBounds() {
+ double size = getSize().convertToPixels();
+ return new Rectangle2D.Double(point.getX() - size, point.getY() - size, size * 2, size * 2);
+ }
+}
diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/connectivity/CopperLayerContinuityTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/connectivity/CopperLayerContinuityTest.java
new file mode 100644
index 000000000..8d55a35d1
--- /dev/null
+++ b/diylc/diylc-library/src/test/java/org/diylc/components/connectivity/CopperLayerContinuityTest.java
@@ -0,0 +1,116 @@
+/*
+
+ 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 .
+
+*/
+package org.diylc.components.connectivity;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.awt.Graphics2D;
+import java.awt.geom.Area;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+
+import org.diylc.TestConfiguration;
+import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
+import org.diylc.appframework.simplemq.MessageDispatcher;
+import org.diylc.common.DrawOption;
+import org.diylc.common.EventType;
+import org.diylc.common.PCBLayer;
+import org.diylc.core.IDIYComponent;
+import org.diylc.core.Project;
+import org.diylc.presenter.DrawingManager;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CopperLayerContinuityTest {
+
+ private static final Point2D JOINT = new Point2D.Double(300, 100);
+ private static final Point2D ON_TOP_TRACE = new Point2D.Double(150, 100);
+ private static final Point2D ON_BOTTOM_TRACE = new Point2D.Double(450, 100);
+
+ @Before
+ public void setUp() {
+ TestConfiguration.initialize();
+ }
+
+ @Test
+ public void testTracesOnDifferentLayersDoNotConnect() {
+ assertFalse(areConnected(null));
+ }
+
+ @Test
+ public void testPlatedPadJoinsBothLayers() {
+ assertTrue(areConnected(createPad(SolderPad.HoleType.PTH)));
+ }
+
+ @Test
+ public void testNonPlatedPadStaysOnItsLayer() {
+ assertFalse(areConnected(createPad(SolderPad.HoleType.NTPH)));
+ }
+
+ @Test
+ public void testViaJoinsBothLayers() {
+ Via via = new Via();
+ via.setControlPoint(JOINT, 0);
+ assertTrue(areConnected(via));
+ }
+
+ private static SolderPad createPad(SolderPad.HoleType holeType) {
+ SolderPad pad = new SolderPad();
+ pad.setLayer(PCBLayer._1);
+ pad.setHoleType(holeType);
+ pad.setControlPoint(JOINT, 0);
+ return pad;
+ }
+
+ // a top trace and a bottom trace that meet at the joint
+ private static boolean areConnected(IDIYComponent> joint) {
+ Project project = new Project();
+ project.getComponents().add(createTrace(PCBLayer._2, new Point2D.Double(100, 100), JOINT));
+ project.getComponents().add(createTrace(PCBLayer._1, JOINT, new Point2D.Double(500, 100)));
+ if (joint != null) {
+ project.getComponents().add(joint);
+ }
+ DrawingManager drawingManager = new DrawingManager(new MessageDispatcher(false),
+ InMemoryConfigurationManager.getInstance());
+ BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g2d = img.createGraphics();
+ drawingManager.drawProject(g2d, project, EnumSet.of(DrawOption.ZOOM), null, null,
+ Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false,
+ Collections.emptySet(), 1.0, 1.0, new Rectangle2D.Double(0, 0, 1000, 1000));
+ g2d.dispose();
+ List areas = drawingManager.getContinuityGraph(project).findAreasFor(ON_TOP_TRACE);
+ return areas.stream().anyMatch(area -> area.contains(ON_BOTTOM_TRACE));
+ }
+
+ private static CopperTrace createTrace(PCBLayer layer, Point2D start, Point2D end) {
+ CopperTrace trace = new CopperTrace();
+ trace.setLayer(layer);
+ trace.setControlPoint(start, 0);
+ trace.setControlPoint(end, 1);
+ return trace;
+ }
+}
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 113a349da..859938a29 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
@@ -33,6 +33,7 @@ import org.diylc.core.ComponentState;
import org.diylc.core.Project;
import org.diylc.core.gerber.GerberG2DWrapper;
import org.diylc.core.gerber.GerberLayer;
+import org.diylc.core.gerber.IGerberComponentCustom;
import org.junit.Test;
public class SolderPadGerberTest {
@@ -53,15 +54,53 @@ public class SolderPadGerberTest {
assertTrue(!layers.contains(GerberLayer.SolderMaskTop));
}
+ @Test
+ public void testPlatedPadIsOnBothLayers() {
+ Set layers = drawGerber(PCBLayer._1, SolderPad.HoleType.PTH);
+ assertTrue(layers.contains(GerberLayer.CopperTop));
+ assertTrue(layers.contains(GerberLayer.CopperBot));
+ assertTrue(layers.contains(GerberLayer.SolderMaskTop));
+ assertTrue(layers.contains(GerberLayer.SolderMaskBot));
+ assertTrue(layers.contains(GerberLayer.DrillPlated));
+ }
+
+ @Test
+ public void testNonPlatedPadIsOnItsLayer() {
+ Set layers = drawGerber(PCBLayer._1, SolderPad.HoleType.NTPH);
+ assertTrue(layers.contains(GerberLayer.CopperBot));
+ assertTrue(!layers.contains(GerberLayer.CopperTop));
+ assertTrue(layers.contains(GerberLayer.DrillNonPlated));
+ }
+
+ @Test
+ public void testViaIsOnBothLayersAndTented() {
+ Via via = new Via();
+ Set layers = drawGerber(via);
+ assertTrue(layers.contains(GerberLayer.CopperTop));
+ assertTrue(layers.contains(GerberLayer.CopperBot));
+ assertTrue(layers.contains(GerberLayer.DrillPlated));
+ assertTrue(!layers.contains(GerberLayer.SolderMaskTop));
+ assertTrue(!layers.contains(GerberLayer.SolderMaskBot));
+ }
+
private static Set drawGerber(PCBLayer layer) {
+ return drawGerber(layer, SolderPad.HoleType.NTPH);
+ }
+
+ private static Set drawGerber(PCBLayer layer, SolderPad.HoleType holeType) {
SolderPad pad = new SolderPad();
pad.setLayer(layer);
+ pad.setHoleType(holeType);
+ return drawGerber(pad);
+ }
+
+ private static Set drawGerber(IGerberComponentCustom component) {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics();
GerberG2DWrapper g2d =
new GerberG2DWrapper(graphics, "test", new Rectangle2D.Double(-50, -50, 100, 100));
g2d.startedDrawingComponent();
- pad.draw(g2d, ComponentState.NORMAL, false, new Project(), g2d, g2d);
+ component.draw(g2d, ComponentState.NORMAL, false, new Project(), g2d, g2d);
graphics.dispose();
return g2d.getLayerMap().keySet();
}
diff --git a/diylc/diylc-library/src/test/resources/pinout-drivers.txt b/diylc/diylc-library/src/test/resources/pinout-drivers.txt
index ab21051d5..0bb07694d 100644
--- a/diylc/diylc-library/src/test/resources/pinout-drivers.txt
+++ b/diylc/diylc-library/src/test/resources/pinout-drivers.txt
@@ -33,6 +33,7 @@ connectivity.SolderPad = fixed
connectivity.TraceCut = none
connectivity.Turret = fixed
connectivity.TwistedWire = Point Count
+connectivity.Via = fixed
displays.CharacterLCD = Interface
displays.LEDBarGraph = Segments
displays.LEDMatrix = fixed
diff --git a/diylc/docs/plans/bottom-side-components.md b/diylc/docs/plans/bottom-side-components.md
index d7e338cff..2de980255 100644
--- a/diylc/docs/plans/bottom-side-components.md
+++ b/diylc/docs/plans/bottom-side-components.md
@@ -57,6 +57,8 @@ projects; the open questions below remain.
| Bottom-side text is mirrored centrally in the Gerber output | Parts draw their labels readable from the top, which is backwards on the bottom silkscreen once the board is turned over. While `GerberExporter.drawComponent` draws a bottom-side part, `GerberG2DWrapper.setMirrorText` mirrors each string about the centre of its layout box, so no component changes and the canvas keeps readable labels. |
| A part without a side placed in the mirror is placed unchanged | A jumper, eyelet or wire has no side to set, so it is placed at the reflected point like any other part rather than refused. |
| A `SolderPad` opens the solder mask on its own side | It used to write `SolderMaskBot` whatever its layer, which was wrong for top-layer pads. It now writes `SolderMaskTop` for a pad on `_2`. |
+| A `SolderPad` with a plated through hole is on both layers, and its Layer is ignored | A plated hole has a copper ring on each side joined by the barrel. The pad reports layer id `0`, which connects to copper on every layer, draws as both sides, and writes both copper and both mask layers. Projects where such a pad overlaps a trace on its other side now net differently; PTH is not the default and arrived with Gerber export, so few projects are affected. |
+| `Via` is its own component, not a `SolderPad` option | It takes no lead and no node name, is tented (no mask opening), and has its own Gerber functions, `ViaPad` and `ViaDrill`. Like the PTH pad it reports layer id `0`. It has no Board Side, like `DrillHole`. |
## 3. Design
diff --git a/diylc/docs/plans/copper-side-view.md b/diylc/docs/plans/copper-side-view.md
index 300af52a9..6314bfb01 100644
--- a/diylc/docs/plans/copper-side-view.md
+++ b/diylc/docs/plans/copper-side-view.md
@@ -67,7 +67,8 @@ were regenerated for the faded copper. The open questions below remain.
- `ProtoBoard` declares its underside traces and underside mini pads `_1`.
- `CopperTrace` and `PCBText` declare their whole canvas `draw` on `getLayer()`. `CurvedTrace` and
`GroundFill` declare their copper fill. `SolderPad` declares the pad on its layer and its
- drilled hole as both sides.
+ drilled hole as both sides; a pad with a plated through hole declares its pad as both sides
+ too, because it has copper on both. `Via` declares its copper and hole as both sides.
- `TraceCut` draws its cut and hole as both sides.
### diylc-swing
diff --git a/diylc/docs/plans/fill-clearance.md b/diylc/docs/plans/fill-clearance.md
index 24270cfdb..d53691a90 100644
--- a/diylc/docs/plans/fill-clearance.md
+++ b/diylc/docs/plans/fill-clearance.md
@@ -116,7 +116,8 @@ the gap always matches the drawn copper.
| --- | --- |
| `CopperTrace` | Stroked line, current thickness and `LineEndingStyle` cap |
| `CurvedTrace` | Stroked curve, same stroke as `draw()` |
-| `SolderPad` | Pad outline per `Type` (round, oval H/V, square); the drill hole is ignored |
+| `SolderPad` | Pad outline per `Type` (round, oval H/V, square); the drill hole is ignored. A pad with a plated through hole is on both layers, so it clears fills on both |
+| `Via` | Round copper outline, on both layers like a plated pad |
| `PCBText` | Glyph outlines (confirm it is copper and not silkscreen before including) |
`PCBTerminalBlock` tracks no continuity and is not layered today, so it waits for phase 2.