more components

This commit is contained in:
Bane Stojkovic
2026-08-17 22:58:35 +02:00
parent 0a76439583
commit 601e715302
7 changed files with 1651 additions and 0 deletions
@@ -0,0 +1,304 @@
/*
*
* 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.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
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.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "NeoPixel Ring (WS2812B)", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "Addressable RGB WS2812B NeoPixel Ring (12/16/24 LEDs)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BRing extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum RingSize {
_12_LED("12 LEDs (37mm OD)", 12, 37.0, 27.0),
_16_LED("16 LEDs (44.5mm OD)", 16, 44.5, 32.0),
_24_LED("24 LEDs (66mm OD)", 24, 66.0, 52.0);
private final String label;
private final int ledCount;
private final double outerDiameterMm;
private final double innerDiameterMm;
RingSize(String label, int ledCount, double outerDiameterMm, double innerDiameterMm) {
this.label = label;
this.ledCount = ledCount;
this.outerDiameterMm = outerDiameterMm;
this.innerDiameterMm = innerDiameterMm;
}
@Override public String toString() { return label; }
public int getLedCount() { return ledCount; }
public double getOuterDiameterMm() { return outerDiameterMm; }
public double getInnerDiameterMm() { return innerDiameterMm; }
}
// Colors shared with WS2812BStick
public static Color NEO_BLACK = Color.decode("#111111");
public static Color LED_PACKAGE = Color.decode("#FDFEFE");
public static Color LED_BORDER = Color.decode("#D0D3D4");
public static Color LED_DIFFUSER = Color.decode("#EAECEE");
public static Color DIFFUSER_BORDER = Color.decode("#BDC3C7");
public static Color CHIP_DOT_COLOR = Color.decode("#333333");
public static Color SOLDER_PAD_COLOR = Color.decode("#D4AC0D");
public static Color SOLDER_PAD_BORDER = Color.decode("#9A7D0A");
private static final String[] PIN_NAMES = {"DIN", "+5V", "GND", "DOUT"};
private RingSize ringSize = RingSize._16_LED;
public WS2812BRing() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
@EditableProperty(name = "Ring Size")
public RingSize getRingSize() {
return ringSize;
}
public void setRingSize(RingSize ringSize) {
this.ringSize = ringSize;
updateControlPoints();
invalidateCache();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
private double getMidRadius() {
double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
double innerR = new Size(ringSize.getInnerDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
return (outerR + innerR) / 2.0;
}
private double getPadAngle(int index) {
double spacing = PIN_SPACING.convertToPixels();
double midR = getMidRadius();
double deltaTheta = spacing / midR;
// 4 pads placed symmetrically at the bottom of the circular ring (near angle PI / 2)
return Math.PI / 2.0 + (index - 1.5) * deltaTheta;
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double midR = getMidRadius();
double theta0 = getPadAngle(0);
double[][] relativeOffsets = new double[4][2];
for (int i = 0; i < 4; i++) {
double theta = getPadAngle(i);
relativeOffsets[i][0] = midR * (Math.cos(theta) - Math.cos(theta0));
relativeOffsets[i][1] = midR * (Math.sin(theta) - Math.sin(theta0));
}
rotatePoints(firstPoint, relativeOffsets);
}
private Point2D getCenter() {
Point2D p0 = controlPoints[0];
double midR = getMidRadius();
double theta0 = getPadAngle(0);
// When unrotated, center is at p0 - (midR*cos(theta0), midR*sin(theta0))
return new Point2D.Double(p0.getX() - midR * Math.cos(theta0), p0.getY() - midR * Math.sin(theta0));
}
@Override
public Shape getBodyShape() {
Point2D center = getCenter();
double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
return new Ellipse2D.Double(center.getX() - outerR, center.getY() - outerR, outerR * 2, outerR * 2);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
Point2D center = getCenter();
double cx = center.getX();
double cy = center.getY();
double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
double innerR = new Size(ringSize.getInnerDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
double midR = (outerR + innerR) / 2.0;
// Annular ring shape: outer circle minus inner circle
Area outerArea = new Area(new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2));
Area innerArea = new Area(new Ellipse2D.Double(cx - innerR, cy - innerR, innerR * 2, innerR * 2));
outerArea.subtract(innerArea);
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(outerArea);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(outerArea);
if (!outlineMode) {
int ledCount = ringSize.getLedCount();
double ledSize = Math.max(8.0, Math.min(15.0, (2 * Math.PI * midR / ledCount) * 0.60));
double halfLed = ledSize / 2.0;
// Draw all LEDs around the circular ring
for (int i = 0; i < ledCount; i++) {
double angle = 2 * Math.PI * i / ledCount - Math.PI / 2.0;
double lx = cx + midR * Math.cos(angle);
double ly = cy + midR * Math.sin(angle);
// 5050 White Package
g2d.setColor(LED_PACKAGE);
g2d.fill(new RoundRectangle2D.Double(lx - halfLed, ly - halfLed, ledSize, ledSize, 2, 2));
g2d.setColor(LED_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(0.5f));
g2d.draw(new RoundRectangle2D.Double(lx - halfLed, ly - halfLed, ledSize, ledSize, 2, 2));
// Milky phosphor lens
double diffR = halfLed * 0.7;
g2d.setColor(LED_DIFFUSER);
g2d.fill(new Ellipse2D.Double(lx - diffR, ly - diffR, diffR * 2, diffR * 2));
g2d.setColor(DIFFUSER_BORDER);
g2d.draw(new Ellipse2D.Double(lx - diffR, ly - diffR, diffR * 2, diffR * 2));
// Tiny IC dot
g2d.setColor(CHIP_DOT_COLOR);
double dotS = Math.max(2.0, diffR * 0.4);
g2d.fill(new Rectangle2D.Double(lx - dotS / 2.0, ly - dotS / 2.0, dotS, dotS));
}
// Center silkscreen text
g2d.setColor(Color.decode("#666666"));
g2d.setFont(SILK_FONT_SMALL);
StringUtils.drawCenteredText(g2d, "NeoPixel", cx, cy - 6, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, ringSize.getLedCount() + "x LED", cx, cy + 6, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Silkscreen labels next to the 4 solder pads on the ring
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 8));
String[] labels = new String[] {"DIN", "5V", "GND", "DOUT"};
for (int i = 0; i < 4; i++) {
double theta = getPadAngle(i);
double labelR = innerR + 5;
double px = cx + labelR * Math.cos(theta);
double py = cy + labelR * Math.sin(theta);
StringUtils.drawCenteredText(g2d, labels[i], px, py, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
g2d.setTransform(oldTx);
// Draw the 4 solder pads directly on the circular ring PCB
drawSolderPads(g2d, 0, 4, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
int cx = width / 2;
int cy = height / 2;
int outerR = width / 2 - 2;
int innerR = outerR / 2;
Area outer = new Area(new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2));
Area inner = new Area(new Ellipse2D.Double(cx - innerR, cy - innerR, innerR * 2, innerR * 2));
outer.subtract(inner);
g2d.setColor(NEO_BLACK);
g2d.fill(outer);
g2d.setColor(Color.DARK_GRAY);
g2d.draw(outer);
Color[] rainbow = new Color[] {
Color.decode("#FF3333"),
Color.decode("#FFD700"),
Color.decode("#00E676"),
Color.decode("#00E5FF"),
Color.decode("#E040FB"),
Color.decode("#FF6B35")
};
double midR = (outerR + innerR) / 2.0;
int dotR = Math.max(2, (int)(midR * 0.35));
int dotCount = 6;
for (int i = 0; i < dotCount; i++) {
double angle = 2 * Math.PI * i / dotCount - Math.PI / 2.0;
double lx = cx + midR * Math.cos(angle);
double ly = cy + midR * Math.sin(angle);
g2d.setColor(rainbow[i % rainbow.length]);
g2d.fill(new Ellipse2D.Double(lx - dotR, ly - dotR, dotR * 2, dotR * 2));
}
}
}
@@ -0,0 +1,337 @@
/*
*
* 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.electromechanical;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
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.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "18650 Battery Holder", category = "Electro-Mechanical",
author = "Branislav Stojkovic", description = "PCB-mount 18650 Li-ion Battery Holder (1S or 2S)",
instanceNamePrefix = "BTR", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class BatteryHolder18650 extends AbstractTransparentComponent<String> {
private static final long serialVersionUID = 1L;
public enum CellCount {
_1S("1S (3.7V) — Single Cell", 1, 77.0, 21.0),
_2S("2S (7.4V) — Two Cells", 2, 77.0, 42.0);
private final String label;
private final int cells;
private final double lengthMm;
private final double widthMm;
CellCount(String label, int cells, double lengthMm, double widthMm) {
this.label = label;
this.cells = cells;
this.lengthMm = lengthMm;
this.widthMm = widthMm;
}
@Override public String toString() { return label; }
public int getCells() { return cells; }
public double getLengthMm() { return lengthMm; }
public double getWidthMm() { return widthMm; }
}
public static Color HOLDER_BODY = Color.decode("#2C2C2C");
public static Color HOLDER_BODY_LIGHT = Color.decode("#3C3C3C");
public static Color SPRING_COLOR = Color.decode("#C0C0C0");
public static Color SPRING_BORDER = Color.decode("#909090");
public static Color PLATE_COLOR = Color.decode("#D0D0D0");
public static Color CELL_CAVITY = Color.decode("#1A1A1A");
private static final Font LABEL_FONT = new Font("SansSerif", Font.BOLD, 12);
private String value = "";
private CellCount cellCount = CellCount._1S;
private Orientation orientation = Orientation.DEFAULT;
// Two control points: Point 0 = + (left), Point 1 = - (right)
private Point2D[] controlPoints = new Point2D[] {
new Point2D.Double(0, 0),
new Point2D.Double(0, 0)
};
public BatteryHolder18650() {
super();
updateControlPoints();
}
@EditableProperty(name = "Cell Count")
public CellCount getCellCount() {
return cellCount;
}
public void setCellCount(CellCount cellCount) {
this.cellCount = cellCount;
updateControlPoints();
}
@EditableProperty(name = "Orientation")
public Orientation getOrientation() {
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
}
private void updateControlPoints() {
double length = new Size(cellCount.getLengthMm(), SizeUnit.mm).convertToPixels();
Point2D p0 = controlPoints[0];
double theta = orientation.toRadians();
// + at p0 (left), - at p0 + (length, 0) rotated (right)
double dx = Math.cos(theta) * length;
double dy = Math.sin(theta) * length;
controlPoints[1] = new Point2D.Double(p0.getX() + dx, p0.getY() + dy);
}
@Override
public int getControlPointCount() { return 2; }
@Override
public Point2D getControlPoint(int index) { return controlPoints[index]; }
@Override
public void setControlPoint(Point2D point, int index) {
controlPoints[index].setLocation(point);
if (index == 0) {
updateControlPoints();
}
}
@Override
public boolean isControlPointSticky(int index) { return true; }
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.WHEN_SELECTED;
}
@Override
public boolean canPointMoveFreely(int pointIndex) { return false; }
@Override
public boolean canControlPointOverlap(int index) { return false; }
@Override
public String getControlPointNodeName(int index) {
return index == 0 ? "+ (VCC)" : "- (GND)";
}
@Override
public String getValue() { return value; }
@Override
public void setValue(String value) { this.value = value; }
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double length = new Size(cellCount.getLengthMm(), SizeUnit.mm).convertToPixels();
double width = new Size(cellCount.getWidthMm(), SizeUnit.mm).convertToPixels();
double cellD = new Size(18.5d, SizeUnit.mm).convertToPixels();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
Composite oldComposite = applyAlpha(g2d, componentState);
// Border color
Color borderColor;
if (outlineMode) {
Theme theme = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
borderColor = (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING)
? SELECTION_COLOR : (theme != null ? theme.getOutlineColor() : Color.BLACK);
} else {
borderColor = (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING)
? SELECTION_COLOR : HOLDER_BODY.darker();
}
double bodyY = y - width / 2.0;
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : HOLDER_BODY);
g2d.fill(new RoundRectangle2D.Double(x, bodyY, length, width, 10, 10));
drawingObserver.stopTracking();
g2d.setColor(borderColor);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(new RoundRectangle2D.Double(x, bodyY, length, width, 10, 10));
if (!outlineMode) {
int cells = cellCount.getCells();
double cellWidth = (width - 8.0) / cells;
for (int i = 0; i < cells; i++) {
double cavityX = x + 6;
double cavityY = bodyY + 4 + i * cellWidth;
double cavityW = length - 12;
double cavityH = cellWidth - 4;
// Cell cavity (dark inset)
g2d.setColor(CELL_CAVITY);
g2d.fill(new RoundRectangle2D.Double(cavityX, cavityY, cavityW, cavityH, 6, 6));
g2d.setColor(HOLDER_BODY_LIGHT);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.draw(new RoundRectangle2D.Double(cavityX, cavityY, cavityW, cavityH, 6, 6));
// Positive flat plate button contact on left (x)
double plateX = x + 6;
double plateW = 6;
double plateH = Math.min(14, cellH(cellD));
double contactY = cavityY + cavityH / 2.0;
g2d.setColor(PLATE_COLOR);
g2d.fill(new Rectangle2D.Double(plateX, contactY - plateH / 2.0, plateW, plateH));
g2d.setColor(SPRING_BORDER);
g2d.draw(new Rectangle2D.Double(plateX, contactY - plateH / 2.0, plateW, plateH));
// Negative spring contact on right (x + length)
double springX = x + length - 16;
double springW = 10;
double springH = Math.min(14, cellH(cellD));
g2d.setColor(SPRING_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(new Rectangle2D.Double(springX, contactY - springH / 2.0, springW, springH));
g2d.draw(new Rectangle2D.Double(springX + 3, contactY - springH / 2.0 + 2, springW - 6, springH - 4));
// Polarity markings (+ on left, - on right)
g2d.setColor(Color.WHITE);
g2d.setFont(LABEL_FONT);
StringUtils.drawCenteredText(g2d, "+", x + 20, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "-", x + length - 20, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// 18650 text in cavity
g2d.setColor(Color.decode("#555555"));
StringUtils.drawCenteredText(g2d, "18650", x + length / 2.0, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
// Series connector bar between cells for 2S
if (cells > 1) {
g2d.setColor(SPRING_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f));
double midY = bodyY + width / 2.0;
g2d.draw(new Rectangle2D.Double(x + length - 10, midY - 6, 6, 12));
}
}
g2d.setTransform(oldTx);
// Draw control point solder tabs (Point 0 = + left, Point 1 = - right)
if (!outlineMode) {
for (Point2D cp : controlPoints) {
g2d.setColor(PLATE_COLOR);
g2d.fill(new Ellipse2D.Double(cp.getX() - 5, cp.getY() - 5, 10, 10));
g2d.setColor(SPRING_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.draw(new Ellipse2D.Double(cp.getX() - 5, cp.getY() - 5, 10, 10));
// Inner hole
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(cp.getX() - 2, cp.getY() - 2, 4, 4));
}
}
g2d.setComposite(oldComposite);
}
private double cellH(double cellD) {
return cellD * 0.45;
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// Dark holder body
g2d.setColor(HOLDER_BODY);
g2d.fill(new RoundRectangle2D.Double(2, height / 2.0 - 8, width - 4, 16, 4, 4));
g2d.setColor(HOLDER_BODY.brighter());
g2d.draw(new RoundRectangle2D.Double(2, height / 2.0 - 8, width - 4, 16, 4, 4));
// Cell cavity
g2d.setColor(CELL_CAVITY);
g2d.fill(new RoundRectangle2D.Double(5, height / 2.0 - 6, width - 10, 12, 3, 3));
// Plate on left (+), spring on right (-)
g2d.setColor(PLATE_COLOR);
g2d.fillRect(5, height / 2 - 3, 3, 6);
g2d.setColor(SPRING_COLOR);
g2d.fillRect(width - 9, height / 2 - 3, 4, 6);
// Polarity
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 7));
StringUtils.drawCenteredText(g2d, "+", 12, height / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "-", width - 13, height / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
@Override
public Rectangle2D getCachingBounds() {
double minX = Math.min(controlPoints[0].getX(), controlPoints[1].getX());
double minY = Math.min(controlPoints[0].getY(), controlPoints[1].getY());
double maxX = Math.max(controlPoints[0].getX(), controlPoints[1].getX());
double maxY = Math.max(controlPoints[0].getY(), controlPoints[1].getY());
double width = new Size(cellCount.getWidthMm(), SizeUnit.mm).convertToPixels();
int margin = 50;
return new Rectangle2D.Double(minX - margin, minY - width / 2.0 - margin,
(maxX - minX) + 2 * margin, (maxY - minY) + width + 2 * margin);
}
}
@@ -0,0 +1,225 @@
/*
*
* 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.modules;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Active Buzzer Module (KY-012)", category = "Modules & Breakouts",
author = "Branislav Stojkovic", description = "KY-012 Active Buzzer Module — produces 2.5kHz continuous tone on HIGH signal",
instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class ActiveBuzzerModule extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_GREEN = Color.decode("#1A6B1A");
public static Color BUZZER_BLACK = Color.decode("#1A1A1A");
public static Color BUZZER_VENT_COLOR = Color.decode("#333333");
public static Size BOARD_WIDTH = new Size(18.5d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(15.0d, SizeUnit.mm);
// Standard Keyes KY-012 3-pin layout from left to right: - (GND), + (VCC), S (Signal)
private static final String[] PIN_NAMES = {"- (GND)", "+ (VCC)", "S (Signal)"};
public ActiveBuzzerModule() {
super();
this.bodyColor = PCB_GREEN;
updateControlPoints();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
// 3 pins along the bottom edge: - (GND), + (VCC), S (Signal)
double[][] relativeOffsets = new double[][] {
{ 0, 0 },
{ spacing, 0 },
{ spacing * 2, 0 }
};
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - boardH + 12;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - boardH + 12;
double spacing = PIN_SPACING.convertToPixels();
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
// Buzzer cylinder — circular element on the board
double buzzerD = new Size(11.0d, SizeUnit.mm).convertToPixels();
double buzzerX = boardX + (boardW - buzzerD) / 2.0;
double buzzerY = boardY + 4;
// Outer black cylinder
g2d.setColor(BUZZER_BLACK);
g2d.fill(new Ellipse2D.Double(buzzerX, buzzerY, buzzerD, buzzerD));
g2d.setColor(BUZZER_BLACK.brighter());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(new Ellipse2D.Double(buzzerX, buzzerY, buzzerD, buzzerD));
// Vent hole ring (inner concentric)
double ventD = buzzerD * 0.55;
double ventX = buzzerX + (buzzerD - ventD) / 2.0;
double ventY = buzzerY + (buzzerD - ventD) / 2.0;
g2d.setColor(BUZZER_VENT_COLOR);
g2d.draw(new Ellipse2D.Double(ventX, ventY, ventD, ventD));
// Small central sound vent hole
double holeD = buzzerD * 0.15;
double holeX = buzzerX + (buzzerD - holeD) / 2.0;
double holeY = buzzerY + (buzzerD - holeD) / 2.0;
g2d.setColor(Color.decode("#111111"));
g2d.fill(new Ellipse2D.Double(holeX, holeY, holeD, holeD));
// Polarity "+" marking on buzzer casing
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 10));
StringUtils.drawCenteredText(g2d, "+", buzzerX + 6, buzzerY + 7, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// "KY-012" model text in top-left silkscreen corner
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 8));
StringUtils.drawCenteredText(g2d, "KY-012", boardX + 14, boardY + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Silkscreen pin labels (-, +, S) directly above the 3 header pins
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 9));
double labelY = y - 9;
StringUtils.drawCenteredText(g2d, "-", x, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "+", x + spacing, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "S", x + spacing * 2, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
g2d.setTransform(oldTx);
drawPins(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// Green PCB
g2d.setColor(PCB_GREEN);
g2d.fill(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3));
g2d.setColor(PCB_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3));
// Black buzzer cylinder
int bD = width - 10;
int bX = (width - bD) / 2;
int bY = 5;
g2d.setColor(BUZZER_BLACK);
g2d.fillOval(bX, bY, bD, bD);
g2d.setColor(Color.DARK_GRAY);
g2d.drawOval(bX, bY, bD, bD);
// Vent circle
int vD = bD / 2;
g2d.setColor(BUZZER_VENT_COLOR);
g2d.drawOval(bX + (bD - vD) / 2, bY + (bD - vD) / 2, vD, vD);
// 3 pin dots
g2d.setColor(PIN_COLOR);
for (int i = 0; i < 3; i++) {
g2d.fillRect(6 + i * 4, height - 5, 2, 2);
}
}
}
@@ -0,0 +1,245 @@
/*
*
* 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.modules;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
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.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "GPS Module (NEO-6M)", category = "Modules & Breakouts",
author = "Branislav Stojkovic", description = "u-blox NEO-6M GPS Module with ceramic patch antenna",
instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class GPSModuleNEO6M extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_GREEN = Color.decode("#1A6B1A");
public static Color ANTENNA_BEIGE = Color.decode("#D5C4A1");
public static Color ANTENNA_BORDER = Color.decode("#928374");
public static Color SHIELD_COLOR = Color.decode("#B8B8B8");
public static Color SHIELD_BORDER = Color.decode("#888888");
public static Color BATTERY_COLOR = Color.decode("#D0D3D4");
public static Size BOARD_WIDTH = new Size(35.5d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(25.5d, SizeUnit.mm);
// Standard GY-GPS6MV2 pinout from pin 1 to 4: VCC, RX, TX, GND
private static final String[] PIN_NAMES = {"VCC", "RX", "TX", "GND"};
public GPSModuleNEO6M() {
super();
this.bodyColor = PCB_GREEN;
updateControlPoints();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
// 4 pins along the left edge: VCC, RX, TX, GND
double[][] relativeOffsets = new double[][] {
{ 0, 0 },
{ 0, spacing },
{ 0, spacing * 2 },
{ 0, spacing * 3 }
};
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double spacing = PIN_SPACING.convertToPixels();
double boardX = x - 14.0;
double boardY = y - (boardH - 3 * spacing) / 2.0;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double spacing = PIN_SPACING.convertToPixels();
double boardX = x - 14.0;
double boardY = y - (boardH - 3 * spacing) / 2.0;
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
// 4 Small Mounting holes in the corners
double holeMargin = 7.0;
drawMountingHole(g2d, boardX + holeMargin, boardY + holeMargin, 8.0);
drawMountingHole(g2d, boardX + holeMargin, boardY + boardH - holeMargin, 8.0);
drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + holeMargin, 8.0);
drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + boardH - holeMargin, 8.0);
// Silkscreen pin labels (VCC, RX, TX, GND) placed neatly next to left header
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 8));
String[] pinLabels = new String[] {"VCC", "RX", "TX", "GND"};
for (int i = 0; i < 4; i++) {
StringUtils.drawCenteredText(g2d, pinLabels[i], x + 9, y + i * spacing, HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
}
// NEO-6M metal shield can (center of board, strictly to the right of pin labels)
double icShieldX = boardX + 46.0;
double icShieldW = 42.0;
double icShieldH = 36.0;
double icShieldY = boardY + (boardH - icShieldH) / 2.0 - 8.0;
g2d.setColor(SHIELD_COLOR);
g2d.fill(new RoundRectangle2D.Double(icShieldX, icShieldY, icShieldW, icShieldH, 4, 4));
g2d.setColor(SHIELD_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.draw(new RoundRectangle2D.Double(icShieldX, icShieldY, icShieldW, icShieldH, 4, 4));
g2d.setColor(Color.decode("#333333"));
g2d.setFont(new Font("SansSerif", Font.BOLD, 8));
StringUtils.drawCenteredText(g2d, "NEO-6M", icShieldX + icShieldW / 2.0, icShieldY + icShieldH / 2.0 - 4,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
g2d.setFont(new Font("SansSerif", Font.PLAIN, 6));
StringUtils.drawCenteredText(g2d, "GPS", icShieldX + icShieldW / 2.0, icShieldY + icShieldH / 2.0 + 5,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Rechargeable backup coin cell battery (CR1220 / MS621) below shield
double battD = 18.0;
double battX = boardX + 58.0;
double battY = boardY + boardH - battD - 7.0;
g2d.setColor(BATTERY_COLOR);
g2d.fill(new Ellipse2D.Double(battX, battY, battD, battD));
g2d.setColor(BATTERY_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.draw(new Ellipse2D.Double(battX, battY, battD, battD));
// Ceramic patch antenna on right side (20x20mm)
double antSize = new Size(18.0d, SizeUnit.mm).convertToPixels();
double antX = boardX + boardW - antSize - 8.0;
double antY = boardY + (boardH - antSize) / 2.0;
g2d.setColor(ANTENNA_BEIGE);
g2d.fill(new RoundRectangle2D.Double(antX, antY, antSize, antSize, 2, 2));
g2d.setColor(ANTENNA_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.2f));
g2d.draw(new RoundRectangle2D.Double(antX, antY, antSize, antSize, 2, 2));
// Center silver electrode dot on antenna
double antCX = antX + antSize / 2.0;
double antCY = antY + antSize / 2.0;
g2d.setColor(Color.decode("#7F8C8D"));
g2d.fill(new Ellipse2D.Double(antCX - 2, antCY - 2, 4, 4));
// Status LED (top left area)
g2d.setColor(Color.RED);
g2d.fill(new Ellipse2D.Double(boardX + 24.0, boardY + 8.0, 5, 5));
}
g2d.setTransform(oldTx);
drawPins(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// Green PCB
g2d.setColor(PCB_GREEN);
g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3));
g2d.setColor(PCB_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3));
// Beige ceramic patch antenna (right half)
int antS = height - 12;
g2d.setColor(ANTENNA_BEIGE);
g2d.fillRect(width - antS - 3, (height - antS) / 2, antS, antS);
g2d.setColor(ANTENNA_BORDER);
g2d.drawRect(width - antS - 3, (height - antS) / 2, antS, antS);
// Shield
g2d.setColor(SHIELD_COLOR);
g2d.fillRoundRect(4, (height - 10) / 2, 10, 10, 2, 2);
}
}
@@ -0,0 +1,317 @@
/*
*
* 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.modules;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Current/Power Sensor (INA219)", category = "Modules & Breakouts",
author = "Branislav Stojkovic", description = "INA219 I2C High-Side Current and Power Sensor Breakout",
instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class INA219CurrentSensor extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_GREEN = Color.decode("#115E1B");
public static Color TERMINAL_BLUE = Color.decode("#1565C0");
public static Color TERMINAL_BLUE_BORDER = Color.decode("#0D47A1");
public static Color SHUNT_BODY = Color.decode("#7D6608");
public static Color SHUNT_METAL = Color.decode("#D4AC0D");
public static Color SHUNT_CORE = Color.decode("#2C3E50");
public static Color SILK_YELLOW = Color.decode("#F4D03F");
public static Size BOARD_WIDTH = new Size(25.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(25.0d, SizeUnit.mm);
// 6 header pins at bottom (0-5) + 2 screw terminal pins at top (6-7)
private static final String[] PIN_NAMES = {
"VCC", "GND", "SCL", "SDA", "VIN-", "VIN+",
"VIN- (Term)", "VIN+ (Term)"
};
public INA219CurrentSensor() {
super();
this.bodyColor = PCB_GREEN;
updateControlPoints();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
// 8 control points total:
// Pins 0-5: Bottom horizontal 6-pin header (VCC, GND, SCL, SDA, VIN-, VIN+)
// Pins 6-7: Top 2-pin screw terminal block (VIN-, VIN+)
double termSpacing = new Size(5.08d, SizeUnit.mm).convertToPixels();
double termCenterX = 2.5 * spacing;
double termY = -boardH + 24.0;
double[][] relativeOffsets = new double[][] {
{ 0, 0 },
{ spacing, 0 },
{ spacing * 2, 0 },
{ spacing * 3, 0 },
{ spacing * 4, 0 },
{ spacing * 5, 0 },
{ termCenterX - termSpacing / 2.0, termY },
{ termCenterX + termSpacing / 2.0, termY }
};
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double spacing = PIN_SPACING.convertToPixels();
double boardX = x - (boardW - 5 * spacing) / 2.0;
double boardY = y - boardH + 12;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double spacing = PIN_SPACING.convertToPixels();
double boardX = x - (boardW - 5 * spacing) / 2.0;
double boardY = y - boardH + 12;
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
// 4 Large Gold Ring Mounting Holes in corners (20mm x 20mm pitch)
double holeMargin = 10.0;
drawMountingHole(g2d, boardX + holeMargin, boardY + holeMargin, 13.0);
drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + holeMargin, 13.0);
drawMountingHole(g2d, boardX + holeMargin, boardY + boardH - holeMargin, 13.0);
drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + boardH - holeMargin, 13.0);
// Top Blue Screw Terminal Block (5.08mm 2-pin block)
double termW = 44.0;
double termH = 30.0;
double termX = boardX + (boardW - termW) / 2.0;
double termY = boardY + 2.0;
// Blue outer block body
g2d.setColor(TERMINAL_BLUE);
g2d.fill(new RoundRectangle2D.Double(termX, termY, termW, termH, 3, 3));
g2d.setColor(TERMINAL_BLUE_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.draw(new RoundRectangle2D.Double(termX, termY, termW, termH, 3, 3));
// Terminal block wire entry slots on top
g2d.setColor(Color.decode("#0D47A1"));
g2d.fillRect((int) (termX + 4), (int) (termY + 1), (int) (termW - 8), 4);
// Screw contacts
double termSpacing = new Size(5.08d, SizeUnit.mm).convertToPixels();
double screw1X = termX + termW / 2.0 - termSpacing / 2.0;
double screw2X = termX + termW / 2.0 + termSpacing / 2.0;
double screwY = termY + termH / 2.0 + 2.0;
double screwD = 14.0;
g2d.setColor(SCREW_CIRCLE_COLOR);
g2d.fill(new Ellipse2D.Double(screw1X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD));
g2d.fill(new Ellipse2D.Double(screw2X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD));
g2d.setColor(SCREW_CIRCLE_COLOR.darker());
g2d.draw(new Ellipse2D.Double(screw1X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD));
g2d.draw(new Ellipse2D.Double(screw2X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD));
// Screw diagonal slots
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.drawLine((int) (screw1X - 4), (int) (screwY - 4), (int) (screw1X + 4), (int) (screwY + 4));
g2d.drawLine((int) (screw2X - 4), (int) (screwY - 4), (int) (screw2X + 4), (int) (screwY + 4));
// Shunt Resistor (Center-Upper, directly below terminal block)
double shuntW = 32.0;
double shuntH = 14.0;
double shuntX = boardX + (boardW - shuntW) / 2.0;
double shuntY = boardY + termH + 6.0;
// Copper outer tabs
g2d.setColor(Color.decode("#C0392B"));
g2d.fill(new RoundRectangle2D.Double(shuntX, shuntY, 7, shuntH, 2, 2));
g2d.fill(new RoundRectangle2D.Double(shuntX + shuntW - 7, shuntY, 7, shuntH, 2, 2));
// Metal sense element (arched center)
g2d.setColor(Color.decode("#BDC3C7"));
g2d.fill(new Rectangle2D.Double(shuntX + 6, shuntY + 1, shuntW - 12, shuntH - 2));
g2d.setColor(Color.decode("#7F8C8D"));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.draw(new Rectangle2D.Double(shuntX + 6, shuntY + 1, shuntW - 12, shuntH - 2));
// "R100" label on shunt
g2d.setColor(Color.decode("#2C3E50"));
g2d.setFont(new Font("SansSerif", Font.BOLD, 7));
StringUtils.drawCenteredText(g2d, "R100", shuntX + shuntW / 2.0, shuntY + shuntH / 2.0,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// INA219 SOIC-8 IC (Center-Lower, directly below shunt)
double icW = 20.0;
double icH = 14.0;
double icX = boardX + (boardW - icW) / 2.0;
double icY = shuntY + shuntH + 4.0;
// IC body
g2d.setColor(Color.decode("#1C2833"));
g2d.fill(new RoundRectangle2D.Double(icX, icY, icW, icH, 2, 2));
g2d.setColor(Color.decode("#111111"));
g2d.draw(new RoundRectangle2D.Double(icX, icY, icW, icH, 2, 2));
// 8 silver IC pins (4 top, 4 bottom)
g2d.setColor(Color.decode("#BDC3C7"));
for (int i = 0; i < 4; i++) {
double px = icX + 2.5 + i * 4.5;
g2d.fillRect((int) px, (int) (icY - 2), 2, 2);
g2d.fillRect((int) px, (int) (icY + icH), 2, 2);
}
// Pin 1 dot
g2d.setColor(Color.decode("#F4D03F"));
g2d.fill(new Ellipse2D.Double(icX + 2.5, icY + 2.5, 2.5, 2.5));
// Vertical silkscreen text on left edge
AffineTransform preTextTx = g2d.getTransform();
g2d.translate(boardX + 14, boardY + boardH / 2.0);
g2d.rotate(-Math.PI / 2.0);
g2d.setColor(SILK_YELLOW);
g2d.setFont(new Font("SansSerif", Font.BOLD, 7));
StringUtils.drawCenteredText(g2d, "INA219 SENSOR", 0, 0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
g2d.setTransform(preTextTx);
// Vertical silkscreen labels above bottom 6 header pins (VCC, GND, SCL, SDA, VIN-, VIN+)
String[] headerLabels = new String[] {"VCC", "GND", "SCL", "SDA", "VIN-", "VIN+"};
g2d.setFont(new Font("SansSerif", Font.BOLD, 7));
g2d.setColor(SILK_YELLOW);
for (int i = 0; i < 6; i++) {
double pinX = x + i * spacing;
AffineTransform pTx = g2d.getTransform();
g2d.translate(pinX, y - 11);
g2d.rotate(-Math.PI / 2.0);
StringUtils.drawCenteredText(g2d, headerLabels[i], 0, 0, HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
g2d.setTransform(pTx);
}
}
g2d.setTransform(oldTx);
// Draw bottom 6 header pins
drawPins(g2d, 0, 6, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// Green square PCB
g2d.setColor(PCB_GREEN);
g2d.fill(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3));
g2d.setColor(PCB_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3));
// Blue terminal block at top
g2d.setColor(TERMINAL_BLUE);
g2d.fillRoundRect(width / 2 - 8, 3, 16, 8, 2, 2);
// Shunt in middle
g2d.setColor(Color.decode("#C0392B"));
g2d.fillRect(width / 2 - 6, 13, 12, 4);
g2d.setColor(Color.decode("#BDC3C7"));
g2d.fillRect(width / 2 - 3, 13, 6, 4);
// Small IC below shunt
g2d.setColor(Color.decode("#1C2833"));
g2d.fillRect(width / 2 - 4, 19, 8, 5);
// 6 pin dots at bottom
g2d.setColor(PIN_COLOR);
for (int i = 0; i < 6; i++) {
g2d.fillRect(4 + i * 4, height - 5, 2, 2);
}
}
}
@@ -0,0 +1,214 @@
/*
*
* 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.sensors;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "IR Receiver Module (KY-022)", category = "Sensors",
author = "Branislav Stojkovic", description = "38kHz Infrared Receiver Module (VS1838B) for remote control",
instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class IRReceiverModule extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_GREEN = Color.decode("#1A6B1A");
public static Color SENSOR_METAL = Color.decode("#85929E");
public static Color SENSOR_DOME = Color.decode("#1C2833");
public static Size BOARD_WIDTH = new Size(18.5d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(15.0d, SizeUnit.mm);
// Standard Keyes KY-022 3-pin layout from left to right: - (GND), + (VCC), S (Signal)
private static final String[] PIN_NAMES = {"- (GND)", "+ (VCC)", "S (Signal)"};
public IRReceiverModule() {
super();
this.bodyColor = PCB_GREEN;
updateControlPoints();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
// 3 pins along the bottom edge: - (GND), + (VCC), S (Signal)
double[][] relativeOffsets = new double[][] {
{ 0, 0 },
{ spacing, 0 },
{ spacing * 2, 0 }
};
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - boardH + 12;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - boardH + 12;
double spacing = PIN_SPACING.convertToPixels();
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
// VS1838B sensor element — metal-shielded rectangular body with central black IR dome
double sensorW = new Size(7.5d, SizeUnit.mm).convertToPixels();
double sensorH = new Size(6.0d, SizeUnit.mm).convertToPixels();
double sensorX = boardX + (boardW - sensorW) / 2.0;
double sensorY = boardY + 6;
// Metal shielded box
g2d.setColor(SENSOR_METAL);
g2d.fill(new RoundRectangle2D.Double(sensorX, sensorY, sensorW, sensorH, 3, 3));
g2d.setColor(SENSOR_METAL.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.draw(new RoundRectangle2D.Double(sensorX, sensorY, sensorW, sensorH, 3, 3));
// Central dark IR lens dome
double domeD = sensorH * 0.75;
g2d.setColor(SENSOR_DOME);
g2d.fill(new Ellipse2D.Double(sensorX + (sensorW - domeD) / 2.0, sensorY + (sensorH - domeD) / 2.0, domeD, domeD));
g2d.setColor(Color.BLACK);
g2d.draw(new Ellipse2D.Double(sensorX + (sensorW - domeD) / 2.0, sensorY + (sensorH - domeD) / 2.0, domeD, domeD));
// Red status LED (top right)
g2d.setColor(Color.RED);
g2d.fill(new Ellipse2D.Double(boardX + boardW - 12, boardY + 6, 6, 6));
// "KY-022" silkscreen text in the open space between the sensor and pin labels
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 9));
StringUtils.drawCenteredText(g2d, "KY-022", boardX + boardW / 2.0, boardY + 36, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Silkscreen pin labels (-, +, S) directly above the 3 header pins
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 9));
double labelY = y - 9;
StringUtils.drawCenteredText(g2d, "-", x, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "+", x + spacing, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "S", x + spacing * 2, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
g2d.setTransform(oldTx);
drawPins(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// Green PCB
g2d.setColor(PCB_GREEN);
g2d.fill(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3));
g2d.setColor(PCB_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3));
// Metal shield + dome
g2d.setColor(SENSOR_METAL);
g2d.fillRoundRect(width / 2 - 5, 6, 10, 8, 2, 2);
g2d.setColor(SENSOR_DOME);
g2d.fillOval(width / 2 - 3, 7, 6, 6);
// Red LED
g2d.setColor(Color.RED);
g2d.fillOval(width - 7, 7, 4, 4);
// 3 pin dots
g2d.setColor(PIN_COLOR);
for (int i = 0; i < 3; i++) {
g2d.fillRect(6 + i * 4, height - 5, 2, 2);
}
}
}
@@ -33,6 +33,7 @@ import org.diylc.common.ComponentType;
import org.diylc.common.DefaultComponentTransformer;
import org.diylc.common.Orientation;
import org.diylc.components.displays.*;
import org.diylc.components.electromechanical.*;
import org.diylc.components.micro.*;
import org.diylc.components.modules.*;
import org.diylc.components.robotics.*;
@@ -78,6 +79,7 @@ public class MakerComponentsTest {
BME280Sensor.class,
SoilMoistureSensor.class,
TCRT5000Sensor.class,
IRReceiverModule.class,
// Displays & Outputs
CharacterLCD.class,
@@ -86,6 +88,7 @@ public class MakerComponentsTest {
LEDMatrix8x8.class,
TFTDisplay.class,
WS2812BStick.class,
WS2812BRing.class,
// Modules & Breakouts
LogicLevelConverter.class,
@@ -106,6 +109,12 @@ public class MakerComponentsTest {
ULN2003Driver.class,
PCA9685ServoDriver.class,
MOSFETSwitchModule.class,
ActiveBuzzerModule.class,
GPSModuleNEO6M.class,
INA219CurrentSensor.class,
// Electro-Mechanical
BatteryHolder18650.class,
// Robotics
MicroServoSG90.class,