via and double-sided pad

This commit is contained in:
Bane Stojkovic
2026-11-12 10:48:19 +01:00
parent d166c58161
commit 3049d4910c
11 changed files with 465 additions and 16 deletions
@@ -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<Void>
? 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<Void>
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<Void>
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
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Void>
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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<EventType>(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<Area> 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;
}
}
@@ -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<GerberLayer> 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<GerberLayer> 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<GerberLayer> 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<GerberLayer> drawGerber(PCBLayer layer) {
return drawGerber(layer, SolderPad.HoleType.NTPH);
}
private static Set<GerberLayer> drawGerber(PCBLayer layer, SolderPad.HoleType holeType) {
SolderPad pad = new SolderPad();
pad.setLayer(layer);
pad.setHoleType(holeType);
return drawGerber(pad);
}
private static Set<GerberLayer> 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();
}
@@ -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