Controllers (#1080)

* initial

* more components

* fixes

* Update TreePanel.java

* fix boost converter

* Improve wrapping of component names

* Update ULN2003Driver.java

* Update NRF24L01Transceiver.java

* imp

* updates

* improvements

* more components

* arduino

* uno

* icon

* cleanup

* mega

* mega

* improveent

* nano

* esp boards

* esp

* esp32

* esp32

* Update ESP8266NodeMCU.java

* d1

* Update WemosD1Mini.java

* pi

* consolidate esp32

* pi pico updates

* pi zero

* Update RaspberryPiZero.java

* pi pico

* cleanup

* pi improvements

* consolidate pins

* antenna consolidation

* improve solder pads

* teensy

* headers

* teensy fix, test cleanup

* s3

* Update ESP32DevKit.java

* usb standardization

* pico w

* button standardization

* arduino improvements

* Update AbstractMakerBoard.java

* Update RaspberryPiZero.java

* Update RaspberryPi.java

* pi 3

* pi 5 improvements

* Update RaspberryPiZero.java

* fix tests

* esp pinout fix

* Update ESP32DevKit.java

* pin name improvements

* Create tier-1-modern-boards.md

* new versions

* pinouts

* nano improvements

* sanitize node names

* label improvements

* minima fixes

* arduino uno labels

* nano labels

* Update ArduinoNano.java

* mega

* mega

* comment out future stuff

* release prep

* Update RaspberryPiPico.java

* Refactor abstract class

* labeling improvement

* label offset fixes

* Update Teensy.java

* consolidate tests

* micro variants

* Update RaspberryPi.java

* displays tweaks

* display improvements

* neopix

* displays

* improvements

* led matrix

* strip

* led bar graph

* jewel

* tft

* tft

* Update maker-peripherals.md

* nokia

* led bar

* labels

* tft

* cleanup

* oled

* cleanup

* display enhancement

* displays

* neopixel

* displays

* displays

* new tft

* displays

* icon

* variants

* 2-digit 0.56 inch 7 segment

* Added ability to hide component categories in the component tree and toolbar from the Config menu

* led bar

* Update variants.xml

* color selection for bar graph

* Added 1.8" sd

* tft fixes

* Update WS2812BRing.java

* neopixel improvements

* netlist and node name improvements

* oled tweak

* cleanup

* mockito fix

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bane Stojkovic
2026-10-09 09:38:09 +02:00
committed by GitHub
parent f62b3ddf04
commit a743de4024
87 changed files with 11343 additions and 1933 deletions
@@ -0,0 +1,74 @@
/*
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;
import java.awt.Color;
import org.diylc.core.annotations.EditableProperty;
/**
* Base for the boards built out of addressable 5050 LEDs -- the rings, the jewel, the stick, the
* strip, the matrix and the bare breakout.
*
* <p>Every one of them is sold fitted with any of the packages {@link LedType} lists, on the same
* PCB with the same footprint, so the package is a property of the board rather than a board of
* its own. What it changes is what the LEDs can emit, which is the palette here, and what the BOM
* has to ask for, which each board folds into its own variant label.
*
* @author Branislav Stojkovic
*/
public abstract class AbstractAddressableLedBoard extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
protected LedType ledType = LedType.RGB;
// Rebuilt only when the count or the package changes, because draw() runs on every repaint.
private transient Color[] ledColors;
@EditableProperty(name = "LED Type")
public LedType getLedType() {
if (ledType == null) {
ledType = LedType.RGB;
}
return ledType;
}
public void setLedType(LedType ledType) {
this.ledType = ledType;
this.ledColors = null;
invalidateCache();
}
/**
* The colours {@code count} LEDs are drawn lit with. These parts are drawn lit because unlit
* they are a dark board with white specks on it, which reads as no particular component; running
* the palette around the part also says at a glance which package it is fitted with, a WWA one
* having no colour to show.
*/
protected Color[] getLedColors(int count) {
if (ledColors == null || ledColors.length != count) {
ledColors = buildLedGradient(count, getLedType().getGradient());
}
return ledColors;
}
}
@@ -24,6 +24,7 @@ package org.diylc.components;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
@@ -90,12 +91,28 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
public static Color BUTTON_ACTUATOR_COLOR = Color.decode("#A0A0A0");
public static Color DARK_PLASTIC_COLOR = Color.decode("#181818");
public static Color DARK_PLASTIC_BORDER = Color.decode("#333333");
public static Color RGB_LED_BODY_COLOR = Color.decode("#FDFEFE");
public static Color RGB_LED_BODY_BORDER = Color.decode("#D0D3D4");
public static Color RGB_LED_LENS_COLOR = Color.decode("#EAECEE");
public static Color RGB_LED_LENS_BORDER = Color.decode("#BDC3C7");
public static Color RGB_LED_CHIP_COLOR = Color.decode("#333333");
// The colour wheel addressable RGB parts are drawn lit with. Treated as a loop, so a gradient
// spread over any LED count closes without a seam.
public static Color[] RGB_LED_GRADIENT = new Color[] {
Color.decode("#FFEE00"), Color.decode("#FF8800"), Color.decode("#FF1122"),
Color.decode("#AA22EE"), Color.decode("#2255FF"), Color.decode("#22DD44")};
// A WWA package carries no colour die, so a part fitted with one cannot show the wheel above.
// Its three anchors are the three dies themselves: warm white, white and amber.
public static Color[] WWA_LED_GRADIENT = new Color[] {
Color.decode("#FFE0BD"), Color.decode("#FFFFFF"), Color.decode("#FFAA00")};
public static Color SILK_COLOR = Color.WHITE;
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
public static Size PAD_SIZE = new Size(0.065d, SizeUnit.in);
public static Size HOLE_SIZE = new Size(0.7d, SizeUnit.mm);
// the 5050 package shared by every addressable RGB part on a maker board
public static Size RGB_LED_SIZE = new Size(5.0d, SizeUnit.mm);
public static Size NOTCH_SIZE = new Size(0.9d, SizeUnit.mm);
public static Size ANTENNA_WIDTH = new Size(15.0d, SizeUnit.mm);
public static Size ANTENNA_LENGTH = new Size(7.0d, SizeUnit.mm);
@@ -149,6 +166,14 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
// labels along a row run across the board rather than down a column of pins, so they can afford
// a size the tightly stacked column labels cannot
public static Font PIN_ROW_FONT = new Font(SILK_FONT_FAMILY, Font.PLAIN, 9);
// a label lying along its row has only the pin pitch to fit into, so it runs a size down from
// the rotated ones and sits closer to the pin, which is all the room a shallow strip has
public static Font PIN_ROW_FLAT_FONT = new Font(SILK_FONT_FAMILY, Font.PLAIN, 8);
public static Size PIN_ROW_FLAT_LABEL_OFFSET = new Size(2.04d, SizeUnit.mm);
// Clearance kept between text printed on a lit panel and the panel's edge. A readout is drawn
// inside the glass rather than on board material, so unlike a silk label it has no neighbour to
// collide with; the margin only stops the longest line from touching the frame.
public static Size SCREEN_TEXT_MARGIN = new Size(0.6d, SizeUnit.mm);
protected Orientation orientation = Orientation.DEFAULT;
protected Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
@@ -223,13 +248,31 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
@Override
public void setValue(Void value) {}
/**
* The variant this board is set to, or {@code null} for a board that is sold in only one form.
* Boards name their variant property differently -- {@code Version}, {@code Size}, {@code Type} --
* so each one that has one answers here for itself.
*/
protected String getVariantLabel() {
return null;
}
/**
* These boards have no value in the usual sense, so the BOM's value column carries the variant
* instead. It is not cosmetic: {@code BomMaker} groups rows on type name plus value, so without
* it every variant of a board collapses into a single row and a BOM cannot say which one to buy.
*/
@Override
public String getValueForDisplay() {
String variant = getVariantLabel();
return variant == null ? "" : variant;
}
protected abstract void updateControlPoints();
protected void invalidateCache() {}
/**
* Helper to rotate an array of relative point offsets around the origin (first point) by orientation.
*/
/** Rotates relative point offsets around the first point by the board's orientation. */
protected void rotatePoints(Point2D firstPoint, double[][] relativeOffsets) {
controlPoints = new Point2D[relativeOffsets.length];
controlPoints[0] = firstPoint;
@@ -242,9 +285,7 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
}
/**
* Helper to draw standard pin header pins at given control point indices.
*/
/** Draws standard pin header pins at the given control point indices. */
protected void drawPinHeader(Graphics2D g2d, int startIndex, int count, boolean outlineMode,
IDrawingObserver drawingObserver) {
if (outlineMode) return;
@@ -268,12 +309,12 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to format a pin name for silkscreen display (e.g. "3V3_1" -> "3V3", "A0 (ADC0)" -> "A0").
* Formats a pin name for silkscreen display, e.g. "3V3_1" to "3V3" and "A0 (ADC0)" to "A0".
*
* <p>Stripping the parenthesized annotation is the same operation the netlist performs, so it is
* delegated to {@link Node#sanitizeNodeName(String)} rather than duplicated. The silkscreen goes
* one step further and also drops the trailing disambiguator, because a board prints "GND" on
* every ground pin even though the netlist has to tell "GND_1" from "GND_2".
* delegated to {@link Node#sanitizeNodeName(String)}. The silkscreen also drops the trailing
* disambiguator, because a board prints "GND" on every ground pin even though the netlist has to
* tell "GND_1" from "GND_2".
*/
public static String getDisplayPinLabel(String name) {
String sanitized = Node.sanitizeNodeName(name);
@@ -286,17 +327,15 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
/**
* Silkscreen text printed next to a pin. Defaults to the node name with its annotation and
* disambiguator stripped, which is right whenever the node name is the silkscreen name plus
* extra detail. A board whose silkscreen genuinely differs from its node names -- the ESP
* DevKits print bare GPIO numbers while their nodes carry the full function list -- overrides
* this to return its own silkscreen label.
* disambiguator stripped. A board whose silkscreen genuinely differs -- the ESP DevKits print
* bare GPIO numbers while their nodes carry the full function list -- overrides this.
*/
protected String getSilkPinLabel(int index) {
return getDisplayPinLabel(getControlPointNodeName(index));
}
/**
* Helper to draw rotated control point / pin names next to the pins for dual-row DIP/header boards.
* Draws rotated pin names beside the pins of a dual-row DIP or header board.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated top-left pin X coordinate (P0.getX())
@@ -351,9 +390,8 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
/**
* Variant for boards too crowded for labels that stand square to their row. Each label is turned
* 45 degrees and tucked into the corner of its pad on the side the row reads towards - the top
* right corner for the left row and the top left corner for the right one - which is the trick
* the Teensy 4.1 uses to fit its pad names between two rows a tenth of an inch apart.
* 45 degrees and tucked into the corner of its pad on the side the row reads towards, which is
* how the Teensy 4.1 fits its pad names between two rows a tenth of an inch apart.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated top-left pin X coordinate (P0.getX())
@@ -394,9 +432,9 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to draw rotated pin names next to a horizontal row of pins, the way Arduino-style boards
* print their headers. Labels read top to bottom, are centered on the pin they belong to and
* start {@link #PIN_ROW_LABEL_OFFSET} away from it, growing away from the row.
* Draws rotated pin names beside a horizontal row, the way Arduino-style boards print theirs.
* Labels read top to bottom, centred on their pin and starting {@link #PIN_ROW_LABEL_OFFSET}
* away from it.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated first control point X coordinate (P0.getX())
@@ -435,15 +473,63 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to draw a complete PCB terminal block (green body + inner compartment lines + screw lugs)
* that matches the visuals of the PCBTerminalBlock component.
* Baseline for labels that have to sit on one line. {@link StringUtils}'s CENTER alignment
* centres each string on its own glyph box, so a name with a descender rides higher than the
* names beside it. Measuring one reference glyph gives every label the same baseline.
*/
private static double getLabelBaseline(Graphics2D g2d, double centreY) {
Rectangle bounds = g2d.getFont()
.createGlyphVector(g2d.getFontRenderContext(), "X").getVisualBounds().getBounds();
return centreY - bounds.height / 2.0 - bounds.y;
}
/**
* Draws pin names lying flat along a horizontal row, for a board whose strip beside the header is
* shallower than the longest name is long. {@link #drawRowPinLabels} stands its labels on end and
* has the whole depth of the board to grow into; these have only the pin pitch, so they run in a
* smaller font.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated first control point X coordinate (P0.getX())
* @param y Unrotated first control point Y coordinate (P0.getY())
* @param offsets Array of [x, y] relative offsets for all control points
* @param startIndex Index of the first pin of the row
* @param count Number of pins in the row
* @param below True to print the labels below the row, false to print them above it
* @param silkColor Silkscreen text color
*/
protected void drawFlatRowPinLabels(Graphics2D g2d, double x, double y, double[][] offsets,
int startIndex, int count, boolean below, Color silkColor) {
if (offsets == null || offsets.length == 0) {
return;
}
double labelOffset = PIN_ROW_FLAT_LABEL_OFFSET.convertToPixels();
g2d.setColor(silkColor);
g2d.setFont(PIN_ROW_FLAT_FONT);
for (int i = startIndex; i < startIndex + count && i < offsets.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty()) {
continue;
}
double pinX = x + offsets[i][0];
double pinY = y + offsets[i][1] + (below ? labelOffset : -labelOffset);
// TOP passes the y straight through as the baseline, which is what keeps the row level
StringUtils.drawCenteredText(g2d, label, pinX, getLabelBaseline(g2d, pinY),
HorizontalAlignment.CENTER, VerticalAlignment.TOP);
}
}
/**
* Draws a PCB terminal block matching the visuals of the PCBTerminalBlock component.
*
* @param g2d Graphics2D context
* @param startIndex First control point index
* @param count Number of positions in this terminal block
* @param isHorizontal true if terminal block runs horizontally (along X), false if vertically (along Y)
* @param wireEntryOffset Direction/offset from pins to the wire entry edge (positive or negative)
* @param blockDepth Total depth of the green body (e.g. 50.0px for 5.08mm blocks, or 35.0px for compact)
* @param isHorizontal true if the block runs horizontally (along X), false if vertically
* @param wireEntryOffset Direction/offset from pins to the wire entry edge
* @param blockDepth Total depth of the green body in pixels
* @param outlineMode Outline mode flag
* @param drawingObserver Observer
*/
@@ -486,7 +572,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
innerCompartment = new Rectangle2D.Double(minX - pitchPx / 2.0, by, pitchPx, bh);
}
// Draw green body and compartment
g2d.setColor(SCREW_TERMINAL_COLOR);
g2d.fill(outerBody);
g2d.setColor(SCREW_TERMINAL_BORDER);
@@ -494,13 +579,11 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
g2d.draw(innerCompartment);
g2d.draw(outerBody);
// Draw screw contacts
drawScrewTerminals(g2d, startIndex, count, pitchPx, outlineMode, drawingObserver);
}
/**
* Helper to draw screw terminal block contacts at given control points.
* Visuals match the PCBTerminalBlock component: light metal screw circles with diagonal slot.
* Draws screw terminal contacts matching the PCBTerminalBlock component.
*/
protected void drawScrewTerminals(Graphics2D g2d, int startIndex, int count, double pitchPx, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) return;
@@ -511,11 +594,9 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f));
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
// Light metal screw head circle
g2d.setColor(SCREW_CIRCLE_COLOR);
g2d.fillOval((int) Math.round(p.getX() - circleDiameter / 2.0), (int) Math.round(p.getY() - circleDiameter / 2.0),
circleDiameter, circleDiameter);
// Diagonal screw slot line
g2d.setColor(SCREW_CIRCLE_COLOR.darker());
g2d.drawLine((int) (p.getX() + Math.cos(Math.PI / 4) * circleDiameter / 2.0),
(int) (p.getY() + Math.sin(Math.PI / 4) * circleDiameter / 2.0),
@@ -526,40 +607,38 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to draw rectangular tinned solder pads with drill holes at given control points.
* Spreads {@link #RGB_LED_GRADIENT} evenly over {@code count} LEDs, treating the anchors as a
* loop so the last LED runs back into the first. Called once per palette at class load.
*/
protected void drawSolderPads(Graphics2D g2d, int startIndex, int count, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) return;
double padW = 22.0;
double padH = 16.0;
double holeD = 7.0;
protected static Color[] buildLedGradient(int count) {
return buildLedGradient(count, RGB_LED_GRADIENT);
}
drawingObserver.startTrackingContinuityArea(true);
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
RoundRectangle2D pad = new RoundRectangle2D.Double(p.getX() - padW / 2.0, p.getY() - padH / 2.0, padW, padH, 2, 2);
g2d.setColor(LIGHT_METAL_COLOR);
g2d.fill(pad);
g2d.setColor(LIGHT_METAL_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(pad);
// Central through-hole / drill hole
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(p.getX() - holeD / 2.0, p.getY() - holeD / 2.0, holeD, holeD));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new Ellipse2D.Double(p.getX() - holeD / 2.0, p.getY() - holeD / 2.0, holeD, holeD));
/** As above, over a palette of the caller's choosing. */
protected static Color[] buildLedGradient(int count, Color[] anchors) {
Color[] colors = new Color[count];
int anchorCount = anchors.length;
for (int i = 0; i < count; i++) {
double position = (double) i * anchorCount / count;
int anchor = (int) position;
double blend = position - anchor;
Color from = anchors[anchor];
Color to = anchors[(anchor + 1) % anchorCount];
colors[i] = new Color(
(int) Math.round(from.getRed() + (to.getRed() - from.getRed()) * blend),
(int) Math.round(from.getGreen() + (to.getGreen() - from.getGreen()) * blend),
(int) Math.round(from.getBlue() + (to.getBlue() - from.getBlue()) * blend));
}
drawingObserver.stopTrackingContinuityArea();
return colors;
}
/**
* Helper to draw standard PCB through-hole solder pads (gold/copper pads with drill holes, square for Pin 1).
* Draws through-hole solder pads with drill holes.
*
* @param g2d Graphics2D context
* @param startIndex First control point index
* @param count Number of pads to draw
* @param squarePin1 If true, the first pad (index == startIndex) is drawn as a square pad
* @param squarePin1 If true, the first pad is drawn square
* @param outlineMode Outline mode flag
* @param drawingObserver Observer
*/
@@ -572,14 +651,12 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
if (squarePin1 && i == startIndex) {
// Pin 1 is a square solder pad
g2d.setColor(PAD_COLOR);
g2d.fill(new Rectangle2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
g2d.setColor(PAD_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new Rectangle2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
} else {
// Round solder pads
g2d.setColor(PAD_COLOR);
g2d.fill(new Ellipse2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
g2d.setColor(PAD_COLOR.darker());
@@ -587,7 +664,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
g2d.draw(new Ellipse2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
}
// Central drill hole
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(p.getX() - holeDiameter / 2.0, p.getY() - holeDiameter / 2.0, holeDiameter, holeDiameter));
g2d.setColor(PAD_COLOR.darker());
@@ -596,6 +672,95 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
drawingObserver.stopTrackingContinuityArea();
}
/**
* Draws rectangular surface pads: bare copper with no drill through it, which is what a part
* meant to be soldered to directly carries rather than the plated holes
* {@link #drawPcbSolderPads} draws. Unlike a round pad a rectangular one has an orientation, so
* each is turned with the board.
*
* @param padWidth Pad size across the board, which for an edge pad is its reach inward
* @param padLength Pad size along the row of pads
*/
protected void drawSurfacePads(Graphics2D g2d, int startIndex, int count, Size padWidth,
Size padLength, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) {
return;
}
double w = padWidth.convertToPixels();
double h = padLength.convertToPixels();
double theta = orientation.toRadians();
drawingObserver.startTrackingContinuityArea(true);
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
if (theta != 0) {
g2d.rotate(theta, p.getX(), p.getY());
}
Rectangle2D pad = new Rectangle2D.Double(p.getX() - w / 2.0, p.getY() - h / 2.0, w, h);
g2d.setColor(PAD_COLOR);
g2d.fill(pad);
g2d.setColor(PAD_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(pad);
// turned back rather than saving the transform, as the LED painter does
if (theta != 0) {
g2d.rotate(-theta, p.getX(), p.getY());
}
}
drawingObserver.stopTrackingContinuityArea();
}
/**
* Draws the pads a cut leaves on a part sold by the length. The uncut pad is a pill lying along
* the part with the cut through its middle, so each end keeps half of one: flat against the
* edge, rounded inward. Built as the pill and then cut rather than drawn as a half, so that it
* stays right at both extremes -- half of a pill no longer than it is wide is a semicircle, with
* no special case.
*
* <p>Both sizes are in pixels rather than {@link Size}, a part that caps its pad against the room
* a variant leaves having already done the arithmetic.
*
* @param padWidth Pad size across the part
* @param padLength Pad size along the part, before the cut -- half of this is drawn
* @param reachesRight Whether the surviving half reaches to the right of the cut
*/
protected void drawBisectedPads(Graphics2D g2d, int startIndex, int count, double padWidth,
double padLength, boolean reachesRight, boolean outlineMode,
IDrawingObserver drawingObserver) {
if (outlineMode) {
return;
}
double w = padWidth;
double depth = padLength / 2.0;
double theta = orientation.toRadians();
drawingObserver.startTrackingContinuityArea(true);
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
if (theta != 0) {
g2d.rotate(theta, p.getX(), p.getY());
}
Area pad = new Area(new RoundRectangle2D.Double(p.getX() - depth, p.getY() - w / 2.0,
depth * 2, w, w, w));
pad.intersect(new Area(new Rectangle2D.Double(
reachesRight ? p.getX() : p.getX() - depth, p.getY() - w / 2.0, depth, w)));
g2d.setColor(PAD_COLOR);
g2d.fill(pad);
g2d.setColor(PAD_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(pad);
if (theta != 0) {
g2d.rotate(-theta, p.getX(), p.getY());
}
}
drawingObserver.stopTrackingContinuityArea();
}
/**
* Pad, drill-hole and edge-notch diameters used by {@link #drawCastellatedPads} and
* {@link #subtractCastellationNotches}. A board whose castellations differ from the package
@@ -618,12 +783,12 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to draw one column of castellated edge pads: a round through-hole pad joined to the
* board edge by a stub, with a semicircular notch bitten out of the edge itself. Pair it with
* Draws one column of castellated edge pads: a round through-hole pad joined to the board edge by
* a stub, with a semicircular notch bitten out of the edge. Pair it with
* {@link #subtractCastellationNotches} so the outline and the pads agree.
*
* <p>Unlike the control-point based pad helpers this one works in unrotated board coordinates,
* so it has to be called while the board rotation is still applied to {@code g2d}.
* <p>Unlike the control-point based pad helpers this works in unrotated board coordinates, so it
* has to be called while the board rotation is still applied to {@code g2d}.
*
* @param g2d Graphics2D context, already transformed for board orientation
* @param pinX Unrotated X coordinate of the pad column
@@ -662,7 +827,7 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(padArea);
// Inner through-hole drill hole (white circle matching Zero and perfboard)
// the drill hole
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(pinX - holeR, py - holeR, holeD, holeD));
g2d.setColor(padColor.darker());
@@ -672,8 +837,8 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to bite the semicircular castellation notches for one column of pads out of a board
* outline, matching what {@link #drawCastellatedPads} draws along the same edge.
* Bites the semicircular castellation notches for one column of pads out of a board outline,
* matching what {@link #drawCastellatedPads} draws along the same edge.
*/
protected void subtractCastellationNotches(Area boardArea, double pinY, int count, double spacing,
double edgeX) {
@@ -685,9 +850,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
}
/**
* Helper to get outline border color.
*/
protected Color getFinalBorderColor(ComponentState componentState, boolean outlineMode) {
if (outlineMode) {
Theme theme = Constants.DEFAULT_THEME;
@@ -744,4 +906,33 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
int margin = 50;
return new Rectangle2D.Double(minX - margin, minY - margin, (maxX - minX) + 2 * margin, (maxY - minY) + 2 * margin);
}
/**
* Which addressable package a part is fitted with. The three are pin-compatible and share the
* 5050 footprint, so this changes only what the LEDs can emit and what the BOM asks for.
*
* <p>RGBW adds a fourth, white die to the RGB three and is sold by the tint of that die; WWA
* replaces the colour dies altogether with warm white, white and amber.
*/
public enum LedType {
RGB("RGB"),
RGBW_WARM("RGBW Warm White"),
RGBW_NATURAL("RGBW Natural White"),
RGBW_COOL("RGBW Cool White"),
WWA("WWA");
private final String label;
LedType(String label) {
this.label = label;
}
@Override public String toString() { return label; }
/** True for the packages that carry a white die beside the colour ones. */
public boolean hasWhiteDie() { return this != RGB && this != WWA; }
/** The wheel a part fitted with this package is drawn lit with. */
public Color[] getGradient() { return this == WWA ? WWA_LED_GRADIENT : RGB_LED_GRADIENT; }
}
}
@@ -22,13 +22,16 @@
package org.diylc.components;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.List;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.VerticalAlignment;
@@ -48,9 +51,6 @@ public class MakerBoardPainter {
private MakerBoardPainter() {}
/**
* Helper to draw a mounting hole.
*/
public static void drawMountingHole(Graphics2D g2d, double cx, double cy, double diameter) {
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(cx - diameter / 2.0, cy - diameter / 2.0, diameter, diameter));
@@ -59,9 +59,6 @@ public class MakerBoardPainter {
g2d.draw(new Ellipse2D.Double(cx - diameter / 2.0, cy - diameter / 2.0, diameter, diameter));
}
/**
* Helper to draw an IC chip on the board.
*/
public static void drawChip(Graphics2D g2d, double x, double y, double w, double h, String label) {
g2d.setColor(AbstractMakerBoard.IC_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 4, 4));
@@ -80,9 +77,7 @@ public class MakerBoardPainter {
}
}
/**
* Helper to draw an FPC / ribbon cable connector (e.g. MIPI CSI/DSI, PCIe FPC).
*/
/** An FPC / ribbon cable connector, such as a MIPI CSI/DSI or PCIe socket. */
public static void drawFpcConnector(Graphics2D g2d, double x, double y, double w, double h, boolean vertical, String label) {
g2d.setColor(AbstractMakerBoard.IC_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 2, 2));
@@ -111,37 +106,23 @@ public class MakerBoardPainter {
}
}
/**
* Helper to draw a standard Micro-USB connector.
*/
public static void drawMicroUsb(Graphics2D g2d, double x, double y, double w, double h, String label) {
drawMetalConnector(g2d, x, y, w, h, label);
}
/**
* Helper to draw a standard USB Type-C connector.
*/
public static void drawUsbC(Graphics2D g2d, double x, double y, double w, double h, String label) {
drawMetalConnector(g2d, x, y, w, h, label);
}
/**
* Helper to draw a standard USB Type-A connector.
*/
public static void drawUsbA(Graphics2D g2d, double x, double y, double w, double h, String label) {
drawMetalConnector(g2d, x, y, w, h, label);
}
/**
* Helper to draw a standard USB Type-B connector.
*/
public static void drawUsbB(Graphics2D g2d, double x, double y, double w, double h, String label) {
drawMetalConnector(g2d, x, y, w, h, label);
}
/**
* Helper to draw a metal connector / shield (e.g. RF shield cans, SD card slots, HDMI).
*/
/** A metal shield or connector body: an RF can, an SD card slot, an HDMI socket. */
public static void drawMetalConnector(Graphics2D g2d, double x, double y, double w, double h, String label) {
g2d.setColor(AbstractMakerBoard.USB_METAL_COLOR);
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 3, 3));
@@ -156,20 +137,18 @@ public class MakerBoardPainter {
}
/**
* Helper to draw a standard ESP-style PCB meander antenna with a dark (#1E1E1E) substrate rectangle underneath.
* An ESP-style PCB meander antenna over a dark substrate rectangle.
*
* @param g2d Graphics2D context
* @param x top-left X coordinate of the antenna substrate rectangle
* @param y top-left Y coordinate of the antenna substrate rectangle
* @param width width of the antenna substrate rectangle (e.g. 15.0mm, matching main chip width)
* @param height height of the antenna substrate rectangle (e.g. 7.0mm)
* @param width width of the antenna substrate rectangle
* @param height height of the antenna substrate rectangle
*/
public static void drawPcbAntenna(Graphics2D g2d, double x, double y, double width, double height) {
// Dark rectangle underneath (#1E1E1E)
g2d.setColor(AbstractMakerBoard.ANTENNA_BG_COLOR);
g2d.fill(new Rectangle2D.Double(x, y, width, height));
// Serpentine antenna trace (gold/copper)
g2d.setColor(AbstractMakerBoard.ANTENNA_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
Path2D.Double antPath = new Path2D.Double();
@@ -212,15 +191,13 @@ public class MakerBoardPainter {
g2d.draw(antPath);
}
/**
* Helper to draw a standard ESP-style PCB meander antenna using default 15.0mm x 7.0mm dimensions.
*/
/** As above, at the default 15.0 x 7.0 mm. */
public static void drawPcbAntenna(Graphics2D g2d, double x, double y) {
drawPcbAntenna(g2d, x, y, AbstractMakerBoard.ANTENNA_WIDTH.convertToPixels(), AbstractMakerBoard.ANTENNA_LENGTH.convertToPixels());
}
/**
* Helper to draw a standard SMD tactile push button (housing + circular actuator).
* An SMD tactile push button: housing plus circular actuator.
*
* @param g2d Graphics2D context
* @param x Top-left X coordinate of the button body
@@ -239,4 +216,149 @@ public class MakerBoardPainter {
g2d.setColor(AbstractMakerBoard.BUTTON_ACTUATOR_COLOR);
g2d.fill(new Ellipse2D.Double(x + (w - actuatorD) / 2.0, y + (h - actuatorD) / 2.0, actuatorD, actuatorD));
}
/**
* An addressable RGB LED in a 5050 package: the white body, the milky phosphor lens over it and
* the driver die visible through the lens. Shared by every WS2812B part.
*
* @param g2d Graphics2D context
* @param cx centre X coordinate of the package
* @param cy centre Y coordinate of the package
* @param size edge length of the square package; lens and die scale with it
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size) {
drawAddressableLed(g2d, cx, cy, size, null);
}
/**
* Draws one 5050 addressable RGB package. A {@code litColor} shows the LED emitting that colour;
* {@code null} draws it dark, which is how an unpowered part looks sitting on a layout.
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
Color litColor) {
drawAddressableLed(g2d, cx, cy, size, litColor, 0);
}
/**
* As above, turned by {@code rotation} radians about its own centre, for parts laid out on a
* circle and mounted facing outwards. The package is square, so the turn only tells modulo a
* quarter turn -- still enough to distinguish a radial arrangement from an axis-aligned one.
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
Color litColor, double rotation) {
drawAddressableLed(g2d, cx, cy, size, litColor, rotation, AbstractMakerBoard.LedType.RGB);
}
/**
* As above, for a package other than the plain RGB one. An RGBW is told from an RGB on sight
* only by the white die beside the colour dies, so that is what is drawn; WWA has the same
* three-die layout and is told apart by what it emits, which is the caller's palette.
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
Color litColor, double rotation, AbstractMakerBoard.LedType ledType) {
if (rotation != 0) {
g2d.rotate(rotation, cx, cy);
}
double half = size / 2.0;
g2d.setColor(AbstractMakerBoard.RGB_LED_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(cx - half, cy - half, size, size, 2, 2));
g2d.setColor(AbstractMakerBoard.RGB_LED_BODY_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(0.5f));
g2d.draw(new RoundRectangle2D.Double(cx - half, cy - half, size, size, 2, 2));
double lensR = half * 0.7;
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_LENS_COLOR : litColor);
g2d.fill(new Ellipse2D.Double(cx - lensR, cy - lensR, lensR * 2, lensR * 2));
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_LENS_BORDER : litColor.darker());
g2d.draw(new Ellipse2D.Double(cx - lensR, cy - lensR, lensR * 2, lensR * 2));
double dieSize = Math.max(2.0, lensR * 0.4);
// the colour dies sit centred on their own, and shoulder aside to make room for a white one
double dieOffset = ledType != null && ledType.hasWhiteDie() ? dieSize * 0.7 : 0;
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_CHIP_COLOR : litColor.brighter());
g2d.fill(new Rectangle2D.Double(cx - dieOffset - dieSize / 2.0, cy - dieSize / 2.0, dieSize,
dieSize));
if (dieOffset > 0) {
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_CHIP_COLOR : Color.WHITE);
g2d.fill(new Rectangle2D.Double(cx + dieOffset - dieSize / 2.0, cy - dieSize / 2.0, dieSize,
dieSize));
}
// turned back rather than saving the transform, so nothing is allocated per LED
if (rotation != 0) {
g2d.rotate(-rotation, cx, cy);
}
}
/**
* Prints a display's own description on its lit area, wrapped and centred.
*
* <p>What it prints is what the component already answers for the BOM, so the glass cannot fall
* out of step with the part list. Nothing is drawn unless the whole block fits, so a panel too
* small for its description shows a bare screen rather than a crop -- the same rule the pin-name
* silkscreen follows.
*
* <p>The area is the part of the panel the text may occupy rather than the panel itself, so a
* round display hands over the square inscribed in its glass.
*/
public static void drawScreenText(Graphics2D g2d, Rectangle2D area, Color inkColor,
Display display, String name, String value) {
String text = screenText(display, name, value);
if (text == null) {
return;
}
g2d.setFont(AbstractMakerBoard.SILK_FONT);
FontMetrics metrics = g2d.getFontMetrics();
double margin = AbstractMakerBoard.SCREEN_TEXT_MARGIN.convertToPixels();
double maxWidth = area.getWidth() - 2 * margin;
double maxHeight = area.getHeight() - 2 * margin;
double lineHeight = metrics.getHeight();
List<String> lines = StringUtils.wrap(text, metrics, (int) maxWidth);
if (lines.isEmpty() || lines.size() * lineHeight > maxHeight) {
return;
}
// a single word wider than the area cannot be broken, so the width is checked after wrapping
// rather than trusted to it
for (String line : lines) {
if (metrics.stringWidth(line) > maxWidth) {
return;
}
}
g2d.setColor(inkColor);
double y = area.getCenterY() - (lines.size() - 1) * lineHeight / 2.0;
for (String line : lines) {
StringUtils.drawCenteredText(g2d, line, area.getCenterX(), y, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER);
y += lineHeight;
}
}
/**
* What each {@link Display} setting puts on the glass, or {@code null} for a screen that prints
* nothing. The variant string reads "size, interface", and breaking it at the comma gives one
* property per line, which keeps the longest line well inside the glass instead of spanning it.
*/
private static String screenText(Display display, String name, String value) {
boolean hasValue = value != null && !value.trim().isEmpty();
String variant = hasValue ? value.replace(", ", "\n") : null;
if (display == null) {
return variant;
}
switch (display) {
case NONE:
return null;
case NAME:
return name;
case BOTH:
return variant == null ? name : name + "\n" + variant;
default:
return variant;
}
}
}
@@ -32,6 +32,7 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
@@ -48,66 +49,54 @@ 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.presenter.CalcUtils;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "Character LCD (16x2 / 20x4)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "HD44780-Compatible Character LCD Display (Parallel / I2C Backpack)",
// instanceNamePrefix = "LCD", zOrder = IDIYComponent.BOARD,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "Character LCD", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "HD44780-Compatible Character LCD Display (Parallel / I2C Backpack)",
instanceNamePrefix = "LCD", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class CharacterLCD extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum LCDSize {
_16x2("16x2 (80x36mm)", 16, 2, 80.0, 36.0),
_20x4("20x4 (98x60mm)", 20, 4, 98.0, 60.0);
private final String label;
private final int cols;
private final int rows;
private final double widthMm;
private final double heightMm;
LCDSize(String label, int cols, int rows, double widthMm, double heightMm) {
this.label = label;
this.cols = cols;
this.rows = rows;
this.widthMm = widthMm;
this.heightMm = heightMm;
}
@Override public String toString() { return label; }
public int getCols() { return cols; }
public int getRows() { return rows; }
public double getWidthMm() { return widthMm; }
public double getHeightMm() { return heightMm; }
}
public enum LCDInterface {
I2C_Backpack("I2C Backpack (4-Pin)"),
Parallel_16Pin("Parallel HD44780 (16-Pin)");
private final String label;
LCDInterface(String label) { this.label = label; }
@Override public String toString() { return label; }
}
public static Size HEADER_OFFSET = new Size(2.54d, SizeUnit.mm);
// The parallel row is not centred: its leftmost pin starts this far in from the left edge.
public static Size PARALLEL_HEADER_INSET = new Size(10.0d, SizeUnit.mm);
// The I2C backpack's four pins stand in a column against the left edge, centred on the height.
public static Size I2C_HEADER_INSET = new Size(2.5d, SizeUnit.mm);
// A 3 mm hole centred 2.5 mm in leaves only 1 mm of board outside it, which is what the part
// does.
public static Size MOUNTING_HOLE_INSET = new Size(2.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(3.0d, SizeUnit.mm);
public static Color PCB_GREEN = Color.decode("#1B5E20");
public static Color SCREEN_BG = Color.decode("#1E88E5");
public static Color SCREEN_TEXT = Color.decode("#FFFFFF");
public static Color BEZEL_COLOR = Color.decode("#212121");
// Both inks are real: the blue-backlit module shows light characters and the yellow-green one
// dark, so the ink follows the screen colour.
public static Color SCREEN_INK_LIGHT = Color.decode("#F5F5F5");
public static Color SCREEN_INK_DARK = Color.decode("#1B2631");
public static final String[] PIN_NAMES_I2C = new String[] {"GND", "VCC", "SDA", "SCL"};
public static final String[] PIN_NAMES_PARALLEL = new String[] {
"VSS (GND)", "VDD (+5V)", "V0 (Contrast)", "RS", "RW", "E",
"VSS (GND)", "VCC (+5V)", "VEE (Contrast)", "RS", "RW", "E",
"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7",
"A (Backlight +)", "K (Backlight -)"
};
// The bare designations the module prints. The supply and backlight pins carry their function
// in the node name instead, there being no room for it along a 0.1" pitch.
public static final String[] SILK_NAMES_PARALLEL = new String[] {
"VSS", "VCC", "VEE", "RS", "RW", "E",
"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "A", "K"
};
private LCDSize lcdSize = LCDSize._16x2;
private LCDInterface lcdInterface = LCDInterface.I2C_Backpack;
private Color screenColor = SCREEN_BG;
private Display screen = Display.VALUE;
public CharacterLCD() {
super();
@@ -137,6 +126,26 @@ public class CharacterLCD extends AbstractMakerBoard {
invalidateCache();
}
@EditableProperty
public Display getScreen() {
return screen == null ? Display.VALUE : screen;
}
public void setScreen(Display screen) {
this.screen = screen;
invalidateCache();
}
@Override
protected String getVariantLabel() {
LCDSize lcdSize = getLcdSize();
if (lcdSize == null) {
return null;
}
LCDInterface lcdInterface = getLcdInterface();
return lcdInterface == null ? lcdSize.toString() : lcdSize + ", " + lcdInterface;
}
@EditableProperty(name = "Backlight Color")
public Color getScreenColor() {
return screenColor;
@@ -158,31 +167,76 @@ public class CharacterLCD extends AbstractMakerBoard {
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
int pinCount = (lcdInterface == LCDInterface.I2C_Backpack) ? 4 : 16;
double[][] relativeOffsets = new double[pinCount][2];
for (int i = 0; i < pinCount; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
protected String getSilkPinLabel(int index) {
if (lcdInterface == LCDInterface.Parallel_16Pin && index >= 0
&& index < SILK_NAMES_PARALLEL.length) {
return SILK_NAMES_PARALLEL[index];
}
return super.getSilkPinLabel(index);
}
rotatePoints(firstPoint, relativeOffsets);
private int getPinCount() {
return lcdInterface == LCDInterface.I2C_Backpack ? PIN_NAMES_I2C.length
: PIN_NAMES_PARALLEL.length;
}
/** The ink that contrasts with the backlight the user has chosen. */
private Color getScreenInk() {
return CalcUtils.calculateLuminance(screenColor) < 128d ? SCREEN_INK_LIGHT : SCREEN_INK_DARK;
}
private double getBoardWidth() {
return new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels();
}
private double getBoardLength() {
return new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels();
}
/**
* Neither header is centred: the parallel row starts a fixed distance in from the left, and the
* I2C column stands against that same edge.
*/
private double getBoardX(double x) {
Size inset =
lcdInterface == LCDInterface.I2C_Backpack ? I2C_HEADER_INSET : PARALLEL_HEADER_INSET;
return x - inset.convertToPixels();
}
/**
* The parallel row hangs below the top edge by a fixed clearance; the I2C column is centred on
* the board's height instead.
*/
private double getBoardY(double y) {
if (lcdInterface == LCDInterface.I2C_Backpack) {
double columnSpan = (getPinCount() - 1) * PIN_SPACING.convertToPixels();
return y - (getBoardLength() - columnSpan) / 2.0;
}
return y - HEADER_OFFSET.convertToPixels();
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
boolean vertical = lcdInterface == LCDInterface.I2C_Backpack;
double[][] relativeOffsets = new double[getPinCount()][2];
for (int i = 0; i < relativeOffsets.length; i++) {
relativeOffsets[i][0] = vertical ? 0 : i * spacing;
relativeOffsets[i][1] = vertical ? i * spacing : 0;
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardW = new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels();
double boardH = new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels();
double boardX = x - 60;
double boardY = y - 20;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 10, 10);
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), getBoardWidth(),
getBoardLength(), 10, 10);
}
@Override
@@ -201,11 +255,10 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels();
double boardH = new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels();
double boardX = x - 60;
double boardY = y - 20;
double boardW = getBoardWidth();
double boardH = getBoardLength();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
@@ -221,39 +274,45 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// 4 Mounting holes in corners
MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + 20, 20);
MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + boardH - 20, 20);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + 20, 20);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + boardH - 20, 20);
double holeInset = MOUNTING_HOLE_INSET.convertToPixels();
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - holeInset, holeSize);
// Metal Bezel around screen
double bezelMarginX = (lcdSize == LCDSize._16x2) ? 45 : 40;
double bezelW = boardW - 2 * bezelMarginX;
double bezelX = boardX + bezelMarginX;
double bezelY = boardY + 45;
double bezelH = boardH - 65;
// The bezel's size is the module's own, not a margin taken off the board, so it stays put
// when the board changes.
double bezelW = new Size(lcdSize.getBezelWidthMm(), SizeUnit.mm).convertToPixels();
double bezelH = new Size(lcdSize.getBezelHeightMm(), SizeUnit.mm).convertToPixels();
double bezelX = boardX + (boardW - bezelW) / 2.0;
double bezelY = boardY + (boardH - bezelH) / 2.0;
g2d.setColor(BEZEL_COLOR);
g2d.fill(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 6, 6));
// LCD Screen area
double screenMargin = 12;
double screenW = bezelW - 2 * screenMargin;
double screenH = bezelH - 2 * screenMargin;
double screenX = bezelX + screenMargin;
double screenY = bezelY + screenMargin;
double screenW = new Size(lcdSize.getDisplayWidthMm(), SizeUnit.mm).convertToPixels();
double screenH = new Size(lcdSize.getDisplayHeightMm(), SizeUnit.mm).convertToPixels();
double screenX = bezelX + (bezelW - screenW) / 2.0;
double screenY = bezelY + (bezelH - screenH) / 2.0;
g2d.setColor(screenColor);
g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
// Render character boxes
g2d.setColor(SCREEN_TEXT);
g2d.setFont(new Font("Monospaced", Font.BOLD, (lcdSize == LCDSize._16x2) ? 14 : 11));
String line1 = (lcdSize == LCDSize._16x2) ? "HELLO WORLD! 16x2" : "DIYLC LCD MODULE";
String line2 = (lcdSize == LCDSize._16x2) ? "DIY LAYOUT CREATOR" : "20x4 CHARACTER LCD";
StringUtils.drawCenteredText(g2d, line1, screenX + screenW / 2.0, screenY + screenH / 3.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, line2, screenX + screenW / 2.0, screenY + screenH * 2.0 / 3.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(screenX, screenY, screenW, screenH), getScreenInk(), getScreen(),
getName(), getValueForDisplay());
// Only the parallel row has anywhere to print. The backpack's column sits 2.5 mm from the
// left edge with the bezel 1.4 mm further in, so a label beside it would lie across the metal
// frame; the backpack prints its names on its own PCB behind the module, not drawn here.
if (lcdInterface == LCDInterface.Parallel_16Pin) {
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, controlPoints.length, true,
SILK_COLOR);
}
}
g2d.setTransform(oldTx);
@@ -270,7 +329,6 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.setColor(PCB_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3));
// Screen
g2d.setColor(SCREEN_BG);
g2d.fillRect(6, 10, width - 12, height - 20);
@@ -278,4 +336,48 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.setFont(new Font("SansSerif", Font.BOLD, 6));
StringUtils.drawCenteredText(g2d, "LCD", width / 2, height / 2 + 1, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum LCDSize {
// Labels reach the BOM's value column, so they stay short; the dimensions are carried by the
// fields beside them. The bezel and lit area are measured off the module rather than derived as
// margins off the board, which made the window grow with the board.
_16x2("16x2", 80.0, 35.0, 72.2, 24.1, 64.5, 14.5),
_20x4("20x4", 98.0, 60.0, 96.8, 39.3, 77.0, 25.2);
private final String label;
private final double widthMm;
private final double heightMm;
private final double bezelWidthMm;
private final double bezelHeightMm;
private final double displayWidthMm;
private final double displayHeightMm;
LCDSize(String label, double widthMm, double heightMm, double bezelWidthMm,
double bezelHeightMm, double displayWidthMm, double displayHeightMm) {
this.label = label;
this.widthMm = widthMm;
this.heightMm = heightMm;
this.bezelWidthMm = bezelWidthMm;
this.bezelHeightMm = bezelHeightMm;
this.displayWidthMm = displayWidthMm;
this.displayHeightMm = displayHeightMm;
}
@Override public String toString() { return label; }
public double getWidthMm() { return widthMm; }
public double getHeightMm() { return heightMm; }
public double getBezelWidthMm() { return bezelWidthMm; }
public double getBezelHeightMm() { return bezelHeightMm; }
public double getDisplayWidthMm() { return displayWidthMm; }
public double getDisplayHeightMm() { return displayHeightMm; }
}
public enum LCDInterface {
I2C_Backpack("I2C Backpack"),
Parallel_16Pin("Parallel HD44780");
private final String label;
LCDInterface(String label) { this.label = label; }
@Override public String toString() { return label; }
}
}
@@ -0,0 +1,589 @@
/*
*
* 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.Graphics2D;
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.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractLabeledComponent;
import org.diylc.components.transform.LEDBarGraphTransformer;
import org.diylc.core.ComponentState;
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.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;
/**
* A segmented LED bar in a dual-in-line package, as used for VU and level meters.
*
* <p>Not an {@code AbstractMakerBoard} like the rest of this category: it is a DIP-outline part,
* so it follows {@code DIL_IC} on {@link AbstractLabeledComponent} and carries its own
* transformer.
*
* <p>It differs from a DIP IC in the way that governs the whole geometry: an IC's leads bend out
* of the sides of the plastic, so its body sits <em>between</em> the pin rows, while a bar
* graph's pins leave the underside within the footprint and the package spans the rows. The pins
* are nonetheless drawn on top, because a pin that cannot be seen cannot be lined up against a
* board.
*
* @author Branislav Stojkovic
*/
@ComponentDescriptor(name = "LED Bar Graph", author = "Branislav Stojkovic",
category = "Displays & Outputs", instanceNamePrefix = "BAR",
description = "Segmented LED bar in a DIP package, for VU and level meters",
zOrder = IDIYComponent.COMPONENT, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_VALUE, transformer = LEDBarGraphTransformer.class,
enableCache = true)
public class LEDBarGraph extends AbstractLabeledComponent<String> {
private static final long serialVersionUID = 1L;
public static Color BODY_COLOR = Color.decode("#2B2B2B");
public static Color BORDER_COLOR = Color.decode("#1A1A1A");
public static Color PIN_COLOR = METAL_COLOR;
public static Color PIN_BORDER_COLOR = PIN_COLOR.darker();
// A bar graph is a scale, not a lamp: green most of the way up, yellow for the last two and
// red for the topmost, as on a level or VU meter.
public static Color SEGMENT_GREEN = Color.decode("#43A047");
public static Color SEGMENT_YELLOW = Color.decode("#FDD835");
public static Color SEGMENT_RED = Color.decode("#E53935");
public static Color SEGMENT_BLUE = Color.decode("#1E88E5");
public static Color SEGMENT_ORANGE = Color.decode("#FB8C00");
public static Color SEGMENT_WHITE = Color.decode("#EEEEEE");
public static int EDGE_RADIUS = 4;
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
// The package being wider than the row spacing is the whole difference from a DIP IC: 10.3
// against 7.62 leaves about 1.34 mm of body outboard of each row, so no pin shows from above.
public static Size ROW_SPACING = new Size(7.62d, SizeUnit.mm);
public static Size PACKAGE_WIDTH = new Size(10.3d, SizeUnit.mm);
// Sizes of their own rather than the package less a margin, so a segment stays the size it is
// when the package or the pin pitch changes.
public static Size SEGMENT_WIDTH = new Size(4.8d, SizeUnit.mm);
public static Size SEGMENT_LENGTH = new Size(1.7d, SizeUnit.mm);
private String value = "";
private Orientation orientation = Orientation.DEFAULT;
private Segments segments = Segments._10;
private BarColor barColor = BarColor.GREEN_YELLOW_RED;
private Color bodyColor = BODY_COLOR;
private Color borderColor = BORDER_COLOR;
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
transient private Area body;
public LEDBarGraph() {
super();
updateControlPoints();
}
/**
* Nothing draws this -- unlike {@code DIL_IC}, which renders it as the chip's part number -- so
* it reaches only the BOM and keyword search.
*/
@EditableProperty
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@EditableProperty
public Orientation getOrientation() {
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
body = null;
}
@EditableProperty(name = "Segments")
public Segments getSegments() {
if (segments == null) {
segments = Segments._10;
}
return segments;
}
/** Changing this changes the pin count, so the control points are rebuilt with it. */
public void setSegments(Segments segments) {
this.segments = segments;
updateControlPoints();
body = null;
}
@EditableProperty(name = "Color")
public BarColor getBarColor() {
if (barColor == null) {
barColor = BarColor.GREEN_YELLOW_RED;
}
return barColor;
}
public void setBarColor(BarColor barColor) {
this.barColor = barColor;
}
/**
* The colour of one segment, counted from the top of the scale. Delegates to the selected
* {@link BarColor}.
*/
private Color getSegmentColor(int index) {
return getBarColor().getSegmentColor(index, getSegments().getCount());
}
@EditableProperty(name = "Body")
public Color getBodyColor() {
if (bodyColor == null) {
bodyColor = BODY_COLOR;
}
return bodyColor;
}
public void setBodyColor(Color bodyColor) {
this.bodyColor = bodyColor;
}
@EditableProperty(name = "Border")
public Color getBorderColor() {
if (borderColor == null) {
borderColor = BORDER_COLOR;
}
return borderColor;
}
public void setBorderColor(Color borderColor) {
this.borderColor = borderColor;
}
/**
* Puts the segment count and colour in the BOM's value column, which otherwise groups every
* variant into one row: {@code BomMaker} groups on type name and value, and the inherited value
* is the part number and usually blank. {@code AbstractMakerBoard.getVariantLabel} does this for
* the boards in this category; the bar graph is on a different base class, so it is written out
* here. A part number the user has typed is kept and appended.
*/
@Override
public String getValueForDisplay() {
String variant = getSegments().toString();
if (getBarColor() != BarColor.GREEN_YELLOW_RED) {
variant += ", " + getBarColor();
}
String partNumber = getValue();
return partNumber == null || partNumber.trim().isEmpty() ? variant
: variant + ", " + partNumber.trim();
}
@Override
public int getControlPointCount() {
return controlPoints.length;
}
@Override
public Point2D getControlPoint(int index) {
return controlPoints[index];
}
@Override
public boolean isControlPointSticky(int index) {
return true;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.NEVER;
}
/**
* Mutates the existing point rather than replacing it, as {@code DIL_IC} does, since callers may
* hold the instance.
*/
@Override
public void setControlPoint(Point2D point, int index) {
controlPoints[index].setLocation(point);
body = null;
}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
}
/** Pin 1 is first on the near row and numbering runs round the package, as on any DIP outline. */
@Override
public String getControlPointNodeName(int index) {
int pinCount = getSegments().getPinCount();
int pin = index + 1;
if (pin > pinCount / 2) {
return Integer.toString(pinCount * 3 / 2 - pin + 1);
}
return Integer.toString(pin);
}
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
int pinCount = getSegments().getPinCount();
Point2D[] points = new Point2D[pinCount];
points[0] = firstPoint;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
for (int i = 0; i < pinCount / 2; i++) {
double dx1;
double dy1;
double dx2;
double dy2;
switch (orientation) {
case DEFAULT:
dx1 = 0;
dy1 = i * pinSpacing;
dx2 = rowSpacing;
dy2 = i * pinSpacing;
break;
case _90:
dx1 = -i * pinSpacing;
dy1 = 0;
dx2 = -i * pinSpacing;
dy2 = rowSpacing;
break;
case _180:
dx1 = 0;
dy1 = -i * pinSpacing;
dx2 = -rowSpacing;
dy2 = -i * pinSpacing;
break;
case _270:
dx1 = i * pinSpacing;
dy1 = 0;
dx2 = i * pinSpacing;
dy2 = -rowSpacing;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
points[i] = new Point2D.Double(Math.round(firstPoint.getX() + dx1),
Math.round(firstPoint.getY() + dy1));
points[i + pinCount / 2] = new Point2D.Double(Math.round(firstPoint.getX() + dx2),
Math.round(firstPoint.getY() + dy2));
}
controlPoints = points;
}
/** How far the plastic reaches past each pin row. */
private static double getSideOverhang() {
return (PACKAGE_WIDTH.convertToPixels() - ROW_SPACING.convertToPixels()) / 2.0;
}
/**
* The package outline. Unlike a DIP IC this spans the pin rows rather than sitting between them,
* and runs half a pin pitch past the end pins at each end, so the pins fall entirely underneath.
*/
public Area getBody() {
if (body == null) {
double x = controlPoints[0].getX();
double y = controlPoints[0].getY();
double width;
double height;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double packageWidth = PACKAGE_WIDTH.convertToPixels();
double overhang = getSideOverhang();
double runLength = getSegments().getCount() * pinSpacing;
switch (orientation) {
case DEFAULT:
width = packageWidth;
height = runLength;
x -= overhang;
y -= pinSpacing / 2;
break;
case _90:
width = runLength;
height = packageWidth;
x -= runLength - pinSpacing / 2;
y -= overhang;
break;
case _180:
width = packageWidth;
height = runLength;
x -= rowSpacing + overhang;
y -= runLength - pinSpacing / 2;
break;
case _270:
width = runLength;
height = packageWidth;
x -= pinSpacing / 2;
y -= rowSpacing + overhang;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
body = new Area(new RoundRectangle2D.Double(x, y, width, height, EDGE_RADIUS, EDGE_RADIUS));
}
return body;
}
/**
* Each segment is centred on the pin pair that drives it, as on the part. Taking the centre from
* the two control points rather than dividing the body also makes it orientation-proof: the
* midpoint of a pin pair is the package centreline at that segment whichever way it is turned.
*/
private Rectangle2D[] getSegmentRects() {
int count = getSegments().getCount();
double across = SEGMENT_WIDTH.convertToPixels();
double along = SEGMENT_LENGTH.convertToPixels();
boolean vertical = orientation == Orientation.DEFAULT || orientation == Orientation._180;
Rectangle2D[] rects = new Rectangle2D[count];
for (int i = 0; i < count; i++) {
Point2D near = controlPoints[i];
Point2D far = controlPoints[i + count];
double cx = (near.getX() + far.getX()) / 2.0;
double cy = (near.getY() + far.getY()) / 2.0;
if (vertical) {
rects[i] = new Rectangle2D.Double(cx - across / 2.0, cy - along / 2.0, across, along);
} else {
rects[i] = new Rectangle2D.Double(cx - along / 2.0, cy - across / 2.0, along, across);
}
}
return rects;
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Area mainArea = getBody();
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
Composite oldComposite = applyAlpha(g2d, componentState);
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getBodyColor());
drawingObserver.startTracking();
g2d.fill(mainArea);
drawingObserver.stopTracking();
g2d.setComposite(oldComposite);
Color finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: getBorderColor();
g2d.setColor(finalBorderColor);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(mainArea);
if (!outlineMode) {
// Drawn lit, as the Ring and the Strip are: an unlit bar graph is a black block with nothing
// to say what it is.
Rectangle2D[] segments = getSegmentRects();
for (int i = 0; i < segments.length; i++) {
g2d.setColor(getSegmentColor(i));
g2d.fill(segments[i]);
g2d.setColor(getSegmentColor(i).darker());
g2d.draw(segments[i]);
}
// Over the package and the segments: on the real part they are underneath and invisible from
// above, but a pin you cannot see is a pin you cannot line up. Round, because a bar graph's
// leads are drawn wire rather than an IC's stamped leadframe.
double pinSize = (int) PIN_SIZE.convertToPixels() / 2 * 2;
for (Point2D point : controlPoints) {
Ellipse2D pin = new Ellipse2D.Double(point.getX() - pinSize / 2,
point.getY() - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
drawingObserver.startTracking();
g2d.fill(pin);
drawingObserver.stopTracking();
g2d.setColor(PIN_BORDER_COLOR);
g2d.draw(pin);
}
}
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(BODY_COLOR);
g2d.fillRoundRect(width / 4, 2, width / 2, height - 4, 2, 2);
g2d.setColor(BORDER_COLOR);
g2d.drawRoundRect(width / 4, 2, width / 2, height - 4, 2, 2);
// compressed to five bands so the icon reads at toolbox size
for (int i = 0; i < 5; i++) {
g2d.setColor(i == 0 ? SEGMENT_RED : i == 1 ? SEGMENT_YELLOW : SEGMENT_GREEN);
g2d.fillRect(width / 4 + 2, 4 + i * (height - 8) / 5, width / 2 - 3, (height - 8) / 5 - 1);
}
}
@Override
public Rectangle2D getCachingBounds() {
double minX = Integer.MAX_VALUE;
double maxX = Integer.MIN_VALUE;
double minY = Integer.MAX_VALUE;
double maxY = Integer.MIN_VALUE;
int margin = 50;
for (int i = 0; i < getControlPointCount(); i++) {
Point2D p = getControlPoint(i);
if (p.getX() < minX) {
minX = p.getX();
}
if (p.getX() > maxX) {
maxX = p.getX();
}
if (p.getY() < minY) {
minY = p.getY();
}
if (p.getY() > maxY) {
maxY = p.getY();
}
}
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin,
maxY - minY + 2 * margin);
}
/**
* Decides the rest of the package: each segment has its own anode and cathode, so the pin count
* is twice the segment count and the body lengthens by one pin pitch per segment.
*/
public enum Segments {
_4("4 Segment", 4),
_5("5 Segment", 5),
_6("6 Segment", 6),
_8("8 Segment", 8),
_10("10 Segment", 10),
_12("12 Segment", 12);
private final String label;
private final int count;
Segments(String label, int count) {
this.label = label;
this.count = count;
}
@Override public String toString() { return label; }
public int getCount() { return count; }
/** Anode and cathode per segment, split evenly between the two rows. */
public int getPinCount() { return count * 2; }
}
public enum BarColor {
GREEN_YELLOW_RED("Green-Yellow-Red"),
BLUE_GREEN_YELLOW_RED("Blue-Green-Yellow-Red"),
RED("Red"),
GREEN("Green"),
YELLOW("Yellow"),
BLUE("Blue"),
ORANGE("Orange"),
WHITE("White");
private final String label;
BarColor(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
public Color getSegmentColor(int index, int totalSegments) {
switch (this) {
case RED:
return SEGMENT_RED;
case GREEN:
return SEGMENT_GREEN;
case YELLOW:
return SEGMENT_YELLOW;
case BLUE:
return SEGMENT_BLUE;
case ORANGE:
return SEGMENT_ORANGE;
case WHITE:
return SEGMENT_WHITE;
case BLUE_GREEN_YELLOW_RED:
int redCount;
int yellowCount;
int blueCount;
if (totalSegments >= 12) {
redCount = 2;
yellowCount = 3;
blueCount = 2;
} else if (totalSegments >= 10) {
redCount = 2;
yellowCount = 3;
blueCount = 1;
} else if (totalSegments >= 8) {
redCount = 1;
yellowCount = 2;
blueCount = 1;
} else {
redCount = 1;
yellowCount = 1;
blueCount = 1;
}
if (index < redCount) {
return SEGMENT_RED;
}
if (index < redCount + yellowCount) {
return SEGMENT_YELLOW;
}
if (index >= totalSegments - blueCount) {
return SEGMENT_BLUE;
}
return SEGMENT_GREEN;
case GREEN_YELLOW_RED:
default:
if (index == 0) {
return SEGMENT_RED;
}
if (index <= 2) {
return SEGMENT_YELLOW;
}
return SEGMENT_GREEN;
}
}
}
}
@@ -0,0 +1,374 @@
/*
*
* 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.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.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.MakerBoardPainter;
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 = "LED Matrix", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "MAX7219 Dot LED Matrix Display Module (Cascadable SPI)",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class LEDMatrix extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_BLUE = Color.decode("#0055A5");
public static Color MATRIX_BODY = Color.decode("#1A1A1A");
public static Color LED_RED = Color.decode("#E53935");
public static Color LED_OFF = Color.decode("#333333");
// The modules are flush with each other and with the board, so the dot pitch runs unbroken
// across the whole strip.
public static Size MATRIX_SIZE = new Size(32.0d, SizeUnit.mm);
// Clearance from a board edge to the pin row nearest it. Both headers keep the same distance
// from their own edge, so the spacing between the rows follows from the board size.
public static Size HEADER_OFFSET = new Size(2.54d, SizeUnit.mm);
public static Size CHIP_MARGIN_X = new Size(2.54d, SizeUnit.mm);
public static Size CHIP_LENGTH = new Size(5.7d, SizeUnit.mm);
public static Size CHIP_GAP = new Size(1.9d, SizeUnit.mm);
public static final String[] PIN_NAMES = new String[] {
"VCC_IN", "GND_IN", "DIN", "CS_IN", "CLK_IN",
"VCC_OUT", "GND_OUT", "DOUT", "CS_OUT", "CLK_OUT"
};
private Modules modules = Modules.Single_8x8;
private Color dotColor = LED_RED;
public LEDMatrix() {
super();
this.bodyColor = PCB_BLUE;
updateControlPoints();
}
@EditableProperty(name = "Modules")
public Modules getModules() {
if (modules == null) {
modules = Modules.Single_8x8;
}
return modules;
}
public void setModules(Modules modules) {
this.modules = modules;
updateControlPoints();
invalidateCache();
}
@Override
protected String getVariantLabel() {
Modules modules = getModules();
return modules == null ? null : modules.toString();
}
@EditableProperty(name = "Dot Color")
public Color getDotColor() {
return dotColor;
}
public void setDotColor(Color dotColor) {
this.dotColor = dotColor;
invalidateCache();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return "Pin " + (index + 1);
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
// The rows lie across the original single's height and stand on end on the boards that bring
// their headers out on the short edges.
boolean vertical = getModules().hasVerticalHeaders();
double far = vertical ? getHeaderSpacing() : -getHeaderSpacing();
for (int i = 0; i < 5; i++) {
relativeOffsets[i][0] = vertical ? 0 : i * spacing;
relativeOffsets[i][1] = vertical ? i * spacing : 0;
relativeOffsets[5 + i][0] = vertical ? far : i * spacing;
relativeOffsets[5 + i][1] = vertical ? i * spacing : far;
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
private double getBoardWidth() {
return new Size(getModules().getBoardWidthMm(), SizeUnit.mm).convertToPixels();
}
private double getBoardLength() {
return new Size(getModules().getBoardHeightMm(), SizeUnit.mm).convertToPixels();
}
/** The span a five-pin row occupies. */
private static double getPinRunLength() {
return 4 * PIN_SPACING.convertToPixels();
}
/**
* The board along the axis the rows face each other across, less the clearance each keeps from
* its own edge.
*/
private double getHeaderSpacing() {
double span = getModules().hasVerticalHeaders() ? getBoardWidth() : getBoardLength();
return span - 2 * HEADER_OFFSET.convertToPixels();
}
/**
* The original single centres its horizontal row across the width; a board with vertical
* headers stands its row against the left edge, one clearance in.
*/
private double getBoardX(double x) {
if (getModules().hasVerticalHeaders()) {
return x - HEADER_OFFSET.convertToPixels();
}
return x - (getBoardWidth() - getPinRunLength()) / 2.0;
}
/**
* Control point 0 is an input pin: on the single it sits one clearance up from the bottom edge,
* with vertical headers it is the top pin of a column centred on the height.
*/
private double getBoardY(double y) {
if (getModules().hasVerticalHeaders()) {
return y - (getBoardLength() - getPinRunLength()) / 2.0;
}
return y - getBoardLength() + HEADER_OFFSET.convertToPixels();
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), getBoardWidth(),
getBoardLength(), 8, 8);
}
/** The demo pattern, in the coordinates of a single 8x8 module. */
private static boolean isDotLit(int row, int col) {
return (row == 0 && (col == 2 || col == 5))
|| (row == 1 && (col == 1 || col == 3 || col == 4 || col == 6))
|| (row == 2 && col >= 1 && col <= 6)
|| (row == 3 && col >= 1 && col <= 6)
|| (row == 4 && col >= 2 && col <= 5)
|| (row == 5 && col >= 3 && col <= 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 = getBoardWidth();
double boardH = getBoardLength();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
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);
// Any header the modules cover is drawn first, so that they cover it. These are drawn and
// painted over rather than skipped, because drawPinHeader is what registers the pins as a
// conductive area and leaving a call out would quietly drop five pins from continuity. The
// header works in unrotated control-point coordinates, so the board rotation comes off for it.
boolean coversBothHeaders = getModules().hasVerticalHeaders();
g2d.setTransform(oldTx);
drawPinHeader(g2d, 5, 5, outlineMode, drawingObserver);
if (coversBothHeaders) {
drawPinHeader(g2d, 0, 5, outlineMode, drawingObserver);
}
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
if (!outlineMode) {
Modules modules = getModules();
double matrixSize = MATRIX_SIZE.convertToPixels();
double matrixX = boardX + (boardW - modules.getCount() * matrixSize) / 2.0;
double matrixY = boardY;
// the board's corners are rounded and the modules are not, so the outline clips them
Shape oldClip = g2d.getClip();
g2d.clip(boardShape);
g2d.setColor(MATRIX_BODY);
g2d.fill(new Rectangle2D.Double(matrixX, matrixY, modules.getCount() * matrixSize,
matrixSize));
// One unbroken grid rather than a tiled one, and centred across the full width: a cascade
// shows one image, not the same image once per module.
double dotPitch = matrixSize / 8.0;
double dotR = dotPitch * 0.7;
int cols = 8 * modules.getCount();
int patternOffset = (cols - 8) / 2;
for (int row = 0; row < 8; row++) {
for (int col = 0; col < cols; col++) {
double dx = matrixX + col * dotPitch + (dotPitch - dotR) / 2.0;
double dy = matrixY + row * dotPitch + (dotPitch - dotR) / 2.0;
g2d.setColor(isDotLit(row, col - patternOffset) ? dotColor : LED_OFF);
g2d.fill(new Ellipse2D.Double(dx, dy, dotR, dotR));
}
}
g2d.setClip(oldClip);
// Only the tall single exposes its driver; hiding it is what makes the others tileable.
if (modules == Modules.Single_8x8) {
double chipMarginX = CHIP_MARGIN_X.convertToPixels();
MakerBoardPainter.drawChip(g2d, boardX + chipMarginX,
matrixY + matrixSize + CHIP_GAP.convertToPixels(), boardW - 2 * chipMarginX,
CHIP_LENGTH.convertToPixels(), "MAX7219");
}
// No IN/OUT silkscreen: with the modules flush there is no strip left for the OUT label, and
// labelling one of a matched pair reads worse than labelling neither.
//
// The single does have bare board for its pin names. They go above its input row rather than
// below, where a label would overrun the 2.54 mm to the board edge. Every other row has a
// module over it, so those pins are named by their tooltips alone.
if (modules == Modules.Single_8x8) {
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, 5, false, SILK_COLOR);
}
}
g2d.setTransform(oldTx);
// Where the modules cover both rows the input row went down with the output row before them.
if (!coversBothHeaders) {
drawPinHeader(g2d, 0, 5, outlineMode, drawingObserver);
}
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(PCB_BLUE);
g2d.fill(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3));
g2d.setColor(PCB_BLUE.darker());
g2d.draw(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3));
g2d.setColor(MATRIX_BODY);
g2d.fillRect(6, 4, width - 12, height - 12);
g2d.setColor(LED_RED);
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
g2d.fillOval(9 + c * 4, 7 + r * 4, 2, 2);
}
}
}
/**
* Which board the modules sit on, which is not the same question as how many there are: two of
* these carry one module each. The original single puts its driver and both headers on a strip
* below the module; the compact one fits the driver underneath, leaving a board that is only
* the module and can therefore be butted to others, which the 4-in-1 and 8-in-1 are.
*
* <p>The cascaded boards carry no dimensions of their own: a strip of flush 32 mm modules is
* 32 mm tall and as wide as it has modules.
*/
public enum Modules {
Single_8x8("Single 8x8 (32x50mm)", 1, 32.0d, 50.0d, false),
Compact_8x8("Compact 8x8 (32x32mm)", 1, 32.0d, 32.0d, true),
FourInOne_32x8("4-in-1 32x8 (128x32mm)", 4, 128.0d, 32.0d, true),
EightInOne_64x8("8-in-1 64x8 (256x32mm)", 8, 256.0d, 32.0d, true);
private final String label;
private final int count;
private final double boardWidthMm;
private final double boardHeightMm;
private final boolean verticalHeaders;
Modules(String label, int count, double boardWidthMm, double boardHeightMm,
boolean verticalHeaders) {
this.label = label;
this.count = count;
this.boardWidthMm = boardWidthMm;
this.boardHeightMm = boardHeightMm;
this.verticalHeaders = verticalHeaders;
}
@Override public String toString() { return label; }
public int getCount() { return count; }
public double getBoardWidthMm() { return boardWidthMm; }
public double getBoardHeightMm() { return boardHeightMm; }
/** True where the headers leave by the short edges, so their rows run down the board. */
public boolean hasVerticalHeaders() { return verticalHeaders; }
}
}
@@ -1,235 +0,0 @@
/*
*
* 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.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.components.MakerBoardPainter;
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 = "8x8 LED Matrix (MAX7219)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "MAX7219 Dot LED Matrix Display Module (Cascadable SPI)",
// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
public class LEDMatrix8x8 extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_BLUE = Color.decode("#0055A5");
public static Color MATRIX_BODY = Color.decode("#1A1A1A");
public static Color LED_RED = Color.decode("#E53935");
public static Color LED_OFF = Color.decode("#333333");
public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(50.0d, SizeUnit.mm);
public static final String[] PIN_NAMES = new String[] {
// Input Header (0..4)
"VCC_IN", "GND_IN", "DIN", "CS_IN", "CLK_IN",
// Output Header (5..9)
"VCC_OUT", "GND_OUT", "DOUT", "CS_OUT", "CLK_OUT"
};
private Color dotColor = LED_RED;
public LEDMatrix8x8() {
super();
this.bodyColor = PCB_BLUE;
updateControlPoints();
}
@EditableProperty(name = "Dot Color")
public Color getDotColor() {
return dotColor;
}
public void setDotColor(Color dotColor) {
this.dotColor = dotColor;
invalidateCache();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return "Pin " + (index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double[][] relativeOffsets = new double[10][2];
// Input header (0..4) at bottom
for (int i = 0; i < 5; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
// Output header (5..9) at top
double topY = -new Size(44.0d, SizeUnit.mm).convertToPixels();
for (int i = 0; i < 5; i++) {
relativeOffsets[5 + i][0] = i * spacing;
relativeOffsets[5 + i][1] = topY;
}
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 - 4 * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - boardH + 20;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8);
}
@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 - 4 * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - boardH + 20;
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) {
// 8x8 LED Matrix block (square 32mm)
double matrixSize = boardW - 10;
double matrixX = boardX + 5;
double matrixY = boardY + 30;
g2d.setColor(MATRIX_BODY);
g2d.fill(new RoundRectangle2D.Double(matrixX, matrixY, matrixSize, matrixSize, 6, 6));
// Draw 64 circular LED dots
double dotPitch = matrixSize / 8.0;
double dotR = dotPitch * 0.7;
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
double dx = matrixX + col * dotPitch + (dotPitch - dotR) / 2.0;
double dy = matrixY + row * dotPitch + (dotPitch - dotR) / 2.0;
// create a demo heart or smile pattern
boolean on = (row == 0 && (col == 2 || col == 5)) ||
(row == 1 && (col == 1 || col == 3 || col == 4 || col == 6)) ||
(row == 2 && col >= 1 && col <= 6) ||
(row == 3 && col >= 1 && col <= 6) ||
(row == 4 && col >= 2 && col <= 5) ||
(row == 5 && col >= 3 && col <= 4);
g2d.setColor(on ? dotColor : LED_OFF);
g2d.fill(new Ellipse2D.Double(dx, dy, dotR, dotR));
}
}
// MAX7219 IC
MakerBoardPainter.drawChip(g2d, boardX + 20, matrixY + matrixSize + 15, boardW - 40, 45, "MAX7219");
// Silkscreen
g2d.setColor(Color.WHITE);
g2d.setFont(SILK_FONT_SMALL);
StringUtils.drawCenteredText(g2d, "IN", x + 2 * PIN_SPACING.convertToPixels(), y - 16, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "OUT", x + 2 * PIN_SPACING.convertToPixels(), y - boardH + 34, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
g2d.setTransform(oldTx);
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(PCB_BLUE);
g2d.fill(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3));
g2d.setColor(PCB_BLUE.darker());
g2d.draw(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3));
// Matrix block
g2d.setColor(MATRIX_BODY);
g2d.fillRect(6, 4, width - 12, height - 12);
// 4x4 sample dots
g2d.setColor(LED_RED);
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
g2d.fillOval(9 + c * 4, 7 + r * 4, 2, 2);
}
}
}
}
@@ -0,0 +1,291 @@
/*
*
* 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.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
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.components.MakerBoardPainter;
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.presenter.CalcUtils;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Nokia 5110 LCD", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "84x48 graphic LCD module with a PCD8544 controller and an SPI interface",
instanceNamePrefix = "LCD", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class Nokia5110LCD extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_BLUE = Color.decode("#15548C");
public static Color BEZEL_COLOR = LIGHT_METAL_COLOR;
public static Color BEZEL_BORDER_COLOR = LIGHT_METAL_COLOR.darker();
public static Color LCD_COLOR = Color.decode("#6B7668");
public static Color LCD_INK_LIGHT = Color.decode("#F5F5F5");
public static Color LCD_INK_DARK = Color.decode("#2B2B2B");
public static Size BOARD_WIDTH = new Size(43.8d, SizeUnit.mm);
public static Size BOARD_LENGTH = new Size(45.8d, SizeUnit.mm);
// The metal frame holding the glass, centred across the board and measured down from the top
// edge; the pin row takes the strip left at the bottom.
public static Size BEZEL_WIDTH = new Size(40.0d, SizeUnit.mm);
public static Size BEZEL_LENGTH = new Size(35.0d, SizeUnit.mm);
public static Size BEZEL_TOP_OFFSET = new Size(4.5d, SizeUnit.mm);
// The glass sits lower in the frame than centred, so it is placed from the board's top edge.
public static Size DISPLAY_WIDTH = new Size(37.0d, SizeUnit.mm);
public static Size DISPLAY_LENGTH = new Size(27.0d, SizeUnit.mm);
public static Size DISPLAY_TOP_OFFSET = new Size(11.0d, SizeUnit.mm);
// Centre-to-centre, which is how the pattern is specified and what a builder drills.
public static Size MOUNTING_HOLE_SPACING_X = new Size(34.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING_Y = new Size(41.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(3.0d, SizeUnit.mm);
public static Size HEADER_OFFSET = new Size(3.5d, SizeUnit.mm);
public static Size CORNER_RADIUS = new Size(1.5d, SizeUnit.mm);
public static Size SILK_TEXT_GAP = new Size(1.0d, SizeUnit.mm);
public static final String[] PIN_NAMES =
new String[] {"RST", "CE", "DC", "DIN", "CLK", "VCC", "BL", "GND"};
private Display screen = Display.VALUE;
private Color screenColor = LCD_COLOR;
public Nokia5110LCD() {
super();
this.bodyColor = PCB_BLUE;
updateControlPoints();
}
@EditableProperty
public Display getScreen() {
return screen == null ? Display.VALUE : screen;
}
public void setScreen(Display screen) {
this.screen = screen;
invalidateCache();
}
/**
* The same module is sold with a blue, a white and a green backlight and nothing else about it
* changes, so it is a colour rather than a variant.
*/
@EditableProperty(name = "Backlight Color")
public Color getScreenColor() {
return screenColor == null ? LCD_COLOR : screenColor;
}
public void setScreenColor(Color screenColor) {
this.screenColor = screenColor;
invalidateCache();
}
/** The ink that contrasts with the backlight the user has chosen. */
private Color getScreenInk() {
return CalcUtils.calculateLuminance(getScreenColor()) < 128d ? LCD_INK_LIGHT : LCD_INK_DARK;
}
/** There is only one of these modules, so this is for the BOM's value column and the glass. */
@Override
protected String getVariantLabel() {
return "84x48 PCD8544";
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return "Pin " + (index + 1);
}
/** Left edge of the board, derived so that the pin row sits centred on the bottom edge. */
private double getBoardX(double x) {
double span = (PIN_NAMES.length - 1) * PIN_SPACING.convertToPixels();
return x - (BOARD_WIDTH.convertToPixels() - span) / 2.0;
}
/** A whole board above the pin row rather than below it. */
private double getBoardY(double y) {
return y - BOARD_LENGTH.convertToPixels() + HEADER_OFFSET.convertToPixels();
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < relativeOffsets.length; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double radius = CORNER_RADIUS.convertToPixels();
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
BOARD_WIDTH.convertToPixels(), BOARD_LENGTH.convertToPixels(), radius, radius);
}
@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_LENGTH.convertToPixels();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
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) {
double holeInsetX = (boardW - MOUNTING_HOLE_SPACING_X.convertToPixels()) / 2.0;
double holeInsetY = (boardH - MOUNTING_HOLE_SPACING_Y.convertToPixels()) / 2.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInsetX, boardY + holeInsetY, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInsetX, boardY + holeInsetY,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInsetX, boardY + boardH - holeInsetY,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInsetX,
boardY + boardH - holeInsetY, holeSize);
// clear of the top left hole and level with it
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT_LARGE);
StringUtils.drawCenteredText(g2d, "Nokia 5110",
boardX + holeInsetX + holeSize / 2.0 + SILK_TEXT_GAP.convertToPixels(),
boardY + holeInsetY, HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
double bezelW = BEZEL_WIDTH.convertToPixels();
double bezelH = BEZEL_LENGTH.convertToPixels();
double bezelX = boardX + (boardW - bezelW) / 2.0;
double bezelY = boardY + BEZEL_TOP_OFFSET.convertToPixels();
g2d.setColor(BEZEL_COLOR);
g2d.fill(new Rectangle2D.Double(bezelX, bezelY, bezelW, bezelH));
g2d.setColor(BEZEL_BORDER_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new Rectangle2D.Double(bezelX, bezelY, bezelW, bezelH));
double displayW = DISPLAY_WIDTH.convertToPixels();
double displayH = DISPLAY_LENGTH.convertToPixels();
double displayX = boardX + (boardW - displayW) / 2.0;
double displayY = boardY + DISPLAY_TOP_OFFSET.convertToPixels();
double displayRadius = CORNER_RADIUS.convertToPixels();
g2d.setColor(getScreenColor());
g2d.fill(new RoundRectangle2D.Double(displayX, displayY, displayW, displayH, displayRadius,
displayRadius));
g2d.setColor(getScreenColor().darker());
g2d.draw(new RoundRectangle2D.Double(displayX, displayY, displayW, displayH, displayRadius,
displayRadius));
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(displayX, displayY, displayW, displayH), getScreenInk(),
getScreen(), getName(), getValueForDisplay());
// Names lie flat along the row: the strip is 3.5 mm deep and a label stood on end needs
// 4.7 mm.
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, PIN_NAMES.length, true,
SILK_COLOR);
}
g2d.setTransform(oldTx);
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(PCB_BLUE);
g2d.fill(new RoundRectangle2D.Double(2, 1, width - 4, height - 5, 3, 3));
g2d.setColor(PCB_BLUE.darker());
g2d.draw(new RoundRectangle2D.Double(2, 1, width - 4, height - 5, 3, 3));
g2d.setColor(BEZEL_COLOR);
g2d.fillRect(4, 3, width - 8, height - 10);
g2d.setColor(LCD_COLOR);
g2d.fill(new RoundRectangle2D.Double(5, 7, width - 10, height - 15, 2, 2));
g2d.setColor(PIN_COLOR);
for (int i = 0; i < 4; i++) {
g2d.fillRect(width / 2 - 5 + i * 3, height - 4, 2, 2);
}
}
}
@@ -24,6 +24,7 @@ package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
@@ -32,6 +33,7 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
@@ -50,35 +52,51 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "0.96\" OLED Display (SSD1306)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "0.96\" Monochrome 128x64 OLED Display Module (I2C / SPI)",
// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "OLED Display", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Monochrome OLED Display Module (SSD1306 / SH1106, I2C or SPI)",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class OLEDDisplay extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum OLEDInterface {
I2C_4Pin("I2C (4-Pin: GND, VCC, SCL, SDA)"),
SPI_7Pin("SPI (7-Pin: GND, VCC, D0, D1, RES, DC, CS)");
private final String label;
OLEDInterface(String label) { this.label = label; }
@Override public String toString() { return label; }
}
public static Color OLED_BLUE = Color.decode("#004488");
public static Color GLASS_COLOR = Color.decode("#0D1B2A");
public static Color GLASS_BORDER_COLOR = Color.decode("#334E68");
public static Color ACTIVE_AREA_COLOR = Color.decode("#060D15");
public static Color PIXEL_BLUE = Color.decode("#00D4FF");
public static Color PIXEL_YELLOW = Color.decode("#FFD700");
public static Size BOARD_SIZE = new Size(27.0d, SizeUnit.mm);
// Clearance a name keeps from the edge of its pad. Tighter than the offset
// drawFlatRowPinLabels uses, which does not fit the 2.27 mm strip on the 0.96".
public static Size LABEL_GAP = new Size(0.5d, SizeUnit.mm);
public static final String[] PIN_NAMES_I2C = new String[] {"GND", "VCC", "SCL", "SDA"};
public static final String[] PIN_NAMES_SPI = new String[] {"GND", "VCC", "D0 (CLK)", "D1 (MOSI)", "RES", "DC", "CS"};
/**
* Clock and data keep their I2C names in both interfaces, since the same two pads carry them.
* SPI-labelled boards print SCK or CLK and MOSI, and the SSD1306 datasheet D0 and D1.
*/
public static final String[] PIN_NAMES_SPI =
new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS"};
/** Chip select tied low on the board instead of brought out, so the bus cannot be shared. */
public static final String[] PIN_NAMES_SPI_NO_CS =
new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC"};
/**
* VIN (4.8 - 5.2 V) and VDD (2.8 - 3.3 V logic) are separate rails and must not be tied
* together. The row follows the controller's pin numbering, which runs the reverse of the silk
* on the other boards.
*/
public static final String[] PIN_NAMES_SPI_SPLIT_SUPPLY =
new String[] {"SDA", "SCL", "DC", "RES", "CS", "VDD", "VIN", "GND"};
private Version version = Version.SSD1306_0_96;
private OLEDInterface oledInterface = OLEDInterface.I2C_4Pin;
private Display screen = Display.VALUE;
private Color pixelColor = PIXEL_BLUE;
public OLEDDisplay() {
super();
@@ -86,9 +104,20 @@ public class OLEDDisplay extends AbstractMakerBoard {
updateControlPoints();
}
@EditableProperty(name = "Size")
public Version getVersion() {
return version == null ? Version.SSD1306_0_96 : version;
}
public void setVersion(Version version) {
this.version = version;
updateControlPoints();
invalidateCache();
}
@EditableProperty(name = "Interface")
public OLEDInterface getOledInterface() {
return oledInterface;
return oledInterface == null ? OLEDInterface.I2C_4Pin : oledInterface;
}
public void setOledInterface(OLEDInterface oledInterface) {
@@ -97,42 +126,155 @@ public class OLEDDisplay extends AbstractMakerBoard {
invalidateCache();
}
@EditableProperty
public Display getScreen() {
return screen == null ? Display.VALUE : screen;
}
public void setScreen(Display screen) {
this.screen = screen;
invalidateCache();
}
@EditableProperty(name = "Pixel Color")
public Color getPixelColor() {
return pixelColor == null ? PIXEL_BLUE : pixelColor;
}
public void setPixelColor(Color pixelColor) {
this.pixelColor = pixelColor;
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion() + ", " + getOledInterface();
}
/**
* The 0.91" is sold as two different boards, one with a column on its left edge and one with a
* row on top, so outline, header edge, holes and panel position follow from size and interface
* together rather than from the size alone.
*/
private Layout getLayout() {
return getVersion().getLayout(getOledInterface());
}
@Override
public String getControlPointNodeName(int index) {
if (oledInterface == OLEDInterface.I2C_4Pin) {
if (index >= 0 && index < PIN_NAMES_I2C.length) return PIN_NAMES_I2C[index];
} else {
if (index >= 0 && index < PIN_NAMES_SPI.length) return PIN_NAMES_SPI[index];
String[] pinNames = getOledInterface().getPinNames();
if (index >= 0 && index < pinNames.length) {
return pinNames[index];
}
return "Pin " + (index + 1);
}
private int getPinCount() {
return getOledInterface().getPinNames().length;
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
/** A top-edge row is centred across the board; a left-edge column stands its clearance in. */
private double getBoardX(double x) {
Layout layout = getLayout();
if (layout.isHeaderOnLeftEdge()) {
return x - px(layout.getHeaderOffsetMm());
}
double span = (getPinCount() - 1) * PIN_SPACING.convertToPixels();
return x - (px(layout.getBoardWidthMm()) - span) / 2.0;
}
/** A top-edge row hangs below the edge by its clearance; a left-edge column is centred on the
* board's height. */
private double getBoardY(double y) {
Layout layout = getLayout();
if (layout.isHeaderOnLeftEdge()) {
double columnSpan = (getPinCount() - 1) * PIN_SPACING.convertToPixels();
return y - (px(layout.getBoardLengthMm()) - columnSpan) / 2.0;
}
return y - px(layout.getHeaderOffsetMm());
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
boolean vertical = getLayout().isHeaderOnLeftEdge();
double[][] relativeOffsets = new double[getPinCount()][2];
for (int i = 0; i < relativeOffsets.length; i++) {
relativeOffsets[i][0] = vertical ? 0 : i * spacing;
relativeOffsets[i][1] = vertical ? i * spacing : 0;
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7;
double[][] relativeOffsets = new double[count][2];
for (int i = 0; i < count; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
rotatePoints(firstPoint, relativeOffsets);
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardSizePx = BOARD_SIZE.convertToPixels();
int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7;
double boardX = x - (boardSizePx - (count - 1) * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - 14;
return new RoundRectangle2D.Double(boardX, boardY, boardSizePx, boardSizePx, 8, 8);
Layout layout = getLayout();
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
px(layout.getBoardWidthMm()), px(layout.getBoardLengthMm()), 8, 8);
}
/**
* Pin names lying flat in the strip between the header and the glass. On the 0.91" I2C board the
* header is a column, so its names go to the right of the pins rather than below them.
*/
private void drawPinNames(Graphics2D g2d, double boardX, double boardY) {
Layout layout = getLayout();
double[][] offsets = getRelativeOffsets();
Point2D p0 = controlPoints[0];
double gap = LABEL_GAP.convertToPixels();
// drawPinHeader fills a square one pixel wider than PIN_SIZE either side of the pin, which is
// the edge a name has to clear rather than the pin itself.
double padHalf = Math.round(PIN_SIZE.convertToPixels()) + 1;
g2d.setFont(PIN_ROW_FLAT_FONT);
g2d.setColor(SILK_COLOR);
FontMetrics metrics = g2d.getFontMetrics();
// One reference glyph keeps a row of names on a single line. Every name here is upper case
// with no descender, so centring on the cap height lands them all alike.
double capHeight = g2d.getFont().createGlyphVector(g2d.getFontRenderContext(), "X")
.getVisualBounds().getHeight();
boolean column = layout.isHeaderOnLeftEdge();
double glassEdge = column ? boardX + px(layout.getPanelLeftMm())
: boardY + px(layout.getPanelTopMm());
for (int i = 0; i < offsets.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty()) {
continue;
}
double pinX = p0.getX() + offsets[i][0];
double pinY = p0.getY() + offsets[i][1];
double room = glassEdge - (column ? pinX : pinY) - padHalf;
double extent = column ? metrics.stringWidth(label) : capHeight;
if (extent > room) {
continue;
}
// Centred in the strip where it is too shallow for the full gap, which the 0.96" is: 1.13 mm
// between pad and glass to print 0.73 mm of lettering in.
double offset = padHalf + Math.min(gap, (room - extent) / 2.0);
if (column) {
StringUtils.drawCenteredText(g2d, label, pinX + offset, pinY, HorizontalAlignment.LEFT,
VerticalAlignment.CENTER);
} else {
StringUtils.drawCenteredText(g2d, label, pinX, pinY + offset + capHeight / 2.0,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
}
@Override
@@ -151,10 +293,11 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardSizePx = BOARD_SIZE.convertToPixels();
int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7;
double boardX = x - (boardSizePx - (count - 1) * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - 14;
Layout layout = getLayout();
double boardW = px(layout.getBoardWidthMm());
double boardH = px(layout.getBoardLengthMm());
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
@@ -170,31 +313,44 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// 4 Corner mounting holes
MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 16);
MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardSizePx - 16, 16);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - 16, boardY + 16, 16);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - 16, boardY + boardSizePx - 16, 16);
if (layout.hasMountingHoles()) {
double holeInset = px(layout.getHoleInsetMm());
double holeSize = px(layout.getHoleSizeMm());
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - holeInset, holeSize);
}
// Glass OLED Panel
double glassMarginX = 18;
double glassW = boardSizePx - 2 * glassMarginX;
double glassH = boardSizePx - 65;
double glassX = boardX + glassMarginX;
double glassY = boardY + 45;
Version version = getVersion();
double glassW = px(version.getPanelWidthMm());
double glassH = px(version.getPanelLengthMm());
double glassX = boardX + px(layout.getPanelLeftMm());
double glassY = boardY + px(layout.getPanelTopMm());
g2d.setColor(GLASS_COLOR);
g2d.fill(new RoundRectangle2D.Double(glassX, glassY, glassW, glassH, 4, 4));
g2d.setColor(Color.decode("#334E68"));
g2d.setColor(GLASS_BORDER_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(glassX, glassY, glassW, glassH, 4, 4));
// Display demo graphics (yellow top banner + blue body)
g2d.setColor(PIXEL_YELLOW);
g2d.setFont(SILK_FONT_SMALL);
StringUtils.drawCenteredText(g2d, "SSD1306 128x64", glassX + glassW / 2.0, glassY + 15, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// The driver's bonding region takes one end of the glass and its bottom, so the lit area is
// offset within the panel and sits high in it. Its top is measured from the board's edge.
double activeW = px(version.getActiveWidthMm());
double activeH = px(version.getActiveLengthMm());
double activeX = glassX + px(version.getActiveLeftInPanelMm());
double activeY = boardY + px(layout.getActiveTopMm());
g2d.setColor(ACTIVE_AREA_COLOR);
g2d.fill(new RoundRectangle2D.Double(activeX, activeY, activeW, activeH, 2, 2));
g2d.setColor(PIXEL_BLUE);
StringUtils.drawCenteredText(g2d, "OLED DISPLAY", glassX + glassW / 2.0, glassY + glassH / 2.0 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(activeX, activeY, activeW, activeH), getPixelColor(), getScreen(),
getName(), getValueForDisplay());
drawPinNames(g2d, boardX, boardY);
}
g2d.setTransform(oldTx);
@@ -211,7 +367,6 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.setColor(OLED_BLUE.darker());
g2d.draw(new RoundRectangle2D.Double(4, 4, width - 8, height - 8, 3, 3));
// Screen
g2d.setColor(GLASS_COLOR);
g2d.fillRect(7, 12, width - 14, height - 18);
@@ -219,10 +374,131 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.setFont(new Font("SansSerif", Font.BOLD, 5));
StringUtils.drawCenteredText(g2d, "OLED", width / 2, height / 2 + 3, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Pins
g2d.setColor(PIN_COLOR);
for (int i = 0; i < 4; i++) {
g2d.fillRect(10 + i * 3, 5, 2, 2);
}
}
public enum OLEDInterface {
// Labels reach the BOM's value column and are printed on the lit area, which the 0.91" has
// little of, so they stay short. Each SPI label states its pin count, the only thing that
// tells the three apart.
I2C_4Pin("I2C", PIN_NAMES_I2C),
SPI_6Pin("SPI-6", PIN_NAMES_SPI_NO_CS),
SPI_7Pin("SPI-7", PIN_NAMES_SPI),
SPI_8Pin("SPI-8", PIN_NAMES_SPI_SPLIT_SUPPLY);
private final String label;
private final String[] pinNames;
OLEDInterface(String label, String[] pinNames) {
this.label = label;
this.pinNames = pinNames;
}
@Override public String toString() { return label; }
public String[] getPinNames() { return pinNames; }
}
public enum Version {
// Panel and lit-area sizes belong to the display part and are shared by both of a size's
// boards. Everything belonging to a PCB is on the Layout, including where the lit area sits
// down the board, which the two 0.91" boards disagree on.
SSD1306_0_91("0.91\" SSD1306 128x32", 30.0d, 12.0d, 22.384d, 5.584d, 2.1d,
new Layout(35.8d, 12.0d, 1.5d, true, 0d, 0d, 5.0d, 0d, 2.35d),
new Layout(32.5d, 20.5d, 1.5d, false, 2.5d, 2.0d, 1.25d, 4.0d, 6.6d)),
SSD1306_0_96("0.96\" SSD1306 128x64", 26.7d, 19.26d, 21.74d, 11.0d, 2.48d,
new Layout(27.0d, 27.0d, 1.6d, false, 2.0d, 2.0d, 0.15d, 3.87d, 6.14d)),
SH1106_1_3("1.3\" SH1106 128x64", 35.4d, 23.0d, 29.42d, 14.7d, 2.99d,
new Layout(35.4d, 33.5d, 1.5d, false, 2.5d, 3.0d, 0d, 5.25d, 7.35d));
private final String label;
private final double panelWidthMm;
private final double panelLengthMm;
private final double activeWidthMm;
private final double activeLengthMm;
private final double activeLeftInPanelMm;
private final Layout i2cLayout;
private final Layout spiLayout;
/** For a size sold on one board, which both headers are built onto. */
Version(String label, double panelWidthMm, double panelLengthMm, double activeWidthMm,
double activeLengthMm, double activeLeftInPanelMm, Layout layout) {
this(label, panelWidthMm, panelLengthMm, activeWidthMm, activeLengthMm, activeLeftInPanelMm,
layout, layout);
}
Version(String label, double panelWidthMm, double panelLengthMm, double activeWidthMm,
double activeLengthMm, double activeLeftInPanelMm, Layout i2cLayout, Layout spiLayout) {
this.label = label;
this.panelWidthMm = panelWidthMm;
this.panelLengthMm = panelLengthMm;
this.activeWidthMm = activeWidthMm;
this.activeLengthMm = activeLengthMm;
this.activeLeftInPanelMm = activeLeftInPanelMm;
this.i2cLayout = i2cLayout;
this.spiLayout = spiLayout;
}
@Override public String toString() { return label; }
public double getPanelWidthMm() { return panelWidthMm; }
public double getPanelLengthMm() { return panelLengthMm; }
public double getActiveWidthMm() { return activeWidthMm; }
public double getActiveLengthMm() { return activeLengthMm; }
public double getActiveLeftInPanelMm() { return activeLeftInPanelMm; }
/** The 6-pin and 8-pin modules borrow the 7-pin board; the header is centred either way. */
public Layout getLayout(OLEDInterface oledInterface) {
return oledInterface == OLEDInterface.I2C_4Pin ? i2cLayout : spiLayout;
}
}
/**
* The PCB a size is sold on in one interface. Hole positions are an inset from each of the two
* edges nearest the hole.
*/
public static class Layout {
private final double boardWidthMm;
private final double boardLengthMm;
private final double headerOffsetMm;
private final boolean headerOnLeftEdge;
private final double holeInsetMm;
private final double holeSizeMm;
private final double panelLeftMm;
private final double panelTopMm;
private final double activeTopMm;
Layout(double boardWidthMm, double boardLengthMm, double headerOffsetMm,
boolean headerOnLeftEdge, double holeInsetMm, double holeSizeMm, double panelLeftMm,
double panelTopMm, double activeTopMm) {
this.boardWidthMm = boardWidthMm;
this.boardLengthMm = boardLengthMm;
this.headerOffsetMm = headerOffsetMm;
this.headerOnLeftEdge = headerOnLeftEdge;
this.holeInsetMm = holeInsetMm;
this.holeSizeMm = holeSizeMm;
this.panelLeftMm = panelLeftMm;
this.panelTopMm = panelTopMm;
this.activeTopMm = activeTopMm;
}
public double getBoardWidthMm() { return boardWidthMm; }
public double getBoardLengthMm() { return boardLengthMm; }
public double getHoleInsetMm() { return holeInsetMm; }
public double getHoleSizeMm() { return holeSizeMm; }
public double getPanelLeftMm() { return panelLeftMm; }
public double getPanelTopMm() { return panelTopMm; }
/** Measured from the board's top edge rather than from the glass. */
public double getActiveTopMm() { return activeTopMm; }
/** Clearance from the edge the header sits on, whichever edge that is. */
public double getHeaderOffsetMm() { return headerOffsetMm; }
public boolean isHeaderOnLeftEdge() { return headerOnLeftEdge; }
public boolean hasMountingHoles() { return holeSizeMm > 0; }
}
}
@@ -51,25 +51,15 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "7-Segment Display", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "7-Segment LED Display (1-Digit, 4-Digit, or TM1637 I2C Driver Module)",
// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "7-Segment Display", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "7-Segment LED Display (1-Digit, 2-Digit, 4-Digit, or TM1637 I2C Driver Module)",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class SevenSegmentDisplay extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum DisplayType {
SingleDigit_10Pin("1-Digit 0.56\" (10-Pin DIP)"),
FourDigit_12Pin("4-Digit Bare (12-Pin DIP)"),
TM1637_Module_4Pin("4-Digit TM1637 Module (4-Pin)");
private final String label;
DisplayType(String label) { this.label = label; }
@Override public String toString() { return label; }
}
public static Color BODY_BLACK = Color.decode("#1C1C1C");
public static Color FACE_BLACK = Color.decode("#111111");
public static Color FACE_BORDER = Color.decode("#333333");
@@ -77,26 +67,50 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
public static Color LED_OFF = Color.decode("#2E2E2E");
public static Color LED_OFF_BORDER = Color.decode("#222222");
// Exact physical proportions from datasheet
public static Size DIGIT_HEIGHT = new Size(14.2d, SizeUnit.mm); // 0.56"
public static Size DIGIT_WIDTH = new Size(8.1d, SizeUnit.mm);
public static Size DIGIT_PITCH = new Size(12.7d, SizeUnit.mm);
public static Size SEGMENT_THICKNESS = new Size(1.4d, SizeUnit.mm);
public static Size SEGMENT_GAP = new Size(0.35d, SizeUnit.mm);
public static double SLANT_DEGREES = 8.0d;
// How far a module's header column sits in from the right edge. The DIP packages straddle their
// pins with a row either side; a module carries its header down one edge instead.
public static Size HEADER_EDGE_OFFSET = new Size(2.5d, SizeUnit.mm);
/**
* The digit is drawn at this fraction of the package's measured size. On the real part the
* digit window runs almost to the pin rows, so a faithful drawing has the pins sitting on the
* digit; this is a deliberate departure for legibility, which is why it is a separate factor
* rather than smaller digit sizes on {@link DisplayType}. It is shared, so changing it to chase
* an exact drawn size moves every package.
*/
public static double DIGIT_DRAW_SCALE = 0.88d;
/**
* Pin arrays are in DIP order: index 0 is pin 1 at the bottom left, counting right along the
* bottom row and then back right-to-left along the top, which is how {@code updateControlPoints}
* places the control points. The two commons are the same net on the part and are numbered only
* to keep the node names distinct.
*/
public static final String[] PIN_NAMES_1DIGIT = new String[] {
"e", "d", "COM1", "c", "DP", "b", "a", "COM2", "f", "g"
"E", "D", "COM1", "C", "DP", "B", "A", "COM2", "F", "G"
};
/** The two-digit member of the same family as the 5641AS, with two digit commons instead of
* four. */
public static final String[] PIN_NAMES_2DIGIT = new String[] {
"E", "D", "DP", "C", "G", "B", "D2", "D1", "F", "A"
};
/** Shared by both bare four-digit packages, which differ in size but not in pin function. */
public static final String[] PIN_NAMES_4DIGIT = new String[] {
"D1", "a", "f", "D2", "D3", "b", "e", "d", "DP", "c", "g", "D4"
"E", "D", "DP", "C", "G", "D4", "B", "D3", "D2", "F", "A", "D1"
};
public static final String[] PIN_NAMES_TM1637 = new String[] {"CLK", "DIO", "VCC", "GND"};
/** The blue of a bare PCB, matching the board under the OLED module. */
public static Color PCB_BLUE = Color.decode("#004488");
private DisplayType displayType = DisplayType.SingleDigit_10Pin;
private Common common = Common.Cathode;
private Punctuation punctuation = Punctuation.DecimalPoints;
private Color ledColor = LED_RED;
private Color boardColor = PCB_BLUE;
public SevenSegmentDisplay() {
super();
@@ -115,6 +129,54 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
invalidateCache();
}
/**
* No drawing path reads this: the two kinds share a pinout, a package and a set of dimensions,
* so it exists to split them in the BOM. Defaulted in the getter so that a project saved before
* the property existed still loads.
*/
@EditableProperty(name = "Common")
public Common getCommon() {
if (common == null) {
common = Common.Cathode;
}
return common;
}
public void setCommon(Common common) {
this.common = common;
}
/**
* Decides whether the decimal points and the colon are drawn. The colon is meaningless on the
* single-digit package and is ignored there.
*/
@EditableProperty(name = "Punctuation")
public Punctuation getPunctuation() {
if (punctuation == null) {
punctuation = Punctuation.DecimalPoints;
}
return punctuation;
}
public void setPunctuation(Punctuation punctuation) {
this.punctuation = punctuation;
invalidateCache();
}
@Override
protected String getVariantLabel() {
DisplayType displayType = getDisplayType();
if (displayType == null) {
return null;
}
// A module drives the digits itself and brings out no common pins, so polarity is not part of
// what you order -- but which punctuation it carries still is.
if (displayType.isModule()) {
return displayType + ", " + getPunctuation();
}
return displayType + ", " + getCommon() + ", " + getPunctuation();
}
@EditableProperty(name = "LED Color")
public Color getLedColor() {
return ledColor;
@@ -125,89 +187,113 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
invalidateCache();
}
/** Only the TM1637 has one; the bare packages are a moulding with pins in it. */
@EditableProperty(name = "Board Color")
public Color getBoardColor() {
return boardColor;
}
public void setBoardColor(Color boardColor) {
this.boardColor = boardColor;
invalidateCache();
}
/**
* The display device itself: the whole part on a bare package, and the module mounted on the
* board on a TM1637. Relabelled from the inherited "Board Color", which fits neither.
*/
@EditableProperty(name = "Body")
@Override
public Color getBodyColor() {
return super.getBodyColor();
}
private String[] getPinNames() {
return displayType.getPinNames();
}
@Override
public String getControlPointNodeName(int index) {
switch (displayType) {
case SingleDigit_10Pin:
if (index >= 0 && index < PIN_NAMES_1DIGIT.length) return PIN_NAMES_1DIGIT[index];
break;
case FourDigit_12Pin:
if (index >= 0 && index < PIN_NAMES_4DIGIT.length) return PIN_NAMES_4DIGIT[index];
break;
case TM1637_Module_4Pin:
if (index >= 0 && index < PIN_NAMES_TM1637.length) return PIN_NAMES_TM1637[index];
break;
String[] names = getPinNames();
if (index >= 0 && index < names.length) {
return names[index];
}
return "Pin " + (index + 1);
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
/**
* The colon sits in the gap between the two middle digits, which exists only on an even count.
*/
private boolean hasMiddlePair() {
int digitCount = displayType.getDigitCount();
return digitCount >= 2 && digitCount % 2 == 0;
}
/**
* Draws the package's digits on their own pitch, centred on {@code rowCentreX}. Bare packages
* and modules differ only in the figures they pass, which is what lets a two-, six- or
* eight-digit part be a constant rather than a branch.
*/
private void drawDigitRow(Graphics2D g2d, double rowCentreX, double dy, double dw, double dh,
double pitch, double nudge, boolean decimalPoints) {
int digitCount = displayType.getDigitCount();
double firstCentreX = rowCentreX - (digitCount - 1) * pitch / 2.0;
for (int d = 0; d < digitCount; d++) {
double dx = firstCentreX + d * pitch - dw / 2.0 - nudge;
drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor, decimalPoints);
}
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
if (displayType == DisplayType.SingleDigit_10Pin) {
double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); // 120px
double[][] relativeOffsets = new double[10][2];
for (int i = 0; i < 5; i++) {
int pinCount = getPinNames().length;
double[][] relativeOffsets = new double[pinCount][2];
if (displayType.isDualRow()) {
int perRow = pinCount / 2;
double rowSpacing =
new Size(displayType.getRowSpacingMm(), SizeUnit.mm).convertToPixels();
for (int i = 0; i < perRow; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
relativeOffsets[perRow + i][0] = (perRow - 1 - i) * spacing;
relativeOffsets[perRow + i][1] = -rowSpacing;
}
for (int i = 0; i < 5; i++) {
relativeOffsets[5 + i][0] = (4 - i) * spacing;
relativeOffsets[5 + i][1] = -rowSpacing;
}
rotatePoints(firstPoint, relativeOffsets);
} else if (displayType == DisplayType.FourDigit_12Pin) {
double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels();
double[][] relativeOffsets = new double[12][2];
for (int i = 0; i < 6; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
for (int i = 0; i < 6; i++) {
relativeOffsets[6 + i][0] = (5 - i) * spacing;
relativeOffsets[6 + i][1] = -rowSpacing;
}
rotatePoints(firstPoint, relativeOffsets);
} else {
// TM1637 Module 4-pin
double[][] relativeOffsets = new double[4][2];
for (int i = 0; i < 4; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
for (int i = 0; i < pinCount; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
}
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardW;
double boardH;
double spacing = PIN_SPACING.convertToPixels();
double boardW = new Size(displayType.getBodyWidthMm(), SizeUnit.mm).convertToPixels();
double boardH = new Size(displayType.getBodyLengthMm(), SizeUnit.mm).convertToPixels();
int pinCount = getPinNames().length;
double boardX;
double boardY;
if (displayType == DisplayType.SingleDigit_10Pin) {
boardW = new Size(12.6d, SizeUnit.mm).convertToPixels();
boardH = new Size(19.0d, SizeUnit.mm).convertToPixels();
double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels();
boardX = x - (boardW - 4 * PIN_SPACING.convertToPixels()) / 2.0;
boardY = y - rowSpacing - (boardH - rowSpacing) / 2.0;
} else if (displayType == DisplayType.FourDigit_12Pin) {
boardW = new Size(50.3d, SizeUnit.mm).convertToPixels();
boardH = new Size(19.0d, SizeUnit.mm).convertToPixels();
double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels();
boardX = x - (boardW - 5 * PIN_SPACING.convertToPixels()) / 2.0;
boardY = y - rowSpacing - (boardH - rowSpacing) / 2.0;
if (displayType.isDualRow()) {
double rowSpacing =
new Size(displayType.getRowSpacingMm(), SizeUnit.mm).convertToPixels();
boardX = p0.getX() - (boardW - (pinCount / 2 - 1) * spacing) / 2.0;
boardY = p0.getY() - rowSpacing - (boardH - rowSpacing) / 2.0;
} else {
boardW = new Size(42.0d, SizeUnit.mm).convertToPixels();
boardH = new Size(24.0d, SizeUnit.mm).convertToPixels();
boardX = x - (boardW - 3 * PIN_SPACING.convertToPixels()) / 2.0;
boardY = y - (boardH - PIN_SPACING.convertToPixels()) / 2.0;
boardX = p0.getX() - boardW + HEADER_EDGE_OFFSET.convertToPixels();
boardY = p0.getY() - (boardH - (pinCount - 1) * spacing) / 2.0;
}
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6);
@@ -238,8 +324,10 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
Composite oldComposite = applyAlpha(g2d, componentState);
Color outlineFill = displayType.isModule() ? boardColor : bodyColor;
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : outlineFill);
g2d.fill(boardShape);
drawingObserver.stopTracking();
@@ -248,89 +336,78 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
if (displayType == DisplayType.SingleDigit_10Pin) {
// Dark display face
g2d.setColor(FACE_BLACK);
if (!displayType.isModule()) {
// the recessed window is a shade of the moulding, so it follows the body colour
g2d.setColor(bodyColor.darker());
g2d.fill(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3));
g2d.setColor(FACE_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3));
// Single 0.56" 7-segment digit with exact datasheet dimensions
double dw = DIGIT_WIDTH.convertToPixels();
double dh = DIGIT_HEIGHT.convertToPixels();
double dx = boardX + (boardW - dw) / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels();
// The pitch is unscaled while the digits are drawn slightly under size, so they keep their
// true centres and only the gaps widen. The row is centred on the body rather than measured
// from its left edge, so it stays put across packages of different widths. A single-digit
// package is the same construction with a count of one.
double dw = px(displayType.getDigitWidthMm()) * DIGIT_DRAW_SCALE;
double dh = px(displayType.getDigitHeightMm()) * DIGIT_DRAW_SCALE;
double pitch = px(displayType.getDigitPitchMm());
double dy = boardY + (boardH - dh) / 2.0;
drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor);
double rowCentreX = boardX + boardW / 2.0;
} else if (displayType == DisplayType.FourDigit_12Pin) {
// Dark display face
g2d.setColor(FACE_BLACK);
g2d.fill(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3));
g2d.setColor(FACE_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3));
Punctuation punctuation = getPunctuation();
drawDigitRow(g2d, rowCentreX, dy, dw, dh, pitch, 0.5 * SEGMENT_THICKNESS.convertToPixels(),
punctuation.hasDecimalPoints());
// 4 digits across with 12.70mm center-to-center pitch
double dw = DIGIT_WIDTH.convertToPixels();
double dh = DIGIT_HEIGHT.convertToPixels();
double pitch = DIGIT_PITCH.convertToPixels();
double firstCenterX = boardX + new Size(6.1d, SizeUnit.mm).convertToPixels();
double dy = boardY + (boardH - dh) / 2.0;
for (int d = 0; d < 4; d++) {
double centerX = firstCenterX + d * pitch;
double dx = centerX - dw / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels();
drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor);
if (punctuation.hasColon() && hasMiddlePair()) {
// Colon sized off the glyph grid like the decimal point, so it tracks the digit rather
// than staying a fixed few pixels across. The row is centred, so the gap between the two
// middle digits is the body's centre line.
double dotD = dw / GLYPH_WIDTH * 2.0;
double colonX = rowCentreX - dotD / 2.0;
g2d.setColor(ledColor);
g2d.fill(new Ellipse2D.Double(colonX, dy + dh * 0.35 - dotD / 2.0, dotD, dotD));
g2d.fill(new Ellipse2D.Double(colonX, dy + dh * 0.65 - dotD / 2.0, dotD, dotD));
}
// Center colon dots between digits 1 & 2
double colonX = boardX + boardW / 2.0;
double colonY1 = dy + dh * 0.35;
double colonY2 = dy + dh * 0.65;
g2d.setColor(ledColor);
g2d.fill(new Ellipse2D.Double(colonX - 2.0, colonY1 - 2.0, 4.0, 4.0));
g2d.fill(new Ellipse2D.Double(colonX - 2.0, colonY2 - 2.0, 4.0, 4.0));
} else {
// TM1637 4-Digit Display Module
MakerBoardPainter.drawMountingHole(g2d, boardX + 14, boardY + 14, 12);
MakerBoardPainter.drawMountingHole(g2d, boardX + 14, boardY + boardH - 14, 12);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 14, boardY + 14, 12);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 14, boardY + boardH - 14, 12);
Module module = displayType.getModule();
double holeInset = px(module.getHoleInsetMm());
double holeSize = px(module.getHoleSizeMm());
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - holeInset, holeSize);
// Display Bezel (Center)
double bezelW = new Size(30.0d, SizeUnit.mm).convertToPixels();
double bezelH = new Size(14.0d, SizeUnit.mm).convertToPixels();
double bezelX = boardX + (boardW - bezelW) / 2.0 - 6.0;
double bezelW = px(module.getBezelWidthMm());
double bezelH = px(module.getBezelLengthMm());
double bezelX = boardX + (boardW - bezelW) / 2.0;
double bezelY = boardY + (boardH - bezelH) / 2.0;
g2d.setColor(FACE_BLACK);
g2d.setColor(bodyColor);
g2d.fill(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 4, 4));
g2d.setColor(FACE_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 4, 4));
// 4 Digits inside 0.36" bezel
double dw = new Size(5.5d, SizeUnit.mm).convertToPixels();
double dh = new Size(9.2d, SizeUnit.mm).convertToPixels();
double pitch = new Size(7.62d, SizeUnit.mm).convertToPixels();
double firstCenterX = bezelX + (bezelW - 3 * pitch) / 2.0;
// at full size here: the module's digits sit on a bezel rather than between pin rows
double dw = px(displayType.getDigitWidthMm());
double dh = px(displayType.getDigitHeightMm());
double pitch = px(displayType.getDigitPitchMm());
double dy = bezelY + (bezelH - dh) / 2.0;
for (int d = 0; d < 4; d++) {
double centerX = firstCenterX + d * pitch;
double dx = centerX - dw / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels() * 0.65;
drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor);
Punctuation punctuation = getPunctuation();
drawDigitRow(g2d, bezelX + bezelW / 2.0, dy, dw, dh, pitch,
0.5 * SEGMENT_THICKNESS.convertToPixels() * 0.65, punctuation.hasDecimalPoints());
if (punctuation.hasColon() && hasMiddlePair()) {
double dotD = dw / GLYPH_WIDTH * 2.0;
double colonX = bezelX + bezelW / 2.0 - dotD / 2.0;
g2d.setColor(ledColor);
g2d.fill(new Ellipse2D.Double(colonX, dy + dh * 0.35 - dotD / 2.0, dotD, dotD));
g2d.fill(new Ellipse2D.Double(colonX, dy + dh * 0.65 - dotD / 2.0, dotD, dotD));
}
// Center colon dots
double colonX = bezelX + bezelW / 2.0;
double colonY1 = bezelY + bezelH / 2.0 - 8.0;
double colonY2 = bezelY + bezelH / 2.0 + 6.0;
g2d.setColor(ledColor);
g2d.fill(new Ellipse2D.Double(colonX - 1.5, colonY1 - 1.5, 3.0, 3.0));
g2d.fill(new Ellipse2D.Double(colonX - 1.5, colonY2 - 1.5, 3.0, 3.0));
}
}
@@ -375,126 +452,88 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
}
/**
* Draws an authentic 7-segment digit matching exact engineering drawing specifications:
* - 8.0° italic forward slant
* - Realistic polygon segment geometry (trapezoids/hexagons with clean junction bevels)
* - 1.40mm segment thickness, 0.35mm inter-segment gaps without overlap
* - Unlit segments rendered in dim silhouette and active segments lit in onColor
* - 1.40mm Decimal point circle at bottom-right
* The seven segments as a unit-grid glyph: a 10 x 18 box with segments two units thick, mitred
* at 45 degrees. Neighbours meet at a shared vertex rather than being held apart by a gap, so
* the separation between them comes from outlining each segment in the background colour. Index
* order is A through G, matching the SEG_* bits.
*/
private void drawSevenSegmentDigit(Graphics2D g2d, double x, double y, double w, double h, String charToDisplay, Color onColor) {
private static final double[][][] SEGMENT_POINTS = new double[][][] {
{{1, 1}, {2, 0}, {8, 0}, {9, 1}, {8, 2}, {2, 2}}, // A, top
{{9, 1}, {10, 2}, {10, 8}, {9, 9}, {8, 8}, {8, 2}}, // B, upper right
{{9, 9}, {10, 10}, {10, 16}, {9, 17}, {8, 16}, {8, 10}}, // C, lower right
{{9, 17}, {8, 18}, {2, 18}, {1, 17}, {2, 16}, {8, 16}}, // D, bottom
{{1, 17}, {0, 16}, {0, 10}, {1, 9}, {2, 10}, {2, 16}}, // E, lower left
{{1, 9}, {0, 8}, {0, 2}, {1, 1}, {2, 2}, {2, 8}}, // F, upper left
{{1, 9}, {2, 8}, {8, 8}, {9, 9}, {8, 10}, {2, 10}} // G, middle
};
/** Width of the glyph grid above, in its own units. */
private static final double GLYPH_WIDTH = 10.0d;
/** Height of the glyph grid above, in its own units. */
private static final double GLYPH_HEIGHT = 18.0d;
/**
* Draws one upright digit into the box at {@code x, y, w, h} from {@link #SEGMENT_POINTS}.
* Unlit segments are drawn first as a dim silhouette, then the lit ones, then every segment is
* outlined in the face colour so that neighbours sharing a vertex still read as separate.
*/
private void drawSevenSegmentDigit(Graphics2D g2d, double x, double y, double w, double h,
String charToDisplay, Color onColor, boolean showDecimalPoint) {
AffineTransform orig = g2d.getTransform();
// Exact 8.0° forward italic slant from engineering drawing
g2d.translate(x + w / 2.0, y + h / 2.0);
g2d.shear(-Math.tan(Math.toRadians(SLANT_DEGREES)), 0);
g2d.translate(-w / 2.0, -h / 2.0);
g2d.translate(x, y);
double t = Math.max(3.0, SEGMENT_THICKNESS.convertToPixels() * (h / DIGIT_HEIGHT.convertToPixels()));
double g = Math.max(1.0, SEGMENT_GAP.convertToPixels() * (h / DIGIT_HEIGHT.convertToPixels()));
double segW = w - t * 0.8;
double sx = w / GLYPH_WIDTH;
double sy = h / GLYPH_HEIGHT;
double x0 = 0;
double x1 = segW;
double y0 = 0;
double ym = h / 2.0;
double y1 = h;
Shape[] segments = new Shape[SEGMENT_POINTS.length];
for (int i = 0; i < SEGMENT_POINTS.length; i++) {
double[][] points = SEGMENT_POINTS[i];
Path2D.Double path = new Path2D.Double();
path.moveTo(points[0][0] * sx, points[0][1] * sy);
for (int p = 1; p < points.length; p++) {
path.lineTo(points[p][0] * sx, points[p][1] * sy);
}
path.closePath();
segments[i] = path;
}
// Segment A (Top horizontal)
Path2D pathA = new Path2D.Double();
pathA.moveTo(x0 + t * 0.6 + g, y0);
pathA.lineTo(x1 - t * 0.6 - g, y0);
pathA.lineTo(x1 - g, y0 + t * 0.45);
pathA.lineTo(x1 - t - g, y0 + t);
pathA.lineTo(x0 + t + g, y0 + t);
pathA.lineTo(x0 + g, y0 + t * 0.45);
pathA.closePath();
// the decimal point is not part of the glyph grid, so it is placed in the same units: one
// segment thickness across, sitting on the baseline just clear of the digit
double dpD = 2.0 * sx;
Shape dpShape = new Ellipse2D.Double(GLYPH_WIDTH * sx + 0.5 * sx, 18.0 * sy - dpD, dpD, dpD);
// Segment B (Top-Right vertical)
Path2D pathB = new Path2D.Double();
pathB.moveTo(x1, y0 + t * 0.5 + g);
pathB.lineTo(x1 - t * 0.35, y0 + g);
pathB.lineTo(x1 - t, y0 + t + 1.4 * g);
pathB.lineTo(x1 - t, ym - t * 0.5 - 0.8 * g);
pathB.lineTo(x1, ym - 0.8 * g);
pathB.closePath();
// Segment C (Bottom-Right vertical)
Path2D pathC = new Path2D.Double();
pathC.moveTo(x1, ym + 0.8 * g);
pathC.lineTo(x1 - t, ym + t * 0.5 + 0.8 * g);
pathC.lineTo(x1 - t, y1 - t - 1.4 * g);
pathC.lineTo(x1 - t * 0.35, y1 - g);
pathC.lineTo(x1, y1 - t * 0.5 - g);
pathC.closePath();
// Segment D (Bottom horizontal)
Path2D pathD = new Path2D.Double();
pathD.moveTo(x0 + t + g, y1 - t);
pathD.lineTo(x1 - t - g, y1 - t);
pathD.lineTo(x1 - g, y1 - t * 0.45);
pathD.lineTo(x1 - t * 0.6 - g, y1);
pathD.lineTo(x0 + t * 0.6 + g, y1);
pathD.lineTo(x0 + g, y1 - t * 0.45);
pathD.closePath();
// Segment E (Bottom-Left vertical)
Path2D pathE = new Path2D.Double();
pathE.moveTo(x0, ym + 0.8 * g);
pathE.lineTo(x0 + t, ym + t * 0.5 + 0.8 * g);
pathE.lineTo(x0 + t, y1 - t - 1.4 * g);
pathE.lineTo(x0 + t * 0.35, y1 - g);
pathE.lineTo(x0, y1 - t * 0.5 - g);
pathE.closePath();
// Segment F (Top-Left vertical)
Path2D pathF = new Path2D.Double();
pathF.moveTo(x0, y0 + t * 0.5 + g);
pathF.lineTo(x0 + t * 0.35, y0 + g);
pathF.lineTo(x0 + t, y0 + t + 1.4 * g);
pathF.lineTo(x0 + t, ym - t * 0.5 - 0.8 * g);
pathF.lineTo(x0, ym - 0.8 * g);
pathF.closePath();
// Segment G (Middle horizontal hexagon)
Path2D pathG = new Path2D.Double();
pathG.moveTo(x0 + 1.4 * g, ym);
pathG.lineTo(x0 + t * 0.6 + 1.4 * g, ym - t * 0.48);
pathG.lineTo(x1 - t * 0.6 - 1.4 * g, ym - t * 0.48);
pathG.lineTo(x1 - 1.4 * g, ym);
pathG.lineTo(x1 - t * 0.6 - 1.4 * g, ym + t * 0.48);
pathG.lineTo(x0 + t * 0.6 + 1.4 * g, ym + t * 0.48);
pathG.closePath();
// Decimal Point (DP)
double dpD = t * 0.95;
Shape dpShape = new Ellipse2D.Double(x1 + t * 0.25, y1 - dpD, dpD, dpD);
Shape[] segments = new Shape[] { pathA, pathB, pathC, pathD, pathE, pathF, pathG };
int mask = getSegmentMask(charToDisplay.isEmpty() ? '8' : charToDisplay.charAt(0));
// 1. Draw unlit segment silhouettes (dim background)
g2d.setColor(LED_OFF);
for (int i = 0; i < 7; i++) {
if ((mask & (1 << i)) == 0) {
g2d.fill(segments[i]);
}
}
if ((mask & SEG_DP) == 0 && !charToDisplay.contains(".")) {
if (showDecimalPoint && (mask & SEG_DP) == 0 && !charToDisplay.contains(".")) {
g2d.fill(dpShape);
}
// 2. Draw lit active segments
g2d.setColor(onColor);
for (int i = 0; i < 7; i++) {
if ((mask & (1 << i)) != 0) {
g2d.fill(segments[i]);
}
}
if ((mask & SEG_DP) != 0 || charToDisplay.contains(".")) {
if (showDecimalPoint && ((mask & SEG_DP) != 0 || charToDisplay.contains("."))) {
g2d.fill(dpShape);
}
// Outlining every segment in the background colour is what stops neighbours that share a
// vertex from fusing into one shape.
g2d.setColor(FACE_BLACK);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke((float) (0.25 * (sx + sy) / 2.0)));
for (Shape segment : segments) {
g2d.draw(segment);
}
g2d.setTransform(orig);
}
@@ -505,7 +544,157 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new RoundRectangle2D.Double(6, 3, width - 12, height - 6, 3, 3));
// Draw single stylized '8'
drawSevenSegmentDigit(g2d, 9, 5, width - 18, height - 10, "8.", LED_RED);
// the icon stands for the component type, so it keeps its decimal point whatever
// Punctuation is set to
drawSevenSegmentDigit(g2d, 9, 5, width - 18, height - 10, "8.", LED_RED, true);
}
/**
* The packages this component can draw, each carrying its own dimensions in millimetres.
*
* <p>Digit heights are definitional -- 0.36 and 0.56 inches are what the parts are named for.
* The 0.56 inch body, digit width and pitch have never been checked against a datasheet, and
* the 0.36 inch equivalents are placeholders scaled from the digit height rather than
* measurements.
*/
public enum DisplayType {
// 5161AS, 5621AS, 3641AS and 5641AS. The two four-digit parts differ in size but share a
// pinout, which is why one array serves both.
SingleDigit_10Pin("1-Digit 0.56\"", 12.7d, 19.0d, 8.1d, 14.2d, 0d, 15.24d, true, 1,
PIN_NAMES_1DIGIT, null),
TwoDigit_0_56_10Pin("2-Digit 0.56\"", 25.0d, 19.0d, 8.1d, 14.2d, 12.7d, 15.24d, true, 2,
PIN_NAMES_2DIGIT, null),
FourDigit_0_36_12Pin("4-Digit 0.36\"", 30.0d, 14.0d, 5.2d, 9.14d, 7.5d, 10.16d, true, 4,
PIN_NAMES_4DIGIT, null),
FourDigit_0_56_12Pin("4-Digit 0.56\"", 50.3d, 19.0d, 8.1d, 14.2d, 12.7d, 15.24d, true, 4,
PIN_NAMES_4DIGIT, null),
TM1637_Module_4Pin("4-Digit TM1637 Module", 42.0d, 24.0d, 5.5d, 9.2d, 7.62d, 0d, false, 4,
PIN_NAMES_TM1637, new Module(30.0d, 14.0d, 1.778d, 1.524d));
private final String label;
private final double bodyWidthMm;
private final double bodyLengthMm;
private final double digitWidthMm;
private final double digitHeightMm;
private final double digitPitchMm;
private final double rowSpacingMm;
private final boolean dualRow;
private final int digitCount;
private final String[] pinNames;
private final Module module;
DisplayType(String label, double bodyWidthMm, double bodyLengthMm, double digitWidthMm,
double digitHeightMm, double digitPitchMm, double rowSpacingMm, boolean dualRow,
int digitCount, String[] pinNames, Module module) {
this.label = label;
this.bodyWidthMm = bodyWidthMm;
this.bodyLengthMm = bodyLengthMm;
this.digitWidthMm = digitWidthMm;
this.digitHeightMm = digitHeightMm;
this.digitPitchMm = digitPitchMm;
this.rowSpacingMm = rowSpacingMm;
this.dualRow = dualRow;
this.digitCount = digitCount;
this.pinNames = pinNames;
this.module = module;
}
/**
* Distance between the two pin rows. It cannot be shared: a package only works if its rows sit
* within its body, and the 0.36 inch part is shorter than the 0.6 inch the larger ones use.
*/
public double getRowSpacingMm() { return rowSpacingMm; }
@Override public String toString() { return label; }
public double getBodyWidthMm() { return bodyWidthMm; }
public double getBodyLengthMm() { return bodyLengthMm; }
public double getDigitWidthMm() { return digitWidthMm; }
public double getDigitHeightMm() { return digitHeightMm; }
public double getDigitPitchMm() { return digitPitchMm; }
/** True for the DIP packages, which carry their pins in two rows rather than one. */
public boolean isDualRow() { return dualRow; }
/** Carried rather than inferred, so a two-, six- or eight-digit part is a constant rather than
* another branch through {@code draw}. */
public int getDigitCount() { return digitCount; }
/** The part's pins in DIP order. */
public String[] getPinNames() { return pinNames; }
/**
* The board a driven module is built on, or {@code null} for a bare package. A module carries
* its own driver, so it brings out no common pins and its polarity is not something you order.
*/
public Module getModule() { return module; }
public boolean isModule() { return module != null; }
}
/**
* Which electrode the digits share. The two are identical in pinout, package and dimensions, so
* this changes nothing that is drawn and reaches only the BOM.
*/
public enum Common {
Cathode("Common Cathode"),
Anode("Common Anode");
private final String label;
Common(String label) { this.label = label; }
@Override public String toString() { return label; }
}
/**
* What the package carries besides the digits. On a twelve-pin part these are not independent:
* A-G plus DP and four digit commons already account for every pin, so a colon has to share the
* decimal point's line or replace it. The property describes the face of the part, not a pin.
*/
public enum Punctuation {
None("No Punctuation"),
DecimalPoints("Decimal Points"),
Colon("Colon"),
Both("Colon + Decimal Points");
private final String label;
Punctuation(String label) { this.label = label; }
@Override public String toString() { return label; }
public boolean hasDecimalPoints() { return this == DecimalPoints || this == Both; }
public boolean hasColon() { return this == Colon || this == Both; }
}
/**
* The circuit board a driven module is built on, with the display mounted on it as a bezel and
* mounting holes at the corners. A bare package has none of this.
*
* <p>The hole figures are the pixel offsets this class drew before they were figures at all,
* converted at the canvas scale rather than measured. The exact conversions are kept rather
* than two-decimal roundings because the roundings moved the holes by a fiftieth of a pixel,
* which antialiasing turned into a visible difference.
*
* @author Branislav Stojkovic
*/
public static class Module {
private final double bezelWidthMm;
private final double bezelLengthMm;
private final double holeInsetMm;
private final double holeSizeMm;
public Module(double bezelWidthMm, double bezelLengthMm, double holeInsetMm,
double holeSizeMm) {
this.bezelWidthMm = bezelWidthMm;
this.bezelLengthMm = bezelLengthMm;
this.holeInsetMm = holeInsetMm;
this.holeSizeMm = holeSizeMm;
}
public double getBezelWidthMm() { return bezelWidthMm; }
public double getBezelLengthMm() { return bezelLengthMm; }
/** Centre of a corner hole, from each of the two edges nearest it. */
public double getHoleInsetMm() { return holeInsetMm; }
public double getHoleSizeMm() { return holeSizeMm; }
}
}
@@ -1,23 +1,23 @@
/*
*
*
* 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;
@@ -27,11 +27,14 @@ 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.Display;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
@@ -44,30 +47,60 @@ 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 = "TFT Touch Screen (ILI9341)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "2.8\" 240x320 Color TFT LCD Display with SPI Interface, Touch, and SD Slot",
// instanceNamePrefix = "DISP", zOrder = IDIYComponent.BOARD,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "TFT Display", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Color TFT LCD display module with an SPI interface",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class TFTDisplay extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
/**
* Silkscreen names for the 2.8". The default label would strip the touch prefix and print five
* pins all reading "T", and the names it keeps do not fit the 0.1" pitch. The nodes keep the
* full forms for the tooltips and the netlist.
*/
public static final String[] SILK_NAMES_ILI9341 = new String[] {"VCC", "GND", "CS", "RST", "DC",
"SDI", "SCK", "LED", "SDO", "TCK", "TCS", "TDI", "TDO", "IRQ"};
/** The 2.4" and the 2.8" are the same product with a different panel in it. */
public static final String[] PIN_NAMES_ILI9341 = new String[] {"VCC", "GND", "CS", "RESET", "DC",
"MOSI (SDI)", "SCK", "LED", "MISO (SDO)", "T_CLK", "T_CS", "T_DIN", "T_DO", "T_IRQ"};
/** The micro-SD socket, brought out on its own row on the opposite edge from everything else. */
public static final String[] PIN_NAMES_SD =
new String[] {"SD_CS", "SD_MOSI", "SD_MISO", "SD_SCK"};
/**
* The SD row drops the prefix its node names carry, which the default label would reduce to four
* pins all reading "SD". Only the row's position marks these as the card socket's.
*/
public static final String[] SILK_NAMES_SD = new String[] {"CS", "MOSI", "MISO", "SCK"};
/**
* Four-wire SPI plus reset, data/command and the backlight, which is what a board of this size
* brings out whichever controller is behind it.
*/
public static final String[] PIN_NAMES_SPI_8PIN =
new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS", "BLK"};
public static Color TFT_RED = Color.decode("#C0392B");
public static Color SCREEN_BG = Color.decode("#111111");
public static Color GLASS_COLOR = Color.decode("#1A1A1A");
public static Color SCREEN_INK = Color.decode("#E0E0E0");
public static Size BOARD_WIDTH = new Size(86.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(50.0d, SizeUnit.mm);
public static Size CORNER_RADIUS = new Size(1.5d, SizeUnit.mm);
public static final String[] PIN_NAMES = new String[] {
"VCC", "GND", "CS", "RESET", "DC", "MOSI (SDI)", "SCK", "LED", "MISO (SDO)",
"T_CLK", "T_CS", "T_DIN", "T_DO", "T_IRQ"
};
private Controller controller = Controller.ILI9341_2_8;
private Display screen = Display.VALUE;
public TFTDisplay() {
super();
@@ -75,43 +108,149 @@ public class TFTDisplay extends AbstractMakerBoard {
updateControlPoints();
}
@EditableProperty(name = "Controller")
public Controller getController() {
return controller == null ? Controller.ILI9341_2_8 : controller;
}
public void setController(Controller controller) {
this.controller = controller;
updateControlPoints();
invalidateCache();
}
@EditableProperty
public Display getScreen() {
return screen == null ? Display.VALUE : screen;
}
public void setScreen(Display screen) {
this.screen = screen;
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getController().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
Controller controller = getController();
String[] pinNames = controller.getPinNames();
if (index >= 0 && index < pinNames.length) {
return pinNames[index];
}
String[] secondary = controller.getSecondaryPinNames();
if (secondary != null && index >= pinNames.length
&& index < pinNames.length + secondary.length) {
return secondary[index - pinNames.length];
}
return "Pin " + (index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
protected String getSilkPinLabel(int index) {
Controller controller = getController();
if (controller.getPinNames() == PIN_NAMES_ILI9341 && index >= 0
&& index < SILK_NAMES_ILI9341.length) {
return SILK_NAMES_ILI9341[index];
}
int secondaryIndex = index - controller.getPinNames().length;
if (controller.getSecondaryPinNames() == PIN_NAMES_SD && secondaryIndex >= 0
&& secondaryIndex < SILK_NAMES_SD.length) {
return SILK_NAMES_SD[secondaryIndex];
}
return super.getSilkPinLabel(index);
}
double[][] relativeOffsets = new double[14][2];
for (int i = 0; i < 14; i++) {
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
/** Left edge of the board, derived so that the primary pin row sits centred across it. */
private double getBoardX(double x) {
Controller controller = getController();
double spacing = PIN_SPACING.convertToPixels();
double span = (controller.getPinNames().length - 1) * spacing;
return x - (px(controller.getBoardWidthMm()) - span) / 2.0;
}
/**
* A board headed at the bottom stands above control point zero; one headed at the top hangs
* below it.
*/
private double getBoardY(double y) {
Controller controller = getController();
if (controller.isHeaderAtTop()) {
return y - px(controller.getHeaderOffsetMm());
}
return y - px(controller.getBoardLengthMm() - controller.getHeaderOffsetMm());
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
Controller controller = getController();
int primaryCount = controller.getPinNames().length;
String[] secondary = controller.getSecondaryPinNames();
int secondaryCount = secondary == null ? 0 : secondary.length;
double[][] relativeOffsets = new double[primaryCount + secondaryCount][2];
for (int i = 0; i < primaryCount; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
rotatePoints(firstPoint, relativeOffsets);
// The second row faces the first, so its distance from control point zero is what the two
// offsets leave of the length. Both rows are centred on the width.
if (secondaryCount > 0) {
double indent = (primaryCount - secondaryCount) * spacing / 2.0;
double across = px(controller.getBoardLengthMm() - controller.getHeaderOffsetMm()
- controller.getSecondaryHeaderOffsetMm());
if (!controller.isHeaderAtTop()) {
across = -across;
}
for (int i = 0; i < secondaryCount; i++) {
relativeOffsets[primaryCount + i][0] = indent + i * spacing;
relativeOffsets[primaryCount + i][1] = across;
}
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@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 - 70;
double boardY = y - 20;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 10, 10);
Controller controller = getController();
double boardX = getBoardX(p0.getX());
double boardY = getBoardY(p0.getY());
double boardW = px(controller.getBoardWidthMm());
double boardH = px(controller.getBoardLengthMm());
if (controller.isRound()) {
// The tab has to reach the disc's centre line before the two are unioned: stopped at the
// bottom of the disc it would meet it at a tangent and leave a notch either side.
double discR = boardW / 2.0;
double discCy = boardY + discR;
double tabW = px(controller.getTabWidthMm());
Area body = new Area(new Ellipse2D.Double(boardX, boardY, boardW, boardW));
body.add(new Area(new Rectangle2D.Double(boardX + (boardW - tabW) / 2.0, discCy, tabW,
boardY + boardH - discCy)));
return body;
}
double radius = CORNER_RADIUS.convertToPixels();
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, radius, radius);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
@@ -125,10 +264,11 @@ public class TFTDisplay extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - 70;
double boardY = y - 20;
Controller controller = getController();
double boardW = px(controller.getBoardWidthMm());
double boardH = px(controller.getBoardLengthMm());
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
@@ -144,38 +284,103 @@ public class TFTDisplay extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// 4 Mounting holes in corners
MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + 20, 20);
MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + boardH - 20, 20);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + 20, 20);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + boardH - 20, 20);
// Centring the panel between the hole rows rather than on an edge is what keeps it clear of
// both pairs. A board without holes falls back to the space below the pin row.
double bandTop = boardY + px(controller.getHeaderOffsetMm());
double bandBottom = boardY + boardH;
// Color Screen Area
double screenMarginX = 35;
double screenW = boardW - 2 * screenMarginX - 25;
double screenH = boardH - 65;
double screenX = boardX + screenMarginX;
double screenY = boardY + 45;
if (controller.hasMountingHoles()) {
double holeInset = px(controller.getHoleInsetMm());
double sideInset = px(controller.getHoleSideInsetMm());
double headerSideInset = px(controller.getHeaderSideHoleInsetMm());
double holeSize = px(controller.getHoleSizeMm());
double topInset = controller.isHeaderAtTop() ? headerSideInset : holeInset;
double bottomInset = controller.isHeaderAtTop() ? holeInset : headerSideInset;
MakerBoardPainter.drawMountingHole(g2d, boardX + sideInset, boardY + topInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - sideInset, boardY + topInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + sideInset, boardY + boardH - bottomInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - sideInset,
boardY + boardH - bottomInset, holeSize);
bandTop = boardY + topInset;
bandBottom = boardY + boardH - bottomInset;
}
double screenW = px(controller.getScreenWidthMm());
double screenH = px(controller.getScreenLengthMm());
Shape panel;
if (controller.isRound()) {
// the lit circle is centred on the disc, which sits above the tab rather than on the
// centre line of the bounding box
double discR = boardW / 2.0;
double discCx = boardX + discR;
double discCy = boardY + discR;
panel = new Ellipse2D.Double(discCx - screenW / 2.0, discCy - screenH / 2.0, screenW,
screenH);
} else {
double bandCentre = (bandTop + bandBottom) / 2.0;
double screenX;
double screenY;
if (controller.hasGlass()) {
Glass glass = controller.getGlass();
double glassH = px(glass.getLengthMm());
double glassW = glass.spansTheBoard() ? boardW : px(glass.getWidthMm());
double glassX = glass.isPlacedFromTheRightEdge()
? boardX + boardW - px(glass.getRightInsetMm()) - glassW
: boardX + (boardW - glassW) / 2.0;
double glassY = bandCentre - glassH / 2.0;
g2d.setColor(GLASS_COLOR);
g2d.fill(new Rectangle2D.Double(glassX, glassY, glassW, glassH));
// Centred in the glass, and vertically too unless the board states its own figure: the
// driver's bonding region takes the bottom of the panel, so centring would drop it.
screenX = glassX + (glassW - screenW) / 2.0;
screenY = controller.hasMeasuredScreenTop() ? boardY + px(controller.getScreenTopMm())
: glassY + (glassH - screenH) / 2.0;
} else {
screenX = boardX + (boardW - screenW) / 2.0;
screenY = controller.hasMeasuredScreenTop() ? boardY + px(controller.getScreenTopMm())
: bandCentre - screenH / 2.0;
}
panel = new Rectangle2D.Double(screenX, screenY, screenW, screenH);
}
g2d.setColor(SCREEN_BG);
g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
g2d.fill(panel);
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(panel);
// Display Demo UI
g2d.setColor(Color.decode("#2980B9"));
g2d.fillRect((int) screenX, (int) screenY, (int) screenW, 25);
g2d.setColor(Color.WHITE);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, "TFT 2.8\" 320x240 SPI", screenX + screenW / 2.0, screenY + 12, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Only the inscribed square is safely inside a round panel.
Rectangle2D textArea = panel.getBounds2D();
if (controller.isRound()) {
double side = textArea.getWidth() / Math.sqrt(2.0);
textArea = new Rectangle2D.Double(textArea.getCenterX() - side / 2.0,
textArea.getCenterY() - side / 2.0, side, side);
}
MakerBoardPainter.drawScreenText(g2d, textArea, SCREEN_INK, getScreen(), getName(),
getValueForDisplay());
// SD Card holder on right edge
MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW - 28, boardY + 100, 36, 120, "SD");
// Names lie flat: the strip is 3.5 mm deep on the two smallest boards and a label stood on
// end needs 4.7 mm. Each row prints into the board rather than off its own edge.
double[][] offsets = getRelativeOffsets();
drawFlatRowPinLabels(g2d, x, y, offsets, 0, controller.getPinNames().length,
controller.isHeaderAtTop(), SILK_COLOR);
if (controller.hasSecondaryHeader()) {
drawFlatRowPinLabels(g2d, x, y, offsets, controller.getPinNames().length,
controller.getSecondaryPinNames().length, !controller.isHeaderAtTop(), SILK_COLOR);
}
}
g2d.setTransform(oldTx);
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawPinHeader(g2d, 0, controller.getPinNames().length, outlineMode, drawingObserver);
if (controller.hasSecondaryHeader()) {
drawPinHeader(g2d, controller.getPinNames().length,
controller.getSecondaryPinNames().length, outlineMode, drawingObserver);
}
g2d.setComposite(oldComposite);
}
@@ -183,18 +388,220 @@ public class TFTDisplay extends AbstractMakerBoard {
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(TFT_RED);
g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3));
g2d.fill(new RoundRectangle2D.Double(8, 1, width - 16, height - 2, 3, 3));
g2d.setColor(TFT_RED.darker());
g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3));
g2d.draw(new RoundRectangle2D.Double(8, 1, width - 16, height - 2, 3, 3));
// Screen
// The glass reaches both edges of the board and is centred down it.
g2d.setColor(SCREEN_BG);
g2d.fillRect(6, 9, width - 12, height - 16);
g2d.setColor(Color.decode("#2980B9"));
g2d.fillRect(6, 9, width - 12, 5);
g2d.fillRect(8, 6, width - 16, height - 12);
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 5));
StringUtils.drawCenteredText(g2d, "TFT", width / 2, height / 2 + 3, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "TFT", width / 2, height / 2, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER);
}
public enum Controller {
// Ordered by diagonal; a new variant goes in at its place in that run rather than at the end.
// A glass length of zero means the module has no dark panel wider than its own lit area, so
// only the screen is drawn, and a hole size of zero means no mounting holes at all. The round
// board carries the same two outline figures as the rest: width is the disc diameter and
// length runs from the tab's outer edge to the far side of the disc, so the tab's projection
// is the difference between them.
//
// A board headed at the top hangs below its row and one headed at the bottom grows upwards
// from it. Both occur here, so every constant states its own edge rather than taking a
// default.
// The one board whose bezel does not span it, being pushed into the right-hand side with the
// driver's circuitry filling the bare L that leaves.
ST7735_0_96("0.96\" ST7735 80x160", 30.0d, 24.0d, 21.7d, 10.8d,
new Glass(12.8d, 23.7d, 2.0d), 1.44d, 2.5d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 0d, null, 0d,
2.5d, true),
GC9A01_1_28("1.28\" GC9A01 240x240 Round", 38.0d, 45.5d, 32.5d, 32.5d, null, 1.76d, 0d, 0d,
22.9d,
new String[] {"RST", "CS", "DC", "SDA", "SCL", "GND", "VCC"}, false),
// Lit area 1.5 mm above the middle of the bezel; rederive it if the hole rows move.
ST7789_1_54("1.54\" ST7789 240x240", 32.0d, 43.72d, 27.66d, 27.66d, new Glass(33.7d), 1.5d,
2.5d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 6.53d, null, 0d, 2.5d, true),
// Bezel narrower than the PCB but centred across it, unlike the 0.96"'s. Lit area 2 mm above
// the middle of the bezel; rederive it if the bezel moves.
ST7735_1_8("1.8\" ST7735 128x160", 35.0d, 56.0d, 28.0d, 35.0d, new Glass(45.83d, 33.0d), 1.41d,
2.5d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 8.5d, null, 0d, 2.5d, true),
// The plain 1.8"'s panel on a longer board with a micro-SD row, which turns the module round.
// The one board whose holes are not the same distance in from every edge, and whose 8-pin row
// runs alongside the holes on its edge rather than between them.
ST7735_1_8_SD("1.8\" ST7735 128x160 (SD)", 34.0d, 58.0d, 28.0d, 35.0d,
new Glass(45.83d, 33.0d), 2.5d, 3.0d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 9.5d, PIN_NAMES_SD,
2.5d, 3.0d, 2.75d, false),
// The pair of holes beside the 14-pin row is set deeper than the rest: at the shared 3 mm the
// row's 14 pins, spanning 33 mm of a 42.72 mm board, would sit on them.
ILI9341_2_4("2.4\" ILI9341 240x320 (Touch + SD)", 42.72d, 77.18d, 36.72d, 48.96d,
new Glass(60.26d), 2.0d, 3.0d, 3.0d, 0d, PIN_NAMES_ILI9341, 9.26d, PIN_NAMES_SD, 2.0d,
6.92d, false),
// As the 2.4", with the pair beside the pin row set deeper again. That carries the panel with
// it, the glass being centred between the hole rows, so the lit area's 9.70 mm has to be
// rederived if the holes move.
ILI9341_2_8("2.8\" ILI9341 240x320 (Touch + SD)", 50.0d, 86.0d, 43.2d, 57.6d, new Glass(69.2d),
2.0d, 3.0d, 3.0d, 0d, PIN_NAMES_ILI9341, 9.70d, PIN_NAMES_SD, 2.5d, 6.50d, false);
private final String label;
private final double boardWidthMm;
private final double boardLengthMm;
private final double screenWidthMm;
private final double screenLengthMm;
private final Glass glass;
private final double headerOffsetMm;
private final double holeInsetMm;
private final double holeSizeMm;
private final double tabWidthMm;
private final String[] pinNames;
private final double screenTopMm;
private final String[] secondaryPinNames;
private final double secondaryHeaderOffsetMm;
private final double headerSideHoleInsetMm;
private final double holeSideInsetMm;
private final boolean headerAtTop;
/** For a board whose lit area is centred and whose pins all leave on one row. */
Controller(String label, double boardWidthMm, double boardLengthMm, double screenWidthMm,
double screenLengthMm, Glass glass, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames, boolean headerAtTop) {
this(label, boardWidthMm, boardLengthMm, screenWidthMm, screenLengthMm, glass, headerOffsetMm,
holeInsetMm, holeSizeMm, tabWidthMm, pinNames, 0d, null, 0d, holeInsetMm, headerAtTop);
}
/** For a board whose holes are the same distance in from every edge they are near. */
Controller(String label, double boardWidthMm, double boardLengthMm, double screenWidthMm,
double screenLengthMm, Glass glass, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames, double screenTopMm,
String[] secondaryPinNames, double secondaryHeaderOffsetMm, double headerSideHoleInsetMm,
boolean headerAtTop) {
this(label, boardWidthMm, boardLengthMm, screenWidthMm, screenLengthMm, glass, headerOffsetMm,
holeInsetMm, holeSizeMm, tabWidthMm, pinNames, screenTopMm, secondaryPinNames,
secondaryHeaderOffsetMm, headerSideHoleInsetMm, holeInsetMm, headerAtTop);
}
Controller(String label, double boardWidthMm, double boardLengthMm, double screenWidthMm,
double screenLengthMm, Glass glass, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames, double screenTopMm,
String[] secondaryPinNames, double secondaryHeaderOffsetMm, double headerSideHoleInsetMm,
double holeSideInsetMm, boolean headerAtTop) {
this.label = label;
this.boardWidthMm = boardWidthMm;
this.boardLengthMm = boardLengthMm;
this.screenWidthMm = screenWidthMm;
this.screenLengthMm = screenLengthMm;
this.glass = glass;
this.headerOffsetMm = headerOffsetMm;
this.holeInsetMm = holeInsetMm;
this.holeSizeMm = holeSizeMm;
this.tabWidthMm = tabWidthMm;
this.pinNames = pinNames;
this.screenTopMm = screenTopMm;
this.secondaryPinNames = secondaryPinNames;
this.secondaryHeaderOffsetMm = secondaryHeaderOffsetMm;
this.headerSideHoleInsetMm = headerSideHoleInsetMm;
this.holeSideInsetMm = holeSideInsetMm;
this.headerAtTop = headerAtTop;
}
@Override public String toString() { return label; }
public double getBoardWidthMm() { return boardWidthMm; }
public double getBoardLengthMm() { return boardLengthMm; }
public double getScreenWidthMm() { return screenWidthMm; }
public double getScreenLengthMm() { return screenLengthMm; }
/** The dark panel, or {@code null} on a board whose lit area is all there is to draw. */
public Glass getGlass() { return glass; }
public double getHeaderOffsetMm() { return headerOffsetMm; }
public double getHoleSizeMm() { return holeSizeMm; }
public String[] getPinNames() { return pinNames; }
/**
* Inset from each of the two edges nearest a hole, except where
* {@link #getHeaderSideHoleInsetMm()} or {@link #getHoleSideInsetMm()} overrides it.
*/
public double getHoleInsetMm() { return holeInsetMm; }
/** Overrides the shared inset across the board; only the 1.8" with the SD socket needs it. */
public double getHoleSideInsetMm() { return holeSideInsetMm; }
/**
* Inset of the two holes on the header's edge, set deeper on boards whose row would reach them.
*/
public double getHeaderSideHoleInsetMm() { return headerSideHoleInsetMm; }
public boolean hasMountingHoles() { return holeSizeMm > 0; }
public boolean hasGlass() { return glass != null; }
/** True where the main row leaves by the top edge, so the board hangs below it. */
public boolean isHeaderAtTop() { return headerAtTop; }
/** Width of the tab the header sits on; meaningful only for a round board. */
public double getTabWidthMm() { return tabWidthMm; }
/** How far the tab projects past the disc: whatever the outline is longer than it is wide. */
public double getTabProjectionMm() { return boardLengthMm - boardWidthMm; }
public boolean isRound() { return tabWidthMm > 0; }
/** Top of the lit area from the board's top edge; zero where it is centred in the glass. */
public double getScreenTopMm() { return screenTopMm; }
public boolean hasMeasuredScreenTop() { return screenTopMm > 0; }
/** The second pin row, or {@code null} on a board that brings everything out on one. */
public String[] getSecondaryPinNames() { return secondaryPinNames; }
public boolean hasSecondaryHeader() { return secondaryPinNames != null; }
/** Clearance from the second row to the top edge, the one the main header does not use. */
public double getSecondaryHeaderOffsetMm() { return secondaryHeaderOffsetMm; }
}
/**
* The dark panel the lit area sits in, whose frame is the display's visible border. Centred
* between the board's mounting holes, and across the board unless it states an inset from the
* right edge.
*
* @author Branislav Stojkovic
*/
public static class Glass {
private final double lengthMm;
private final double widthMm;
private final double rightInsetMm;
/** A panel that spans the board. */
public Glass(double lengthMm) {
this(lengthMm, 0d, 0d);
}
/** A panel narrower than its board, centred across it. */
public Glass(double lengthMm, double widthMm) {
this(lengthMm, widthMm, 0d);
}
/** A panel narrower than its board and pushed towards the right edge. */
public Glass(double lengthMm, double widthMm, double rightInsetMm) {
this.lengthMm = lengthMm;
this.widthMm = widthMm;
this.rightInsetMm = rightInsetMm;
}
public double getLengthMm() { return lengthMm; }
/** Zero where the panel is as wide as the board. */
public double getWidthMm() { return widthMm; }
/** How far the panel stops short of the right edge; meaningful only where it is narrower. */
public double getRightInsetMm() { return rightInsetMm; }
public boolean spansTheBoard() { return widthMm <= 0; }
/** True for a narrower panel placed from the right edge rather than centred on the board. */
public boolean isPlacedFromTheRightEdge() { return rightInsetMm > 0; }
}
}
@@ -0,0 +1,232 @@
/*
*
* 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.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
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 = "NeoPixel Breakout", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Single-LED Addressable RGB WS2812B NeoPixel Breakout",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BBreakout extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
public static Color NEO_BLACK = Color.decode("#111111");
// A three-pad column up each short edge, with the two mounting holes on the centre line
// between them.
public static Size BOARD_WIDTH = new Size(0.5d, SizeUnit.in);
public static Size BOARD_HEIGHT = new Size(0.4d, SizeUnit.in);
// The real board's three-way connectors are drawn as plain pads: at this size a connector body
// would hide the LED, and what a layout needs is where the three wires land. The pads are flush
// with their own edge, so how far in their centres sit is half their reach.
public static Size PAD_WIDTH = new Size(1.5d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(0.3d, SizeUnit.in);
// The two grounds are the same net on the board, as are the two supplies, and are numbered only
// to keep the node names distinct.
private static final String[] PIN_NAMES = {
"GND_1", "VIN_1", "IN",
"GND_2", "VIN_2", "OUT"
};
public WS2812BBreakout() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
@Override
protected String getVariantLabel() {
return getLedType().toString();
}
@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();
// the columns are flush against their own edges, so what separates them is the board less one
// whole pad
double columnSpacing = BOARD_WIDTH.convertToPixels() - PAD_WIDTH.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < 3; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
relativeOffsets[3 + i][0] = columnSpacing;
relativeOffsets[3 + i][1] = i * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
}
/** The first pad column is flush with this edge, so its centres sit half a pad inboard. */
private double getBoardX(double x) {
return x - PAD_WIDTH.convertToPixels() / 2.0;
}
/** Derived so that the three-pad column is centred across the height. */
private double getBoardY(double y) {
double padSpan = 2 * PIN_SPACING.convertToPixels();
return y - (BOARD_HEIGHT.convertToPixels() - padSpan) / 2.0;
}
Point2D getLedCentre() {
Point2D p0 = controlPoints[0];
return new Point2D.Double(getBoardX(p0.getX()) + BOARD_WIDTH.convertToPixels() / 2.0,
getBoardY(p0.getY()) + BOARD_HEIGHT.convertToPixels() / 2.0);
}
/**
* The holes sit on the centre line astride the LED and are what the board is tightest against:
* 0.27 mm of board outside each rim and 0.31 mm between the rim and the package.
*/
Point2D getHoleCentre(boolean bottom) {
Point2D centre = getLedCentre();
double offset = MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
return new Point2D.Double(centre.getX(), centre.getY() + (bottom ? offset : -offset));
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
BOARD_WIDTH.convertToPixels(), BOARD_HEIGHT.convertToPixels(), 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);
}
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) {
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (boolean bottom : new boolean[] {false, true}) {
Point2D hole = getHoleCentre(bottom);
MakerBoardPainter.drawMountingHole(g2d, hole.getX(), hole.getY(), holeSize);
}
// The 2.35 mm between a pad and the package is not room beside a pad, so there is nowhere a
// name would go and this face carries no silkscreen. Drawn lit off the family's wheel, so a
// breakout reads as the same part as one pixel of a stick.
Point2D led = getLedCentre();
MakerBoardPainter.drawAddressableLed(g2d, led.getX(), led.getY(),
RGB_LED_SIZE.convertToPixels(), getLedColors(1)[0], 0, getLedType());
}
g2d.setTransform(oldTx);
drawSurfacePads(g2d, 0, controlPoints.length, PAD_WIDTH, PAD_LENGTH, outlineMode,
drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// landscape in the board's own 5:4, so the icon does not read as one of the portrait modules
double rectW = width - 4;
double rectH = rectW * 0.8;
double rectX = 2;
double rectY = (height - rectH) / 2.0;
g2d.setColor(NEO_BLACK);
g2d.fill(new RoundRectangle2D.Double(rectX, rectY, rectW, rectH, 3, 3));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new RoundRectangle2D.Double(rectX, rectY, rectW, rectH, 3, 3));
double padW = 3;
double padH = 2;
double padStep = 5;
g2d.setColor(PAD_COLOR);
for (int i = 0; i < 3; i++) {
double padY = height / 2.0 - padStep + i * padStep - padH / 2.0;
g2d.fill(new Rectangle2D.Double(rectX, padY, padW, padH));
g2d.fill(new Rectangle2D.Double(rectX + rectW - padW, padY, padW, padH));
}
double ledSize = Math.min(rectW - 6, 12);
MakerBoardPainter.drawAddressableLed(g2d, width / 2.0, height / 2.0, ledSize,
Color.decode("#FF3333"));
}
}
@@ -0,0 +1,256 @@
/*
*
* 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.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
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;
// A sibling of WS2812BRing rather than another RingSize constant: the Jewel is a solid disc
// with no inner rim, six LEDs on a circle with a seventh in the middle, pads inside that circle
// rather than out by the rim, and two mounting holes no ring has.
@ComponentDescriptor(name = "NeoPixel Jewel", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Addressable RGB WS2812B NeoPixel Jewel (7 LEDs)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BJewel extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
public static Color NEO_BLACK = Color.decode("#111111");
public static Size OUTER_DIAMETER = new Size(23.0d, SizeUnit.mm);
// Measured: with no inner rim there is no midpoint to place the LEDs on, as the rings do.
public static Size LED_CIRCLE_DIAMETER = new Size(16.0d, SizeUnit.mm);
// Inside the LED circle, where the rings put theirs just in from the outer rim.
public static Size PAD_CIRCLE_DIAMETER = new Size(9.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(19.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(3.0d, SizeUnit.mm);
/** The LEDs on the circle. The seventh sits in the middle and is not one of them. */
public static final int RING_LED_COUNT = 6;
private static final String[] PAD_NAMES = new String[] {"OUT", "G_1", "G_2", "PWR", "IN"};
// Which gap between two consecutive ring LEDs each pad sits in. OUT is anticlockwise of the
// top LED and the rest follow clockwise, so the sixth gap carries no pad: it is the one due
// left, where the left mounting hole sits.
private static final int[] PAD_GAP_INDEX = new int[] {0, 1, 2, 3, 4};
public WS2812BJewel() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
@Override
protected String getVariantLabel() {
return getLedType().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PAD_NAMES.length) {
return PAD_NAMES[index];
}
return Integer.toString(index + 1);
}
private double getOuterRadius() {
return OUTER_DIAMETER.convertToPixels() / 2.0;
}
/**
* The one place the pad radius is decided: {@link #updateControlPoints()} offsets every pad from
* the first by it and {@link #getCenter()} works backwards from the first pad, so the two
* disagreeing would draw the body away from its own control points.
*/
private double getPadRadius() {
return PAD_CIRCLE_DIAMETER.convertToPixels() / 2.0;
}
/**
* Each pad sits in a gap between two consecutive ring LEDs, the gaps {@link #PAD_GAP_INDEX}
* records. Where the sequence starts is arbitrary, since the component rotates.
*/
private double getPadAngle(int index) {
double gap = PAD_GAP_INDEX[index] + RING_LED_COUNT - 0.5;
return 2 * Math.PI * gap / RING_LED_COUNT - Math.PI / 2.0;
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double padR = getPadRadius();
double theta0 = getPadAngle(0);
double[][] relativeOffsets = new double[PAD_NAMES.length][2];
for (int i = 0; i < PAD_NAMES.length; i++) {
double theta = getPadAngle(i);
relativeOffsets[i][0] = padR * (Math.cos(theta) - Math.cos(theta0));
relativeOffsets[i][1] = padR * (Math.sin(theta) - Math.sin(theta0));
}
rotatePoints(firstPoint, relativeOffsets);
}
private Point2D getCenter() {
Point2D p0 = controlPoints[0];
double padR = getPadRadius();
double theta0 = getPadAngle(0);
// unrotated, the centre sits one pad radius back along the first pad's angle
return new Point2D.Double(p0.getX() - padR * Math.cos(theta0),
p0.getY() - padR * Math.sin(theta0));
}
@Override
public Shape getBodyShape() {
Point2D center = getCenter();
double outerR = getOuterRadius();
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 = getOuterRadius();
Shape disc = new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2);
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(disc);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(disc);
if (!outlineMode) {
double holeOffset = MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, cx - holeOffset, cy, holeSize);
MakerBoardPainter.drawMountingHole(g2d, cx + holeOffset, cy, holeSize);
double ledR = LED_CIRCLE_DIAMETER.convertToPixels() / 2.0;
double ledSize = RGB_LED_SIZE.convertToPixels();
Color[] ledColors = getLedColors(RING_LED_COUNT + 1);
LedType ledType = getLedType();
for (int i = 0; i < RING_LED_COUNT; i++) {
double angle = 2 * Math.PI * i / RING_LED_COUNT - Math.PI / 2.0;
MakerBoardPainter.drawAddressableLed(g2d, cx + ledR * Math.cos(angle),
cy + ledR * Math.sin(angle), ledSize, ledColors[i % ledColors.length],
angle + Math.PI / 2.0, ledType);
}
// the centre package has no radius to face along, so it stays square to the board
MakerBoardPainter.drawAddressableLed(g2d, cx, cy, ledSize,
ledColors[RING_LED_COUNT % ledColors.length], 0, ledType);
// A pad sits in the gap between two LEDs, nowhere near enough room for its name.
}
g2d.setTransform(oldTx);
drawPcbSolderPads(g2d, 0, PAD_NAMES.length, false, 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;
Ellipse2D disc = new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2);
g2d.setColor(NEO_BLACK);
g2d.fill(disc);
g2d.setColor(Color.DARK_GRAY);
g2d.draw(disc);
Color[] rainbow = new Color[] {
Color.decode("#FF3333"),
Color.decode("#FFD700"),
Color.decode("#00E676"),
Color.decode("#00E5FF"),
Color.decode("#E040FB"),
Color.decode("#FF6B35")
};
double ledR = outerR * 0.6;
int dotR = Math.max(2, (int) (outerR * 0.22));
for (int i = 0; i < RING_LED_COUNT; i++) {
double angle = 2 * Math.PI * i / RING_LED_COUNT - Math.PI / 2.0;
double lx = cx + ledR * Math.cos(angle);
double ly = cy + ledR * Math.sin(angle);
g2d.setColor(rainbow[i % rainbow.length]);
g2d.fill(new Ellipse2D.Double(lx - dotR, ly - dotR, dotR * 2, dotR * 2));
}
// the filled centre is what separates this icon from the ring's
g2d.setColor(Color.WHITE);
g2d.fill(new Ellipse2D.Double(cx - dotR, cy - dotR, dotR * 2, dotR * 2));
}
}
@@ -0,0 +1,326 @@
/*
*
* 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.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.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
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;
// Figures are read off Adafruit's published EagleCAD board file rather than a product listing,
// which gives only the outline.
//
// Three relationships hold on this board and are recorded as the measurements they are rather
// than derived, because a panel from another maker need not share them: the board is exactly
// eight pitches across, the pixel grid is centred leaving half a pitch outside the outer
// pixels, and the port row lands in the gap between the first and second rows of pixels.
@ComponentDescriptor(name = "NeoPixel Panel", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Addressable RGB WS2812B NeoPixel Matrix Panel (8x8, 64 LEDs)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BPanel extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
public static Color NEO_BLACK = Color.decode("#111111");
public static Size BOARD_SIZE = new Size(71.12d, SizeUnit.mm);
public static Size PIXEL_PITCH = new Size(8.89d, SizeUnit.mm);
/** How far the outer pad of a port sits in from the side edge its port stands against. */
public static Size PAD_EDGE_INSET = new Size(1.905d, SizeUnit.mm);
/** How far a port row sits in from the end of the board it runs beside. */
public static Size PAD_ROW_INSET = new Size(8.89d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.8d, SizeUnit.mm);
public static Size CORNER_RADIUS = new Size(1.0d, SizeUnit.mm);
/** Pixels along one edge of the square matrix. */
public static final int MATRIX_ORDER = 8;
/**
* Hole centres from the middle of the board. The pattern is symmetric about both axes, so which
* way the vertical axis points does not change the drawing.
*/
private static final double[][] MOUNTING_HOLES = new double[][] {
{-33.02d, 17.78d}, {-33.02d, 0d}, {-33.02d, -17.78d},
{33.02d, 17.78d}, {33.02d, 0d}, {33.02d, -17.78d},
{-8.89d, 26.67d}, {8.89d, 26.67d},
{-8.89d, 0d}, {8.89d, 0d},
{-8.89d, -26.67d}, {8.89d, -26.67d}};
/**
* Input first, then output. Both ports carry the same two rails, numbered to keep the node names
* distinct; the board prints them +5V and GND, on its back rather than this face.
*/
public static final String[] PAD_NAMES =
new String[] {"DIN", "+5V_1", "GND_1", "DOUT", "+5V_2", "GND_2"};
/**
* Eagle specifies text by cap height and a Java font by em size, about a third larger again for
* this family. Converting is what makes the drawn strings reach as far across the board as the
* real artwork; set at the cap height they come out well short.
*/
private static final double CAP_HEIGHT_RATIO = 0.72d;
/**
* What the face prints, from layer 21 of the board file. Every line lies in a gap between two
* rows of pixels, which is the only bare board this part has. The port names are on layer 22,
* its back.
*/
static final Silk[] SILKSCREEN = new Silk[] {
new Silk(-19.685d, -17.653d, "Adafruit NeoPixel 8X8", silkFont(2.286d)),
new Silk(-29.972d, 0.0635d, "64 RGB LEDs", silkFont(1.778d)),
new Silk(-6.858d, -0.1397d, "24 bit Color", silkFont(1.4224d)),
new Silk(12.446d, -0.1905d, "Only ONE Pin", silkFont(1.778d)),
new Silk(-25.4d, 17.8435d, "bLiNkY bLiNkY bLiNkY bLiNkY bLiNkY", silkFont(1.778d))};
public WS2812BPanel() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
/** The panel comes in one size, so the package rather than the grid keeps the BOM rows apart. */
@Override
protected String getVariantLabel() {
return "8x8, 64 LEDs, " + getLedType();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PAD_NAMES.length) {
return PAD_NAMES[index];
}
return Integer.toString(index + 1);
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
private static Font silkFont(double capHeightMm) {
return new Font(SILK_FONT_FAMILY, Font.PLAIN,
(int) Math.round(px(capHeightMm) / CAP_HEIGHT_RATIO));
}
/** Left edge of the board; the input port's first pad sits one inset in from it. */
private double getBoardX(double x) {
return x - PAD_EDGE_INSET.convertToPixels();
}
/** Top edge of the board; the input port's row sits its own inset down from it. */
private double getBoardY(double y) {
return y - PAD_ROW_INSET.convertToPixels();
}
/**
* The two ports are each other turned about the centre of the board, which is what lets panels
* be chained: the input runs inward from the left edge near the top, the output inward from the
* right edge near the bottom.
*/
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double board = BOARD_SIZE.convertToPixels();
double acrossBoard = board - 2 * PAD_EDGE_INSET.convertToPixels();
double downBoard = board - 2 * PAD_ROW_INSET.convertToPixels();
double[][] relativeOffsets = new double[PAD_NAMES.length][2];
for (int i = 0; i < 3; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
relativeOffsets[3 + i][0] = acrossBoard - i * spacing;
relativeOffsets[3 + i][1] = downBoard;
}
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double board = BOARD_SIZE.convertToPixels();
double radius = CORNER_RADIUS.convertToPixels();
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), board, board,
radius, radius);
}
@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 board = BOARD_SIZE.convertToPixels();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
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) {
double centreX = boardX + board / 2.0;
double centreY = boardY + board / 2.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (double[] hole : MOUNTING_HOLES) {
MakerBoardPainter.drawMountingHole(g2d, centreX + px(hole[0]), centreY + px(hole[1]),
holeSize);
}
// printed before the packages, so a glyph straying under one is covered as it is on the board
g2d.setColor(SILK_COLOR);
for (Silk silk : SILKSCREEN) {
g2d.setFont(silk.getFont());
StringUtils.drawCenteredText(g2d, silk.getText(), centreX + px(silk.getXMm()),
centreY + px(silk.getYMm()), HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
}
double pitch = PIXEL_PITCH.convertToPixels();
double margin = (board - (MATRIX_ORDER - 1) * pitch) / 2.0;
double ledSize = RGB_LED_SIZE.convertToPixels();
// The sweep follows the data chain, so it shows the addressing order: a progressive raster,
// left to right along each row and back, not the serpentine most cheap panels use.
int ledCount = MATRIX_ORDER * MATRIX_ORDER;
Color[] ledColors = getLedColors(ledCount);
LedType ledType = getLedType();
for (int i = 0; i < ledCount; i++) {
MakerBoardPainter.drawAddressableLed(g2d, boardX + margin + (i % MATRIX_ORDER) * pitch,
boardY + margin + (i / MATRIX_ORDER) * pitch, ledSize, ledColors[i], 0, ledType);
}
// The port names are printed on the back of the real board, so this face carries none.
}
g2d.setTransform(oldTx);
drawPcbSolderPads(g2d, 0, PAD_NAMES.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
double boardSize = Math.min(width, height) - 4;
double boardX = (width - boardSize) / 2.0;
double boardY = (height - boardSize) / 2.0;
g2d.setColor(NEO_BLACK);
g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardSize, boardSize, 2, 2));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardSize, boardSize, 2, 2));
// four by four rather than eight by eight: at toolbox size eight rows fill in to a block
Color[] rainbow = new Color[] {
Color.decode("#FF3333"),
Color.decode("#FFD700"),
Color.decode("#00E676"),
Color.decode("#00E5FF"),
Color.decode("#E040FB"),
Color.decode("#FF6B35")
};
int order = 4;
double pitch = boardSize / order;
double dotSize = Math.max(2.0, pitch * 0.55);
for (int i = 0; i < order * order; i++) {
double dotX = boardX + (i % order + 0.5) * pitch - dotSize / 2.0;
double dotY = boardY + (i / order + 0.5) * pitch - dotSize / 2.0;
g2d.setColor(rainbow[i % rainbow.length]);
g2d.fill(new Ellipse2D.Double(dotX, dotY, dotSize, dotSize));
}
}
/**
* One line of silkscreen, positioned by its left edge and vertical centre from the middle of the
* board, with y running down the drawing rather than up as Eagle records it.
*/
static class Silk {
private final double xMm;
private final double yMm;
private final String text;
private final Font font;
Silk(double xMm, double yMm, String text, Font font) {
this.xMm = xMm;
this.yMm = yMm;
this.text = text;
this.font = font;
}
public double getXMm() { return xMm; }
public double getYMm() { return yMm; }
public String getText() { return text; }
public Font getFont() { return font; }
}
}
@@ -23,22 +23,20 @@ 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.components.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -51,49 +49,22 @@ 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 {
@ComponentDescriptor(name = "NeoPixel Ring", 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 AbstractAddressableLedBoard {
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"};
// The inherited solder pad footprint is sized for a breakout board rather than a ring.
public static Size PAD_EDGE_INSET = new Size(1.4d, SizeUnit.mm);
// Measured from the pad's edge, so the gap stays the same if the pad footprint changes.
public static Size PAD_LABEL_GAP = new Size(0.4d, SizeUnit.mm);
private RingSize ringSize = RingSize._16_LED;
@@ -114,39 +85,63 @@ public class WS2812BRing extends AbstractMakerBoard {
invalidateCache();
}
// Every ring is sold in each package, so neither half identifies what to buy on its own.
@Override
protected String getVariantLabel() {
RingSize ringSize = getRingSize();
return ringSize == null ? null : ringSize + ", " + getLedType();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
String[] padNames = ringSize.getPadNames();
if (index >= 0 && index < padNames.length) {
return padNames[index];
}
return Integer.toString(index + 1);
}
/** The circle the LED centres sit on: the midpoint between the inner and outer rims. */
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;
}
/**
* Pads sit out towards the rim rather than on the LED circle. This has to be the one place the
* radius is decided: {@link #updateControlPoints()} offsets every pad from the first by it and
* {@link #getCenter()} works backwards from the first pad, so the two disagreeing would draw the
* body away from its own control points.
*/
private double getPadRadius() {
double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
return outerR - PAD_EDGE_INSET.convertToPixels();
}
/**
* Each pad sits in a gap between two consecutive LEDs, the gaps {@link RingSize} records. The
* sequence is anchored at the gap just past the bottom of the ring and runs clockwise; where it
* starts is arbitrary, since the component rotates.
*/
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;
int ledCount = ringSize.getLedCount();
double gap = ringSize.getPadGapIndex(index) + ledCount / 2.0 + 0.5;
return 2 * Math.PI * gap / ledCount - Math.PI / 2.0;
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double midR = getMidRadius();
double padR = getPadRadius();
double theta0 = getPadAngle(0);
double[][] relativeOffsets = new double[4][2];
for (int i = 0; i < 4; i++) {
int padCount = ringSize.getPadNames().length;
double[][] relativeOffsets = new double[padCount][2];
for (int i = 0; i < padCount; 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));
relativeOffsets[i][0] = padR * (Math.cos(theta) - Math.cos(theta0));
relativeOffsets[i][1] = padR * (Math.sin(theta) - Math.sin(theta0));
}
rotatePoints(firstPoint, relativeOffsets);
@@ -154,10 +149,10 @@ public class WS2812BRing extends AbstractMakerBoard {
private Point2D getCenter() {
Point2D p0 = controlPoints[0];
double midR = getMidRadius();
double padR = getPadRadius();
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));
// unrotated, the centre sits one pad radius back along the first pad's angle
return new Point2D.Double(p0.getX() - padR * Math.cos(theta0), p0.getY() - padR * Math.sin(theta0));
}
@Override
@@ -189,9 +184,7 @@ public class WS2812BRing extends AbstractMakerBoard {
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);
@@ -209,58 +202,49 @@ public class WS2812BRing extends AbstractMakerBoard {
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;
double ledR = getMidRadius();
double ledSize = RGB_LED_SIZE.convertToPixels();
Color[] ledColors = getLedColors(ledCount);
LedType ledType = getLedType();
// Draw all LEDs around the circular ring
// each package is turned to face out along its own radius, the way it is mounted
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));
MakerBoardPainter.drawAddressableLed(g2d, cx + ledR * Math.cos(angle),
cy + ledR * Math.sin(angle), ledSize, ledColors[i % ledColors.length],
angle + Math.PI / 2.0, ledType);
}
// 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);
// Names run out along their own radius, turned with the pad. A pad sits in the gap between
// two consecutive LEDs, and that is the only room a name has: square to the board it would
// lie across the LEDs either side.
g2d.setColor(SILK_COLOR);
g2d.setFont(PIN_FONT);
double labelR =
getPadRadius() - PAD_SIZE.convertToPixels() / 2.0 - PAD_LABEL_GAP.convertToPixels();
String[] padNames = ringSize.getPadNames();
for (int i = 0; i < padNames.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty()) {
continue;
}
double angle = getPadAngle(i);
double textX = cx + labelR * Math.cos(angle);
double textY = cy + labelR * Math.sin(angle);
// 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);
// the turn maps the text advance direction onto the outward radius, so RIGHT alignment ends
// the name at the pad
AffineTransform oldLabelTx = g2d.getTransform();
g2d.rotate(angle, textX, textY);
StringUtils.drawCenteredText(g2d, label, textX, textY, HorizontalAlignment.RIGHT,
VerticalAlignment.CENTER);
g2d.setTransform(oldLabelTx);
}
}
g2d.setTransform(oldTx);
// Draw the 4 solder pads directly on the circular ring PCB
drawSolderPads(g2d, 0, 4, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, ringSize.getPadNames().length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -301,4 +285,58 @@ public class WS2812BRing extends AbstractMakerBoard {
g2d.fill(new Ellipse2D.Double(lx - dotR, ly - dotR, dotR * 2, dotR * 2));
}
}
public enum RingSize {
// Dimensioned in inches, so both diameters are exact conversions rather than roundings. LEDs
// sit on the midpoint between the rims, which misses the measured LED circle by 0.64 mm on the
// 12-LED ring and 0.51 mm on the 24 rather than carrying a third diameter per ring.
//
// Each ring lists its pads in order around the circle with the number of LEDs following each
// one. The final figure wraps back to the first pad, so the gaps must sum to the LED count;
// the constructor checks that.
_12_LED("12 LEDs", 12, 36.83, 23.368,
new String[] {"OUT", "IN", "GND", "PWR"},
new int[] {2, 4, 2, 4}),
_16_LED("16 LEDs", 16, 44.45, 31.75,
new String[] {"IN", "OUT", "G_1", "G_2", "V+_1", "V+_2"},
new int[] {2, 5, 1, 4, 1, 3}),
_24_LED("24 LEDs", 24, 65.532, 52.324,
new String[] {"OUT", "IN", "G_1", "G_2", "PWR_1", "PWR_2"},
new int[] {2, 8, 2, 2, 2, 8});
private final String label;
private final int ledCount;
private final double outerDiameterMm;
private final double innerDiameterMm;
private final String[] padNames;
private final int[] padGapIndex;
RingSize(String label, int ledCount, double outerDiameterMm, double innerDiameterMm,
String[] padNames, int[] ledsAfterPad) {
this.label = label;
this.ledCount = ledCount;
this.outerDiameterMm = outerDiameterMm;
this.innerDiameterMm = innerDiameterMm;
this.padNames = padNames;
this.padGapIndex = new int[padNames.length];
int gap = 0;
for (int i = 0; i < padNames.length; i++) {
padGapIndex[i] = gap;
gap += ledsAfterPad[i];
}
if (gap != ledCount) {
throw new IllegalArgumentException(
label + ": pad gaps sum to " + gap + " but the ring carries " + ledCount + " LEDs");
}
}
@Override public String toString() { return label; }
public int getLedCount() { return ledCount; }
public double getOuterDiameterMm() { return outerDiameterMm; }
public double getInnerDiameterMm() { return innerDiameterMm; }
public String[] getPadNames() { return padNames; }
/** Which gap between LEDs the pad at {@code index} sits in, counted from the first pad. */
public int getPadGapIndex(int index) { return padGapIndex[index]; }
}
}
@@ -23,7 +23,6 @@ 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;
@@ -32,12 +31,10 @@ 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.components.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -49,30 +46,40 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "NeoPixel Stick (8x WS2812B)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "8-LED Addressable RGB WS2812B NeoPixel Stick",
// instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
public class WS2812BStick extends AbstractMakerBoard {
@ComponentDescriptor(name = "NeoPixel Stick", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "8-LED Addressable RGB WS2812B NeoPixel Stick",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BStick extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
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 Font PIN_LABEL_FONT = new Font("SansSerif", Font.BOLD, 9);
public static Size BOARD_WIDTH = new Size(51.1d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(10.22d, SizeUnit.mm);
public static Size BOARD_WIDTH = new Size(53.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(10.2d, SizeUnit.mm);
// Surface pads flush with the cut end, so how far in their centres sit is half their width.
// The width is the reach inward from the edge; the length has to fit the 0.1 inch pad pitch.
public static Size PAD_WIDTH = new Size(3.0d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.5d, SizeUnit.mm);
// Air between a pad and the nearest package. Spreading the row to the pads' inner edge instead
// leaves the two touching, which draws as copper against plastic with no board between.
public static Size PAD_CLEARANCE = new Size(0.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(1.0d, SizeUnit.in);
public static Size MOUNTING_HOLE_TOP_OFFSET = new Size(2.0d, SizeUnit.mm);
public static final int LED_COUNT = 8;
// Both grounds on an end are the same net, numbered only to keep the node names distinct; the
// silkscreen prints "GND" for all four.
private static final String[] PIN_NAMES = {
"DIN", "+5V (In)", "GND (In)", "GND (In)",
"DOUT", "+5V (Out)", "GND (Out)", "GND (Out)"
"GND_1", "DIN", "+5V_1", "GND_2",
"GND_3", "DOUT", "+5V_2", "GND_4"
};
public WS2812BStick() {
@@ -81,6 +88,11 @@ public class WS2812BStick extends AbstractMakerBoard {
updateControlPoints();
}
@Override
protected String getVariantLabel() {
return getLedType().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
@@ -93,33 +105,66 @@ public class WS2812BStick extends AbstractMakerBoard {
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double w = BOARD_WIDTH.convertToPixels();
// the columns are flush against their own ends, so what separates them is the board less one
// whole pad
double columnSpacing = BOARD_WIDTH.convertToPixels() - PAD_WIDTH.convertToPixels();
// 8 pins total:
// Left input header (4 pins): 0 = DIN, 1 = +5V, 2 = GND, 3 = GND
// Right output header (4 pins): 4 = DOUT, 5 = +5V, 6 = GND, 7 = GND
double[][] relativeOffsets = new double[][] {
{ 0, 0 },
{ 0, spacing },
{ 0, spacing * 2 },
{ 0, spacing * 3 },
{ w - 20, 0 },
{ w - 20, spacing },
{ w - 20, spacing * 2 },
{ w - 20, spacing * 3 }
};
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < 4; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
relativeOffsets[4 + i][0] = columnSpacing;
relativeOffsets[4 + i][1] = i * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
}
/** The first pad column is flush with this edge, so its centres sit half a pad inboard. */
private double getBoardX(double x) {
return x - PAD_WIDTH.convertToPixels() / 2.0;
}
/** Derived so that the four-pad column is centred across the width. */
private double getBoardY(double y) {
double padSpan = 3 * PIN_SPACING.convertToPixels();
return y - (BOARD_HEIGHT.convertToPixels() - padSpan) / 2.0;
}
/**
* The row is spread evenly between the inner edges of the two pad columns rather than packed
* edge to edge, and sits low on the board: the mounting holes take the top, so the packages are
* centred in what is left below them.
*/
Point2D getLedCentre(int index) {
Point2D p0 = controlPoints[0];
double boardX = getBoardX(p0.getX());
double boardY = getBoardY(p0.getY());
double ledSize = RGB_LED_SIZE.convertToPixels();
double inset = PAD_WIDTH.convertToPixels() + PAD_CLEARANCE.convertToPixels();
double fieldStart = boardX + inset;
double fieldEnd = boardX + BOARD_WIDTH.convertToPixels() - inset;
double pitch = (fieldEnd - fieldStart - ledSize) / (LED_COUNT - 1);
return new Point2D.Double(fieldStart + ledSize / 2.0 + index * pitch,
(getHoleCentre(false).getY() + MOUNTING_HOLE_SIZE.convertToPixels() / 2.0 + boardY
+ BOARD_HEIGHT.convertToPixels()) / 2.0);
}
/** The two holes sit a fixed span apart astride the board's centre. */
Point2D getHoleCentre(boolean right) {
Point2D p0 = controlPoints[0];
double offset = MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
return new Point2D.Double(
getBoardX(p0.getX()) + BOARD_WIDTH.convertToPixels() / 2.0 + (right ? offset : -offset),
getBoardY(p0.getY()) + MOUNTING_HOLE_TOP_OFFSET.convertToPixels());
}
@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();
return new RoundRectangle2D.Double(x - 10, y - 10, boardW, boardH, 4, 4);
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
BOARD_WIDTH.convertToPixels(), BOARD_HEIGHT.convertToPixels(), 4, 4);
}
@Override
@@ -138,11 +183,6 @@ public class WS2812BStick extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - 10;
double boardY = y - 10;
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
@@ -157,56 +197,30 @@ public class WS2812BStick extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// 8x 5050 SMD WS2812B RGB LEDs (centered between left and right pin areas)
double spacing = PIN_SPACING.convertToPixels();
double ledAreaStartX = boardX + 68;
double ledAreaEndX = boardX + boardW - 68;
double ledPitch = (ledAreaEndX - ledAreaStartX) / 7.0;
for (int i = 0; i < 8; i++) {
double lx = ledAreaStartX + i * ledPitch;
double ly = boardY + boardH / 2.0;
// 5050 White Package (28x28)
g2d.setColor(LED_PACKAGE);
g2d.fill(new RoundRectangle2D.Double(lx - 14, ly - 14, 28, 28, 3, 3));
g2d.setColor(LED_BORDER);
g2d.draw(new RoundRectangle2D.Double(lx - 14, ly - 14, 28, 28, 3, 3));
// Circular milky phosphor/lens
g2d.setColor(LED_DIFFUSER);
g2d.fill(new Ellipse2D.Double(lx - 10, ly - 10, 20, 20));
g2d.setColor(DIFFUSER_BORDER);
g2d.draw(new Ellipse2D.Double(lx - 10, ly - 10, 20, 20));
// Internal tiny IC dot
g2d.setColor(CHIP_DOT_COLOR);
g2d.fill(new Rectangle2D.Double(lx - 2.5, ly - 2.5, 5, 5));
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (boolean right : new boolean[] {false, true}) {
Point2D hole = getHoleCentre(right);
MakerBoardPainter.drawMountingHole(g2d, hole.getX(), hole.getY(), holeSize);
}
// Silk Screen Pin Labels — all 4 pins labeled on both sides with clean spacing
g2d.setColor(Color.WHITE);
g2d.setFont(PIN_LABEL_FONT);
// Left Input Header Labels (DIN, 5V, GND, GND)
String[] inLabels = {"DIN", "5V", "GND", "GND"};
for (int i = 0; i < 4; i++) {
StringUtils.drawCenteredText(g2d, inLabels[i], boardX + 23, y + i * spacing,
HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
}
// Right Output Header Labels (DOUT, 5V, GND, GND)
String[] outLabels = {"DOUT", "5V", "GND", "GND"};
for (int i = 0; i < 4; i++) {
StringUtils.drawCenteredText(g2d, outLabels[i], boardX + boardW - 23, y + i * spacing,
HorizontalAlignment.RIGHT, VerticalAlignment.CENTER);
// The packages go down before the pads, so the row starts at the pads' inner edge rather than
// under them. The pad names are printed on the back of the real board.
double ledSize = RGB_LED_SIZE.convertToPixels();
Color[] ledColors = getLedColors(LED_COUNT);
LedType ledType = getLedType();
for (int i = 0; i < LED_COUNT; i++) {
Point2D led = getLedCentre(i);
MakerBoardPainter.drawAddressableLed(g2d, led.getX(), led.getY(), ledSize, ledColors[i], 0,
ledType);
}
}
g2d.setTransform(oldTx);
// Draw input and output header pins
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
// The stick ships bare and is wired by soldering, so it carries surface pads at the cut ends
// rather than the plated holes the ring and the jewel have.
drawSurfacePads(g2d, 0, controlPoints.length, PAD_WIDTH, PAD_LENGTH, outlineMode,
drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -239,11 +253,9 @@ public class WS2812BStick extends AbstractMakerBoard {
double dotX = rectX + margin + i * step;
double dotY = height / 2.0 - dotSize / 2.0;
// 5050 package mini white backing
g2d.setColor(Color.WHITE);
g2d.fill(new Rectangle2D.Double(dotX - 0.5, dotY - 0.5, dotSize + 1, dotSize + 1));
// RGB LED dot
g2d.setColor(rainbow[i]);
g2d.fill(new Ellipse2D.Double(dotX, dotY, dotSize, dotSize));
}
@@ -0,0 +1,373 @@
/*
*
* 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.FontMetrics;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
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.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
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.presenter.CalcUtils;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "NeoPixel Strip", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Addressable RGB WS2812B LED Strip (30, 60 or 144 LEDs per metre)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BStrip extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
public static Color TAPE_WHITE = Color.decode("#F2F2F2");
// White tape is the one light-coloured board in this family, so the ink follows the board
// colour rather than being fixed.
public static Color SILK_DARK_COLOR = Color.decode("#333333");
// The uncut pad is a pill lying along the tape with the cut through its middle, so a cut end
// keeps half of one. Along the tape it is this nominal where there is room and otherwise as
// much as the density leaves, which at 144/m brings it down to its own width.
public static Size PAD_WIDTH = new Size(1.5d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(3.0d, SizeUnit.mm);
// Air left between a pad and the nearest package, and between a pad and its printed name.
public static Size PAD_CLEARANCE = new Size(0.2d, SizeUnit.mm);
public static Size LABEL_GAP = new Size(0.5d, SizeUnit.mm);
/**
* A cut length of tape rather than a board, so the count is a free number. The ceiling is about
* the shape rather than the control-point array, which stays at six: 144 LEDs of 30/m tape is
* already most of five metres.
*/
public static final int MIN_LED_COUNT = 1;
public static final int MAX_LED_COUNT = 144;
public static final int DEFAULT_LED_COUNT = 8;
// Both cuts carry all three lines, which is what makes tape chainable: the rails run the whole
// length as continuous copper and only the data pad changes sense. The rails are numbered to
// keep the node names distinct; the tape prints them +5V and GND at both ends.
public static final String[] PIN_NAMES =
new String[] {"+5V_1", "DIN", "GND_1", "+5V_2", "DOUT", "GND_2"};
// The bare designations the tape prints: the rails are not numbered on the part, and the data
// pad is printed two letters whichever end it is.
public static final String[] SILK_NAMES =
new String[] {"+5V", "DI", "GND", "+5V", "DO", "GND"};
private Density density = Density._60;
private int ledCount = DEFAULT_LED_COUNT;
public WS2812BStrip() {
super();
this.bodyColor = TAPE_WHITE;
updateControlPoints();
}
@EditableProperty(name = "Density")
public Density getDensity() {
if (density == null) {
density = Density._60;
}
return density;
}
public void setDensity(Density density) {
this.density = density;
updateControlPoints();
invalidateCache();
}
@EditableProperty(name = "LEDs")
public int getLedCount() {
if (ledCount < MIN_LED_COUNT) {
ledCount = DEFAULT_LED_COUNT;
}
return ledCount;
}
public void setLedCount(int ledCount) {
this.ledCount = Math.max(MIN_LED_COUNT, Math.min(MAX_LED_COUNT, ledCount));
updateControlPoints();
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getDensity() + ", " + getLedCount() + " LEDs, " + getLedType();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected String getSilkPinLabel(int index) {
if (index >= 0 && index < SILK_NAMES.length) {
return SILK_NAMES[index];
}
return super.getSilkPinLabel(index);
}
/** The lettering that contrasts with the tape colour the user has chosen. */
private Color getSilkInk() {
return CalcUtils.calculateLuminance(bodyColor) < 128d ? SILK_COLOR : SILK_DARK_COLOR;
}
private double getPitch() {
return new Size(getDensity().getPitchMm(), SizeUnit.mm).convertToPixels();
}
private double getTapeWidth() {
return new Size(getDensity().getWidthMm(), SizeUnit.mm).convertToPixels();
}
/** Linear in the count rather than proportional to it, since the two cut ends do not scale. */
private double getTapeLength() {
return (getLedCount() - 1) * getPitch() + 2 * getLeadIn();
}
/** The span a column of three pads occupies across the tape. */
private static double getPadRunLength() {
return 2 * PIN_SPACING.convertToPixels();
}
/**
* Tape is a repeating cell one pitch long with its package in the middle and a cut falling on a
* cell boundary, so this is half a pitch. That is what makes a length exactly as many pitches
* long as it has LEDs, and keeps the pitch across the joint when two are butted together.
*/
double getLeadIn() {
return getPitch() / 2.0;
}
/**
* Half of the pill it was cut from: the nominal length where the density leaves room, otherwise
* as much as fits between the cut and the first package, never less than its own width.
*/
double getPadDepth() {
return getPadLength() / 2.0;
}
double getPadLength() {
double room = 2 * (getLeadIn() - RGB_LED_SIZE.convertToPixels() / 2.0
- PAD_CLEARANCE.convertToPixels());
return Math.max(PAD_WIDTH.convertToPixels(),
Math.min(PAD_LENGTH.convertToPixels(), room));
}
/** Control point zero sits here, on the cut, which runs through the pad. */
private double getBoardX(double x) {
return x;
}
/** Each column of pads is centred across the width, so the first sits above centre. */
private double getBoardY(double y) {
return y - (getTapeWidth() - getPadRunLength()) / 2.0;
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
// Spacing the three rows by the pad pitch rather than by a fraction of the width keeps them
// apart: fractions of a 10 mm tape would leave them almost edge to edge.
//
// The cut runs through the middle of a pad, so a control point sits on the cut itself and the
// two columns are a whole tape length apart. That is also what makes two lengths chain.
double columnSpacing = getTapeLength();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < 3; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
relativeOffsets[3 + i][0] = columnSpacing;
relativeOffsets[3 + i][1] = i * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), getTapeLength(),
getTapeWidth(), 4, 4);
}
/**
* Names go in the bare strip between the pad and the first package, which the density decides:
* 12.7 mm at 30/m, 4.3 mm at 60/m and 0.2 mm at 144/m. They are left off where they do not fit
* rather than shrunk to suit the worst case.
*/
private void drawCutEndLabels(Graphics2D g2d, double boardX, double boardY) {
double room = getLeadIn() - RGB_LED_SIZE.convertToPixels() / 2.0 - getPadDepth();
double gap = LABEL_GAP.convertToPixels();
double spacing = PIN_SPACING.convertToPixels();
double firstRow = boardY + (getTapeWidth() - getPadRunLength()) / 2.0;
g2d.setFont(PIN_ROW_FLAT_FONT);
g2d.setColor(getSilkInk());
FontMetrics metrics = g2d.getFontMetrics();
for (int i = 0; i < PIN_NAMES.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty() || metrics.stringWidth(label) + gap > room) {
continue;
}
boolean input = i < 3;
double x = input ? boardX + getPadDepth() + gap
: boardX + getTapeLength() - getPadDepth() - gap;
StringUtils.drawCenteredText(g2d, label, x, firstRow + (i % 3) * spacing,
input ? HorizontalAlignment.LEFT : HorizontalAlignment.RIGHT,
VerticalAlignment.CENTER);
}
}
@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 boardX = getBoardX(x);
double boardY = getBoardY(y);
double tapeH = getTapeWidth();
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) {
drawCutEndLabels(g2d, boardX, boardY);
double pitch = getPitch();
double ledSize = RGB_LED_SIZE.convertToPixels();
double firstCenter = boardX + getLeadIn();
int count = getLedCount();
Color[] colors = getLedColors(count);
LedType ledType = getLedType();
for (int i = 0; i < count; i++) {
MakerBoardPainter.drawAddressableLed(g2d, firstCenter + i * pitch, boardY + tapeH / 2.0,
ledSize, colors[i], 0, ledType);
}
}
g2d.setTransform(oldTx);
double padWidth = PAD_WIDTH.convertToPixels();
drawBisectedPads(g2d, 0, 3, padWidth, getPadLength(), true, outlineMode, drawingObserver);
drawBisectedPads(g2d, 3, 3, padWidth, getPadLength(), false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(TAPE_WHITE);
g2d.fill(new RoundRectangle2D.Double(1, height / 2.0 - 5, width - 2, 10, 2, 2));
g2d.setColor(Color.GRAY);
g2d.draw(new RoundRectangle2D.Double(1, height / 2.0 - 5, width - 2, 10, 2, 2));
Color[] colors = new Color[] {Color.decode("#FFEE00"), Color.decode("#FF1122"),
Color.decode("#2255FF"), Color.decode("#22DD44")};
for (int i = 0; i < 4; i++) {
g2d.setColor(colors[i]);
g2d.fillRect(3 + i * (width - 6) / 4, (int) (height / 2.0 - 3), 5, 6);
}
}
/**
* Tape is sold by LEDs per metre, so the pitch is derived rather than stored. Width does not
* follow and is a stated norm rather than a measurement: 10 mm at 30 and 60, 12 mm at 144,
* where the package leaves no room for the conductor beside it.
*/
public enum Density {
_30("30 LED/m", 30, 10.0d),
_60("60 LED/m", 60, 10.0d),
_144("144 LED/m", 144, 12.0d);
private final String label;
private final int ledsPerMetre;
private final double widthMm;
Density(String label, int ledsPerMetre, double widthMm) {
this.label = label;
this.ledsPerMetre = ledsPerMetre;
this.widthMm = widthMm;
}
@Override public String toString() { return label; }
public int getLedsPerMetre() { return ledsPerMetre; }
public double getWidthMm() { return widthMm; }
/** Centre-to-centre LED spacing: one metre shared out between that many LEDs. */
public double getPitchMm() { return 1000.0d / ledsPerMetre; }
}
}
@@ -64,7 +64,7 @@ public class ArduinoMega extends AbstractMakerBoard {
// Pin names in sequence (98 pins total)
public static final String[] PIN_NAMES = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Low A0..A7 (8..15)
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7",
// Analog High A8..A15 (16..23)
@@ -72,7 +72,7 @@ public class ArduinoMega extends AbstractMakerBoard {
// Digital Low D0..D7 (24..31)
"D0 (RX0)", "D1 (TX0)", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)",
// Digital High D8..D13, GND, AREF, SDA, SCL (32..41)
"D8 (~)", "D9 (~)", "D10 (~)", "D11 (~)", "D12 (~)", "D13 (~)", "GND3", "AREF", "SDA", "SCL",
"D8 (~)", "D9 (~)", "D10 (~)", "D11 (~)", "D12 (~)", "D13 (~)", "GND_3", "AREF", "SDA", "SCL",
// Communication Header D14..D21 (42..49)
"D14 (TX3)", "D15 (RX3)", "D16 (TX2)", "D17 (RX2)", "D18 (TX1)", "D19 (RX1)", "D20 (SDA)", "D21 (SCL)",
// Double Digital 2x18 Header (50..85: D22..D53, GNDx2, 5Vx2)
@@ -294,7 +294,6 @@ public class ArduinoMega extends AbstractMakerBoard {
Composite oldComposite = applyAlpha(g2d, componentState);
// Draw PCB body
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
@@ -305,7 +304,7 @@ public class ArduinoMega extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// Mounting holes (6 mounting holes from technical design)
// six, from the technical design
double holeDiameter = new Size(0.12d, SizeUnit.in).convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.55d, SizeUnit.in).convertToPixels(), boardY + new Size(2.0d, SizeUnit.in).convertToPixels(), holeDiameter); // (550, 100 mils)
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.6d, SizeUnit.in).convertToPixels(), boardY + new Size(0.1d, SizeUnit.in).convertToPixels(), holeDiameter); // (600, 2000 mils)
@@ -343,11 +342,9 @@ public class ArduinoMega extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "RST", btnX + btnW / 2.0,
btnY + btnH + new Size(1.0d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Arduino Infinity Logo
MakerBoardLogos.drawArduinoLogo(g2d, boardX + new Size(1.2d, SizeUnit.in).convertToPixels(),
boardY + new Size(0.4d, SizeUnit.in).convertToPixels());
// Silkscreen text & branding
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT_LARGE);
StringUtils.drawCenteredText(g2d, "ARDUINO", boardX + new Size(1.44d, SizeUnit.in).convertToPixels(),
@@ -403,7 +400,6 @@ public class ArduinoMega extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw header pins with continuity tracking
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
@@ -416,38 +412,32 @@ public class ArduinoMega extends AbstractMakerBoard {
double boardW = width - boardX - 2;
double boardH = height - 6;
// USB Type-B Jack (metallic silver, top left protruding)
double usbW = 7;
double usbH = 8;
double usbX = 1;
double usbY = boardY + 2;
// DC Power Jack (dark body, bottom left protruding)
double dcW = 7;
double dcH = 6;
double dcX = 1;
double dcY = boardY + boardH - dcH - 3;
// Board PCB
g2d.setColor(ARDUINO_TEAL);
g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
g2d.setColor(ARDUINO_TEAL.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
// Draw USB Jack
g2d.setColor(USB_METAL_COLOR);
g2d.fill(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2));
g2d.setColor(METAL_SHIELD_BORDER);
g2d.draw(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2));
// Draw DC Jack
g2d.setColor(IC_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2));
g2d.setColor(Color.BLACK);
g2d.draw(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2));
// Arduino Infinity logo
double scale = 14.0 / 95.56;
double logoW = 95.56 * scale;
double logoH = 45.33 * scale;
@@ -455,7 +445,6 @@ public class ArduinoMega extends AbstractMakerBoard {
double logoY = boardY + 3.5;
MakerBoardLogos.drawArduinoLogo(g2d, logoX, logoY, scale);
// MEGA text below logo
g2d.setColor(SILK_COLOR);
int fontSize = Math.max(6, (int) Math.round(boardH * 0.26));
g2d.setFont(ICON_FONT.deriveFont((float) fontSize));
@@ -76,63 +76,11 @@ public class ArduinoNano extends AbstractMakerBoard {
public static final int PINS_PER_ROW = 15;
// Ordered by label so the Version drop-down reads alphabetically. XStream serializes enum
// constants by name, so the order here is free to change without affecting existing .diy files.
public enum NanoVersion {
CLASSIC("Nano (ATmega328)", "NANO", "m328P", null),
NANO_33_BLE("Nano 33 BLE", "33 BLE", null, "NINA-B306"),
NANO_33_BLE_SENSE("Nano 33 BLE Sense", "33 BLE SENSE", null, "NINA-B306"),
NANO_33_IOT("Nano 33 IoT", "33 IOT", "SAMD21", "NINA-W102"),
NANO_ESP32("Nano ESP32", "NANO ESP32", null, "NORA-W106"),
EVERY("Nano Every", "EVERY", "m4809", "SAMD11"),
NANO_R4("Nano R4", "NANO R4", "RA4M1", "QWIIC"),
// Silkscreened "CONNECT" rather than the full name, which does not fit between the two rows of
// pad names; the RP2040 package right below it carries the part number
NANO_RP2040_CONNECT("Nano RP2040 Connect", "CONNECT", "RP2040", "NINA-W102");
private final String label;
private final String silkLabel;
private final String mcuLabel;
private final String moduleLabel;
NanoVersion(String label, String silkLabel, String mcuLabel, String moduleLabel) {
this.label = label;
this.silkLabel = silkLabel;
this.mcuLabel = mcuLabel;
this.moduleLabel = moduleLabel;
}
public String getSilkLabel() {
return silkLabel;
}
/**
* Marking of the separately visible MCU package, or null when the MCU is a die inside the
* module rather than its own part on the board.
*/
public String getMcuLabel() {
return mcuLabel;
}
/**
* Marking of the part occupying the end of the board opposite the USB jack, where the classic
* Nano has its ICSP header. Null on the classic Nano, which has the header instead.
*/
public String getModuleLabel() {
return moduleLabel;
}
@Override
public String toString() {
return label;
}
}
public static final String[] PIN_NAMES = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13",
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13",
// ICSP (30..35)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP"
};
@@ -145,26 +93,26 @@ public class ArduinoNano extends AbstractMakerBoard {
// Nano Every (ABX00028): PWM on D3, D5, D6, D9, D10 only, and D13 is SCK.
public static final String[] PIN_NAMES_EVERY = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
};
// Nano 33 IoT (ABX00027): the SAMD21 adds PWM on D2 relative to the Every.
public static final String[] PIN_NAMES_33_IOT = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2 (~)", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
};
// Nano 33 BLE and BLE Sense (ABX00030): the nRF52840 routes PWM to every digital pin, D13
// included.
public static final String[] PIN_NAMES_33_BLE = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (~, SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (~, SCK)"
};
// Nano RP2040 Connect (ABX00053): every RP2040 GPIO has a PWM slice. Note REC (BOOTSEL) where
@@ -173,17 +121,17 @@ public class ArduinoNano extends AbstractMakerBoard {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "REC", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13 (~, SCK)"
"VIN", "GND_2", "REC", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13 (~, SCK)"
};
// Nano R4 (ABX00142): the Renesas RA4M1 board. Same PWM set as the classic, but D4 / D5 double as
// the CAN pins, A1-A3 reach the on-chip OPAMP, A0 is the DAC, and BOOT replaces the second RESET.
public static final String[] PIN_NAMES_R4 = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4 (CAN TX)", "D5 (~, CAN RX)", "D6 (~)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4 (CAN TX)", "D5 (~, CAN RX)", "D6 (~)",
"D7", "D8", "D9 (~)", "D10 (~, CS)", "D11 (~, MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "BOOT", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3 (OPAMP OUT)",
"VIN", "GND_2", "BOOT", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3 (OPAMP OUT)",
"A2 (OPAMP -)", "A1 (OPAMP +)", "A0 (DAC)", "AREF", "3.3V", "D13 (SCK)",
// Qwiic socket (30), a single point at the centre of the connector so a wire can land on it
"QWIIC"
@@ -194,11 +142,11 @@ public class ArduinoNano extends AbstractMakerBoard {
// number, which is what the ESP-IDF documentation refers to.
public static final String[] PIN_NAMES_ESP32 = new String[] {
// Left row (0..14, top to bottom)
"D1 (~, TX/GPIO43)", "D0 (~, RX/GPIO44)", "RST1", "GND1", "D2 (~, GPIO5)", "D3 (~, GPIO6)",
"D1 (~, TX/GPIO43)", "D0 (~, RX/GPIO44)", "RST1", "GND_1", "D2 (~, GPIO5)", "D3 (~, GPIO6)",
"D4 (~, GPIO7)", "D5 (~, GPIO8)", "D6 (~, GPIO9)", "D7 (~, GPIO10)", "D8 (~, GPIO17)",
"D9 (~, GPIO18)", "D10 (~, GPIO21)", "D11 (~, MOSI/GPIO38)", "D12 (~, MISO/GPIO47)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "B1", "VUSB", "A7 (~, GPIO14)", "A6 (~, GPIO13)", "A5 (~, SCL/GPIO12)",
"VIN", "GND_2", "B1", "VUSB", "A7 (~, GPIO14)", "A6 (~, GPIO13)", "A5 (~, SCL/GPIO12)",
"A4 (~, SDA/GPIO11)", "A3 (~, GPIO4)", "A2 (~, GPIO3)", "A1 (~, GPIO2)", "A0 (~, GPIO1)",
"B0", "3.3V", "D13 (~, SCK/GPIO48)"
};
@@ -262,6 +210,11 @@ public class ArduinoNano extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
private String[] getPinNames() {
switch (getVersion()) {
case EVERY:
@@ -391,9 +344,8 @@ public class ArduinoNano extends AbstractMakerBoard {
return relativeOffsets;
}
// ICSP header (2x3 pins, 30..35) flush with top edge:
// Outer row is 0.05" (10px) from top edge (-20px / -0.10" relative to Pin 0)
// Inner row is 0.15" (30px) from top edge (0px / 0.00" relative to Pin 0, aligned with Pin 0 & Pin 15)
// ICSP block flush with the top edge: the outer row 0.05" from it and the inner row
// 0.15", which puts the inner row level with pins 0 and 15.
double icspOuterY = -spacing; // -20px (-0.10")
double icspInnerY = 0; // 0px (0.00")
@@ -401,17 +353,11 @@ public class ArduinoNano extends AbstractMakerBoard {
double col1X = rowSpacing / 2.0; // 60px (0.30")
double col2X = rowSpacing / 2.0 + spacing; // 80px (0.40")
// Pin 1 (MISO at col2X, outer row)
relativeOffsets[30] = new double[] {col2X, icspOuterY};
// Pin 2 (5V_ICSP at col2X, inner row)
relativeOffsets[31] = new double[] {col2X, icspInnerY};
// Pin 3 (SCK at col1X, outer row)
relativeOffsets[32] = new double[] {col1X, icspOuterY};
// Pin 4 (MOSI at col1X, inner row)
relativeOffsets[33] = new double[] {col1X, icspInnerY};
// Pin 5 (RST_ICSP at col0X, outer row)
relativeOffsets[34] = new double[] {col0X, icspOuterY};
// Pin 6 (GND_ICSP at col0X, inner row)
relativeOffsets[35] = new double[] {col0X, icspInnerY};
return relativeOffsets;
@@ -480,7 +426,6 @@ public class ArduinoNano extends AbstractMakerBoard {
Composite oldComposite = applyAlpha(g2d, componentState);
// Draw PCB body
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
@@ -529,7 +474,6 @@ public class ArduinoNano extends AbstractMakerBoard {
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(-chipSize / 2.0, -chipSize / 2.0, chipSize, chipSize, 4, 4));
// Pin 1 dot
g2d.setColor(PIN_MARKER_COLOR);
g2d.fill(new Ellipse2D.Double(-chipSize / 2.0 + 3, -chipSize / 2.0 + 3, 3, 3));
@@ -583,7 +527,6 @@ public class ArduinoNano extends AbstractMakerBoard {
}
}
// Reset Button
double btnW = BUTTON_WIDTH.convertToPixels();
double btnH = BUTTON_LENGTH.convertToPixels();
double btnX = boardX + (boardW - btnW) / 2.0;
@@ -615,7 +558,6 @@ public class ArduinoNano extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw pins or solder pads
if (headers) {
drawPinHeader(g2d, 0, getHeaderPinCount(), outlineMode, drawingObserver);
} else if (getVersion() == NanoVersion.CLASSIC) {
@@ -637,15 +579,12 @@ public class ArduinoNano extends AbstractMakerBoard {
g2d.setColor(ARDUINO_TEAL.darker());
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
// ICSP header at top
g2d.setColor(HEADER_BODY_COLOR);
g2d.fill(new Rectangle2D.Double(width / 2.0 - 4, boardY, 8, 4));
// Mini USB at bottom
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect((int) (width / 2.0 - 4), (int) (boardY + boardH - 4), 8, 4);
// 45-degree rotated diamond IC chip in center
AffineTransform oldTx = g2d.getTransform();
g2d.translate(width / 2.0, boardY + boardH / 2.0 + 1);
g2d.rotate(Math.PI / 4.0);
@@ -655,11 +594,62 @@ public class ArduinoNano extends AbstractMakerBoard {
g2d.draw(new RoundRectangle2D.Double(-4, -4, 8, 8, 1, 1));
g2d.setTransform(oldTx);
// Pin strips on sides
g2d.setColor(PIN_COLOR);
for (int y = 5; y < height - 5; y += 3) {
g2d.fillRect(7, y, 2, 2);
g2d.fillRect(width - 9, y, 2, 2);
}
}
// Ordered by label so the Version drop-down reads alphabetically. XStream serializes enum
// constants by name, so the order here is free to change without affecting existing .diy files.
public enum NanoVersion {
CLASSIC("Nano (ATmega328)", "NANO", "m328P", null),
NANO_33_BLE("Nano 33 BLE", "33 BLE", null, "NINA-B306"),
NANO_33_BLE_SENSE("Nano 33 BLE Sense", "33 BLE SENSE", null, "NINA-B306"),
NANO_33_IOT("Nano 33 IoT", "33 IOT", "SAMD21", "NINA-W102"),
NANO_ESP32("Nano ESP32", "NANO ESP32", null, "NORA-W106"),
EVERY("Nano Every", "EVERY", "m4809", "SAMD11"),
NANO_R4("Nano R4", "NANO R4", "RA4M1", "QWIIC"),
// Silkscreened "CONNECT" rather than the full name, which does not fit between the two rows of
// pad names; the RP2040 package right below it carries the part number
NANO_RP2040_CONNECT("Nano RP2040 Connect", "CONNECT", "RP2040", "NINA-W102");
private final String label;
private final String silkLabel;
private final String mcuLabel;
private final String moduleLabel;
NanoVersion(String label, String silkLabel, String mcuLabel, String moduleLabel) {
this.label = label;
this.silkLabel = silkLabel;
this.mcuLabel = mcuLabel;
this.moduleLabel = moduleLabel;
}
public String getSilkLabel() {
return silkLabel;
}
/**
* Marking of the separately visible MCU package, or null when the MCU is a die inside the
* module rather than its own part on the board.
*/
public String getMcuLabel() {
return mcuLabel;
}
/**
* Marking of the part occupying the end of the board opposite the USB jack, where the classic
* Nano has its ICSP header. Null on the classic Nano, which has the header instead.
*/
public String getModuleLabel() {
return moduleLabel;
}
@Override
public String toString() {
return label;
}
}
}
@@ -61,24 +61,6 @@ public class ArduinoUno extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum ArduinoUnoVersion {
LEONARDO("Leonardo"),
REV3("UNO R3"),
R4_MINIMA("UNO R4 Minima"),
R4_WIFI("UNO R4 WiFi");
private String label;
private ArduinoUnoVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
private ArduinoUnoVersion version = ArduinoUnoVersion.REV3;
public static Size BOARD_WIDTH = new Size(68.6d, SizeUnit.mm);
@@ -93,13 +75,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// Pin names in sequence (44 pins total)
public static final String[] PIN_NAMES = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, ATmega328P)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP",
// Top-Left ICSP Header (38..43, ATmega16U2)
@@ -130,13 +112,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// ICSP header, and I2C is shared with D2 / D3 rather than living solely on A4 / A5.
public static final String[] PIN_NAMES_LEONARDO = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2 (SDA)", "D3 (~, SCL)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, ATmega32U4)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP"
};
@@ -159,13 +141,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// carries a 10-pin SWD debug connector in the top right corner, modelled as a single node.
public static final String[] PIN_NAMES_R4_MINIMA = new String[] {
// Power Header (0..7)
"BOOT", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"BOOT", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, RA4M1)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP",
// SWD Connector (38)
@@ -222,15 +204,20 @@ public class ArduinoUno extends AbstractMakerBoard {
public void setVersion(ArduinoUnoVersion version) {
this.version = version;
if (isR4()) {
this.bodyColor = ARDUINO_BLUE;
} else {
this.bodyColor = ARDUINO_TEAL;
// The two generations are different colours, but the board follows the version only while its
// colour is still one of ours; a colour the user chose survives a version switch.
if (ARDUINO_TEAL.equals(bodyColor) || ARDUINO_BLUE.equals(bodyColor)) {
this.bodyColor = isR4() ? ARDUINO_BLUE : ARDUINO_TEAL;
}
updateControlPoints();
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
/**
* True for the Renesas RA4M1 generation, which shares a USB-C connector and the RA4M1 in place of
* the ATmega328P.
@@ -442,7 +429,6 @@ public class ArduinoUno extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
// Board bounding origin
double boardX = x - new Size(1.1d, SizeUnit.in).convertToPixels();
double boardY = y - new Size(2.0d, SizeUnit.in).convertToPixels();
@@ -450,7 +436,6 @@ public class ArduinoUno extends AbstractMakerBoard {
Composite oldComposite = applyAlpha(g2d, componentState);
// Draw PCB body
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
@@ -461,7 +446,6 @@ public class ArduinoUno extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// Mounting holes
double holeDiameter = new Size(0.12d, SizeUnit.in).convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.6d, SizeUnit.in).convertToPixels(), boardY + new Size(0.1d, SizeUnit.in).convertToPixels(), holeDiameter);
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.55d, SizeUnit.in).convertToPixels(), boardY + new Size(2.0d, SizeUnit.in).convertToPixels(), holeDiameter);
@@ -544,7 +528,6 @@ public class ArduinoUno extends AbstractMakerBoard {
new Size(0.22d, SizeUnit.in).convertToPixels(), "ATmega328P");
}
// Reset Button near USB
double btnW = BUTTON_WIDTH.convertToPixels();
double btnH = BUTTON_LENGTH.convertToPixels();
double btnX = boardX + new Size(0.235d, SizeUnit.in).convertToPixels() - btnW / 2.0;
@@ -560,11 +543,9 @@ public class ArduinoUno extends AbstractMakerBoard {
double brandShiftY = getVersion() == ArduinoUnoVersion.R4_MINIMA
? new Size(24d, SizeUnit.mm).convertToPixels() : 0;
// Arduino Infinity Logo
MakerBoardLogos.drawArduinoLogo(g2d, boardX + new Size(1.26d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels(),
boardY + new Size(0.4411d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels() + brandShiftY);
// Silkscreen text & branding
g2d.setColor(SILK_COLOR);
double arduinoX = boardX + new Size(1.5d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels();
@@ -577,7 +558,6 @@ public class ArduinoUno extends AbstractMakerBoard {
g2d.setFont(SILK_FONT_LARGE);
StringUtils.drawCenteredText(g2d, subText, arduinoX, subTextY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Header silkscreen labels
g2d.setFont(SILK_FONT_SMALL);
// the header captions sit beyond the rotated pin names, which take up the space right next to
// the headers themselves
@@ -606,8 +586,7 @@ public class ArduinoUno extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw header pins with continuity tracking; the Minima's SWD point sits inside the connector
// body drawn above and gets no header pin of its own
// the Minima's SWD point sits inside the connector body drawn above and gets no header pin
int headerPinCount = getVersion() == ArduinoUnoVersion.R4_MINIMA
? controlPoints.length - 1 : controlPoints.length;
drawPinHeader(g2d, 0, headerPinCount, outlineMode, drawingObserver);
@@ -622,38 +601,32 @@ public class ArduinoUno extends AbstractMakerBoard {
double boardW = width - boardX - 2;
double boardH = height - 6;
// USB Type-B Jack (metallic silver, top left protruding)
double usbW = 7;
double usbH = 8;
double usbX = 1;
double usbY = boardY + 2;
// DC Power Jack (dark body, bottom left protruding)
double dcW = 7;
double dcH = 6;
double dcX = 1;
double dcY = boardY + boardH - dcH - 3;
// Board PCB
g2d.setColor(this.bodyColor);
g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
g2d.setColor(this.bodyColor.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
// Draw USB Jack
g2d.setColor(USB_METAL_COLOR);
g2d.fill(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2));
g2d.setColor(METAL_SHIELD_BORDER);
g2d.draw(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2));
// Draw DC Jack
g2d.setColor(IC_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2));
g2d.setColor(Color.BLACK);
g2d.draw(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2));
// Arduino Infinity logo
double scale = 15.0 / 95.56;
double logoW = 95.56 * scale;
double logoH = 45.33 * scale;
@@ -661,7 +634,6 @@ public class ArduinoUno extends AbstractMakerBoard {
double logoY = boardY + 3.5;
MakerBoardLogos.drawArduinoLogo(g2d, logoX, logoY, scale);
// UNO text below logo
g2d.setColor(SILK_COLOR);
String iconText = getVersion().toString().toUpperCase(Locale.ROOT).replace("UNO R4 ", "R4 ");
int fontSize = Math.max(5, (int) Math.round(boardH * 0.20));
@@ -670,4 +642,22 @@ public class ArduinoUno extends AbstractMakerBoard {
double textX = boardX + (boardW / 2.0) + 0.5;
StringUtils.drawCenteredText(g2d, iconText, textX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum ArduinoUnoVersion {
LEONARDO("Leonardo"),
REV3("UNO R3"),
R4_MINIMA("UNO R4 Minima"),
R4_WIFI("UNO R4 WiFi");
private String label;
private ArduinoUnoVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -62,25 +62,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum DevKitVersion {
DevKit_V1_30Pin("ESP32 DevKit V1 (30-Pin)"),
DevKitC_V4_38Pin("ESP32 DevKitC V4 (38-Pin)"),
ESP32_S3_DevKitC_44Pin("ESP32-S3 DevKitC-1 (44-Pin)"),
ESP32_C3_DevKitM_1("ESP32-C3 DevKitM-1 (30-Pin)"),
ESP32_C6_DevKitC_1("ESP32-C6 DevKitC-1 (32-Pin)");
private final String label;
DevKitVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
public static Size BOARD_WIDTH_30 = new Size(28.33d, SizeUnit.mm);
public static Size BOARD_LENGTH_30 = new Size(51.45d, SizeUnit.mm);
public static Size TOP_MARGIN_30 = new Size(6.7d, SizeUnit.mm);
@@ -354,6 +335,11 @@ public class ESP32DevKit extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
@EditableProperty(name = "Headers")
public boolean getHeaders() {
return headers;
@@ -373,9 +359,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
return "Pin " + (index + 1);
}
/**
* Calculates unrotated relative offsets of all control points for the current version.
*/
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
@@ -522,7 +505,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
// Determine the center X for the current version
double centerX;
if (getVersion() == DevKitVersion.ESP32_S3_DevKitC_44Pin || isRiscV()) {
centerX = x + ROW_SPACING_S3.convertToPixels() / 2.0;
@@ -554,7 +536,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
g2d.setColor(bodyColor);
g2d.fill(new Rectangle2D.Double(mainX, mainY, boardW, boardH));
// Antenna tab in #1e1e1e
g2d.setColor(ANTENNA_BG_COLOR);
g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH));
} else if (isRiscV()) {
@@ -572,7 +553,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
g2d.setColor(bodyColor);
g2d.fill(new Rectangle2D.Double(mainX, mainY, boardW, boardH));
// Antenna tab in #1e1e1e
g2d.setColor(ANTENNA_BG_COLOR);
g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH));
} else {
@@ -590,7 +570,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
g2d.setColor(bodyColor);
g2d.fill(new RoundRectangle2D.Double(mainX, mainY, mainW, mainH, 8, 8));
// Antenna tab in #1e1e1e
g2d.setColor(ANTENNA_BG_COLOR);
g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH));
}
@@ -657,7 +636,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
double btnRightX = rightHoleX - 14 - 0.5 * shift1mm - btnW;
drawButtons(g2d, btnLeftX, btnRightX, btnY, btnW, btnH, "EN", "BOOT");
// Silkscreen
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, "ESP32 DevKit V1", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
@@ -704,7 +682,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
MakerBoardPainter.drawMicroUsb(g2d, usbLeftX, usbY, usbW, usbH, "UART");
MakerBoardPainter.drawMicroUsb(g2d, usbRightX, usbY, usbW, usbH, "USB");
// Silkscreen
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, "ESP32-S3 DevKitC-1", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
@@ -759,7 +736,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
MakerBoardPainter.drawUsbC(g2d, mainX + boardW - usbEdgeDist - usbW / 2.0, usbY, usbW, usbH, "USB");
}
// Silkscreen
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, isC3 ? "ESP32-C3 DevKitM-1" : "ESP32-C6 DevKitC-1", centerX,
@@ -804,13 +780,11 @@ public class ESP32DevKit extends AbstractMakerBoard {
double btnRightX = mainX + mainW - 38 - btnW;
drawButtons(g2d, btnLeftX, btnRightX, btnY, btnW, btnH, "EN", "BOOT");
// Silkscreen
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, "ESP32 DevKitC V4", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
// Draw pin labels next to control points
drawPinLabels(g2d, x, y, getRelativeOffsets(), SILK_COLOR);
}
@@ -854,4 +828,23 @@ public class ESP32DevKit extends AbstractMakerBoard {
g2d.setFont(ICON_FONT);
StringUtils.drawCenteredText(g2d, "ESP32", width / 2, height / 2 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum DevKitVersion {
DevKit_V1_30Pin("ESP32 DevKit V1 (30-Pin)"),
DevKitC_V4_38Pin("ESP32 DevKitC V4 (38-Pin)"),
ESP32_S3_DevKitC_44Pin("ESP32-S3 DevKitC-1 (44-Pin)"),
ESP32_C3_DevKitM_1("ESP32-C3 DevKitM-1 (30-Pin)"),
ESP32_C6_DevKitC_1("ESP32-C6 DevKitC-1 (32-Pin)");
private final String label;
DevKitVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -83,23 +83,6 @@ public class RaspberryPi extends AbstractMakerBoard {
"PCIe", "MIPI 1", "MIPI 0", "UART"
};
public enum RaspberryPiVersion {
PI_3_B("Pi 3 (Model B+)"),
PI_4_B("Pi 4 (Model B)"),
PI_5("Pi 5");
private String label;
private RaspberryPiVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
private RaspberryPiVersion version = RaspberryPiVersion.PI_5;
@org.diylc.core.annotations.EditableProperty(name = "Version")
@@ -113,6 +96,12 @@ public class RaspberryPi extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
RaspberryPiVersion version = getVersion();
return version == null ? null : version.toString();
}
public RaspberryPi() {
super();
this.bodyColor = RPI_GREEN;
@@ -258,7 +247,6 @@ public class RaspberryPi extends AbstractMakerBoard {
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(61.5d, SizeUnit.mm).convertToPixels(),
boardY + new Size(52.5d, SizeUnit.mm).convertToPixels(), holeDiameter);
// USB & Ethernet Ports on the right edge
double topDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 45.75d : 47.0d;
double midDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 27.0d : 29.0d;
double botDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 9.0d : 10.25d;
@@ -270,25 +258,21 @@ public class RaspberryPi extends AbstractMakerBoard {
double ethernetCenterY = (version == RaspberryPiVersion.PI_4_B) ? topCenterY : bottomCenterY;
double topUsbCenterY = (version == RaspberryPiVersion.PI_4_B) ? bottomCenterY : topCenterY;
// Top USB slot (USB 3.0)
MakerBoardPainter.drawUsbA(g2d, boardX + boardW - new Size(14.5d, SizeUnit.mm).convertToPixels(),
boardY + new Size(topUsbCenterY - 7.0d, SizeUnit.mm).convertToPixels(),
USB_A_DUAL_LENGTH.convertToPixels(),
new Size(14.0d, SizeUnit.mm).convertToPixels(), "USB 3.0");
// USB 2.0 (middle)
MakerBoardPainter.drawUsbA(g2d, boardX + boardW - new Size(14.5d, SizeUnit.mm).convertToPixels(),
boardY + new Size(middleCenterY - 7.0d, SizeUnit.mm).convertToPixels(),
USB_A_DUAL_LENGTH.convertToPixels(),
new Size(14.0d, SizeUnit.mm).convertToPixels(), "USB 2.0");
// Ethernet
MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW - new Size(18.0d, SizeUnit.mm).convertToPixels(),
boardY + new Size(ethernetCenterY - 8.0d, SizeUnit.mm).convertToPixels(),
new Size(21.0d, SizeUnit.mm).convertToPixels(),
new Size(16.0d, SizeUnit.mm).convertToPixels(), "ETHERNET");
// Broadcom SoC
double socW;
double socH;
double socX;
@@ -492,7 +476,6 @@ public class RaspberryPi extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "GPIO", boardX + new Size(32.5d, SizeUnit.mm).convertToPixels(),
boardY + new Size(7.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Raspberry Pi Silkscreen text below 40-pin header
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT_TITLE);
String versionLabel = version.toString();
@@ -511,7 +494,6 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw 40 GPIO header pins and 4 PoE header pins
drawPinHeader(g2d, 0, 44, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
@@ -524,7 +506,6 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.setColor(RPI_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 4, 4));
// USB & Ethernet (Top USB 3.0, Middle USB 2.0, Bottom Ethernet)
g2d.setColor(USB_METAL_COLOR);
if (version == RaspberryPiVersion.PI_4_B) {
g2d.fillRect(width - 9, 5, 7, 6);
@@ -536,7 +517,6 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.fillRect(width - 9, 20, 7, 6);
}
// SoC
g2d.setColor(IC_BODY_COLOR);
if (version == RaspberryPiVersion.PI_5) {
g2d.fillRect(13, 14, 7, 7);
@@ -546,11 +526,9 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.fillRect(10, 11, 5, 5);
}
// GPIO Header
g2d.setColor(HEADER_BODY_COLOR);
g2d.fillRect(4, 5, 18, 3);
// PCIe & MIPI connectors & PoE
g2d.setColor(IC_BODY_COLOR);
if (version == RaspberryPiVersion.PI_5) {
g2d.setColor(IC_BODY_COLOR);
@@ -575,4 +553,21 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.setFont(ICON_FONT_LARGE);
StringUtils.drawCenteredText(g2d, "RPi", 14, 25, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum RaspberryPiVersion {
PI_3_B("Pi 3 (Model B+)"),
PI_4_B("Pi 4 (Model B)"),
PI_5("Pi 5");
private String label;
private RaspberryPiVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -61,24 +61,6 @@ public class RaspberryPiPico extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum PicoVersion {
PICO("Pi Pico"),
PICO_W("Pi Pico W"),
PICO_2("Pi Pico 2"),
PICO_2_W("Pi Pico 2 W");
private final String label;
PicoVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
public static Size BOARD_WIDTH = new Size(21.0d, SizeUnit.mm);
public static Size BOARD_LENGTH = new Size(51.0d, SizeUnit.mm);
@@ -107,11 +89,11 @@ public class RaspberryPiPico extends AbstractMakerBoard {
public static final String[] PIN_NAMES = new String[] {
// Left row (pins 0..19)
"GP0", "GP1", "GND1", "GP2", "GP3", "GP4", "GP5", "GND2", "GP6", "GP7",
"GP8", "GP9", "GND3", "GP10", "GP11", "GP12", "GP13", "GND4", "GP14", "GP15",
"GP0", "GP1", "GND_1", "GP2", "GP3", "GP4", "GP5", "GND_2", "GP6", "GP7",
"GP8", "GP9", "GND_3", "GP10", "GP11", "GP12", "GP13", "GND_4", "GP14", "GP15",
// Right row (pins 20..39)
"VBUS", "VSYS", "GND5", "3V3_EN", "3V3 (OUT)", "ADC_VREF", "GP28", "GND6", "GP27", "GP26",
"RUN", "GP22", "GND7", "GP21", "GP20", "GP19", "GP18", "GND8", "GP17", "GP16",
"VBUS", "VSYS", "GND_5", "3V3_EN", "3V3 (OUT)", "ADC_VREF", "GP28", "GND_6", "GP27", "GP26",
"RUN", "GP22", "GND_7", "GP21", "GP20", "GP19", "GP18", "GND_8", "GP17", "GP16",
// SWD debug pins (pins 40..42)
"SWCLK", "GND_SWD", "SWDIO"
};
@@ -139,6 +121,11 @@ public class RaspberryPiPico extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
/**
* True for the wireless variants, which carry the CYW43439 radio and move the SWD pads out of
* the bottom edge to make room for it.
@@ -290,7 +277,6 @@ public class RaspberryPiPico extends AbstractMakerBoard {
MakerBoardPainter.drawMountingHole(g2d, holeCenterX - holeDistX, bottomHoleY, holeDiameter);
MakerBoardPainter.drawMountingHole(g2d, holeCenterX + holeDistX, bottomHoleY, holeDiameter);
// Micro USB Connector
double usbW = USB_MICRO_WIDTH.convertToPixels();
double usbH = USB_MICRO_LENGTH.convertToPixels();
double usbOverhang = new Size(1.3d, SizeUnit.mm).convertToPixels();
@@ -405,10 +391,6 @@ public class RaspberryPiPico extends AbstractMakerBoard {
return NOTCH_SIZE;
}
/**
* Helper to draw Raspberry Pi Pico's castellated solder pads along the left and right edges
* and the SWD round pads.
*/
protected void drawCastellatedPads(Graphics2D g2d, double boardX, double boardY, double boardW, double boardH,
double pin1OffsetX, double pin1OffsetY, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) return;
@@ -485,14 +467,12 @@ public class RaspberryPiPico extends AbstractMakerBoard {
g2d.setColor(RPI_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(6, 2, width - 12, height - 4, 3, 3));
// Castellated edge pads on left and right in icon
g2d.setColor(PAD_COLOR);
for (int y = 5; y <= height - 6; y += 3) {
g2d.fillRect(6, y, 3, 2);
g2d.fillRect(width - 9, y, 3, 2);
}
// USB Connector
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect(11, 2, 10, 3);
@@ -504,4 +484,22 @@ public class RaspberryPiPico extends AbstractMakerBoard {
g2d.setFont(ICON_FONT);
StringUtils.drawCenteredText(g2d, "PICO", width / 2, height / 2 + 10, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum PicoVersion {
PICO("Pi Pico"),
PICO_W("Pi Pico W"),
PICO_2("Pi Pico 2"),
PICO_2_W("Pi Pico 2 W");
private final String label;
PicoVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -75,23 +75,6 @@ public class RaspberryPiZero extends AbstractMakerBoard {
PIN_NAMES[44] = "TV 2";
}
public enum ZeroVersion {
PI_ZERO("Pi Zero"),
PI_ZERO_W("Pi Zero W"),
PI_ZERO_2_W("Pi Zero 2 W");
private final String label;
ZeroVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
protected ZeroVersion version = ZeroVersion.PI_ZERO;
protected boolean headers = false;
@@ -115,6 +98,11 @@ public class RaspberryPiZero extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
@EditableProperty(name = "Headers")
public boolean getHeaders() {
return headers;
@@ -288,7 +276,6 @@ public class RaspberryPiZero extends AbstractMakerBoard {
double socY = boardY + new Size(11.5d, SizeUnit.mm).convertToPixels();
MakerBoardPainter.drawChip(g2d, socX, socY, socW, socH, "");
// Raspberry Pi logo on SoC chip
double logoSize = new Size(7.0d, SizeUnit.mm).convertToPixels();
double logoW = logoSize * (72.51 / 92.604);
double logoX = socX + (socW - logoW) / 2.0;
@@ -324,7 +311,6 @@ public class RaspberryPiZero extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "GPIO", boardX + new Size(42.5d, SizeUnit.mm).convertToPixels(),
y + PIN_SPACING.convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Silkscreen for RUN and TV pads
double spacing = PIN_SPACING.convertToPixels();
double runBoxX = x + 18 * spacing - spacing / 2.0;
double runBoxY = y + 1 * spacing - spacing / 2.0;
@@ -357,9 +343,6 @@ public class RaspberryPiZero extends AbstractMakerBoard {
g2d.setComposite(oldComposite);
}
/**
* Helper to draw GPIO solder pads (copper pads with drill holes, square for Pin 1).
*/
protected void drawSolderPads(Graphics2D g2d, boolean outlineMode, IDrawingObserver drawingObserver) {
drawPcbSolderPads(g2d, 0, 40, true, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 41, 2, true, outlineMode, drawingObserver);
@@ -374,17 +357,14 @@ public class RaspberryPiZero extends AbstractMakerBoard {
g2d.setColor(RPI_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3));
// GPIO Solder Pads in icon
g2d.setColor(PAD_COLOR);
for (int x = 6; x <= width - 8; x += 3) {
g2d.fillRect(x, 7, 2, 2);
}
// SoC
g2d.setColor(IC_BODY_COLOR);
g2d.fillRect(10, 12, 6, 6);
// Connectors on bottom
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect(6, height - 7, 4, 3);
g2d.fillRect(18, height - 7, 3, 3);
@@ -394,4 +374,21 @@ public class RaspberryPiZero extends AbstractMakerBoard {
g2d.setFont(ICON_FONT);
StringUtils.drawCenteredText(g2d, "ZERO", width / 2 + 4, 21, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum ZeroVersion {
PI_ZERO("Pi Zero"),
PI_ZERO_W("Pi Zero W"),
PI_ZERO_2_W("Pi Zero 2 W");
private final String label;
ZeroVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -58,23 +58,6 @@ public class Teensy extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum TeensyVersion {
Teensy_3_2("Teensy 3.2"),
Teensy_4_0("Teensy 4.0"),
Teensy_4_1("Teensy 4.1");
private final String label;
TeensyVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
// gap between the Ethernet header pads and the silkscreen box printed around them
public static Size ETH_BOX_MARGIN = new Size(1.2d, SizeUnit.mm);
@@ -106,15 +89,15 @@ public class Teensy extends AbstractMakerBoard {
// VUSB (33, near VIN): VUSB
public static final String[] PIN_NAMES_40 = new String[] {
// Left row (0..13)
"GND", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"GND_1", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"5 (IN2)", "6 (OUT1D)", "7 (RX2/OUT1A)", "8 (TX2/IN1)", "9 (OUT1C)", "10 (CS/MQSR)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)",
// Right row (14..27)
"VIN (3.6-5.5V)", "GND", "3.3V (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"VIN (3.6-5.5V)", "GND_2", "3.3V_1 (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"20 (A6/TX5)", "19 (A5/SCL0)", "18 (A4/SDA0)", "17 (A3/TX4/SDA1)", "16 (A2/RX4/SCL1)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/CRX1/LED)",
// End row (28..32, left to right along bottom edge)
"VBAT", "3.3V (End)", "GND (End)", "Program", "On/Off",
"VBAT", "3.3V_2 (End)", "GND_3 (End)", "Program", "On/Off",
// VUSB (33)
"VUSB"
};
@@ -142,19 +125,19 @@ public class Teensy extends AbstractMakerBoard {
// VUSB (64, near VIN): VUSB
public static final String[] PIN_NAMES_41 = new String[] {
// Left row (0..23)
"GND", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"GND_1", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"5 (IN2)", "6 (OUT1D)", "7 (RX2/OUT1A)", "8 (TX2/IN1)", "9 (OUT1C)", "10 (CS/MQSR)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)", "3.3V", "24 (A10/TX6/SCL2)", "25 (A11/RX6/SDA2)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)", "3.3V_1", "24 (A10/TX6/SCL2)", "25 (A11/RX6/SDA2)",
"26 (A12/MOSI1)", "27 (A13/SCK1)", "28 (RX7)", "29 (TX7)", "30 (CRX3)", "31 (CTX3)",
"32 (OUT1B)",
// Right row (24..47)
"VIN (3.6-5.5V)", "GND", "3.3V (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"VIN (3.6-5.5V)", "GND_2", "3.3V_2 (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"20 (A6/TX5)", "19 (A5/SCL)", "18 (A4/SDA)", "17 (A3/TX4/SDA1)", "16 (A2/RX4/SCL1)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/LED)", "GND", "41 (A17)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/LED)", "GND_3", "41 (A17)",
"40 (A16)", "39 (A15/MISO1)", "38 (A14/CS1)", "37 (CS)", "36 (CS)", "35 (TX8)",
"34 (RX8)", "33 (MCLK2)",
// Middle cluster (48..52, left to right near push button)
"VBAT", "3.3V (Mid)", "GND (Mid)", "Program", "On/Off",
"VBAT", "3.3V_3 (Mid)", "GND_4 (Mid)", "Program", "On/Off",
// Ethernet header (53..58, 3x2)
"ETH_TX-", "ETH_LED", "ETH_RX+", "ETH_TX+", "ETH_GND", "ETH_RX-",
// USB Host header (59..63, 1x5)
@@ -177,14 +160,14 @@ public class Teensy extends AbstractMakerBoard {
// severable VUSB pad next to Vin. Labels follow the PJRC Teensy 3.2 pinout card.
public static final String[] PIN_NAMES_32 = new String[] {
// Left row (0..13)
"GND", "0 (RX1)", "1 (TX1)", "2", "3 (PWM)", "4 (PWM)", "5 (PWM)", "6 (PWM)", "7 (RX3)",
"GND_1", "0 (RX1)", "1 (TX1)", "2", "3 (PWM)", "4 (PWM)", "5 (PWM)", "6 (PWM)", "7 (RX3)",
"8 (TX3)", "9 (RX2/PWM)", "10 (TX2/CS/PWM)", "11 (MOSI)", "12 (MISO)",
// Right row (14..27)
"Vin (3.6-6.0V)", "AGND", "3.3V (100mA)", "23 (A9/PWM)", "22 (A8/PWM)", "21 (A7/PWM)",
"Vin (3.6-6.0V)", "AGND", "3.3V_1 (100mA)", "23 (A9/PWM)", "22 (A8/PWM)", "21 (A7/PWM)",
"20 (A6/PWM)", "19 (A5/SCL0)", "18 (A4/SDA0)", "17 (A3)", "16 (A2)", "15 (A1/CS)",
"14 (A0)", "13 (SCK/LED)",
// End row (28..32, left to right along bottom edge)
"VBAT", "3.3V (End)", "GND (End)", "Program", "A14/DAC",
"VBAT", "3.3V_2 (End)", "GND_2 (End)", "Program", "A14/DAC",
// VUSB (33)
"VUSB"
};
@@ -238,6 +221,11 @@ public class Teensy extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
@EditableProperty(name = "Headers")
public boolean getHeaders() {
return headers;
@@ -415,11 +403,9 @@ public class Teensy extends AbstractMakerBoard {
drawingObserver.startTracking();
// Fill board body (green PCB)
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
// Board outline
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
@@ -453,7 +439,6 @@ public class Teensy extends AbstractMakerBoard {
MakerBoardPainter.drawChip(g2d, chipX, chipY, chipW, chipH,
getVersion() == TeensyVersion.Teensy_3_2 ? "MK20DX256" : "iMXRT1062");
// Pushbutton (Program button)
double btnW = BUTTON_WIDTH.convertToPixels();
double btnH = BUTTON_LENGTH.convertToPixels();
double btnX = centerX - btnW / 2.0;
@@ -462,9 +447,9 @@ public class Teensy extends AbstractMakerBoard {
: boardY + new Size(35.5d, SizeUnit.mm).convertToPixels() + new Size(0.25d, SizeUnit.in).convertToPixels();
MakerBoardPainter.drawButton(g2d, btnX, btnY, btnW, btnH);
// Teensy 4.1 extras: Ethernet PHY + SD card slot
// the 4.1's Ethernet PHY and SD card slot
if (getVersion() == TeensyVersion.Teensy_4_1) {
// MicroSD card slot at bottom edge (12mm long, flush with bottom edge so it does not stick out)
// flush with the bottom edge so it does not stick out
double sdW = new Size(12.0d, SizeUnit.mm).convertToPixels();
double sdH = new Size(12.0d, SizeUnit.mm).convertToPixels();
double sdX = centerX - sdW / 2.0;
@@ -494,7 +479,6 @@ public class Teensy extends AbstractMakerBoard {
g2d.draw(new Rectangle2D.Double(usbBoxX, usbBoxY, usbBoxW, usbBoxW));
}
// Silkscreen "TEENSY" label
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
String silkText = getVersion().toString();
@@ -516,7 +500,6 @@ public class Teensy extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Render pin headers or gold solder pads with drill holes
if (headers) {
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
} else {
@@ -526,9 +509,6 @@ public class Teensy extends AbstractMakerBoard {
g2d.setComposite(oldComposite);
}
/**
* Helper to draw GPIO solder pads (copper/gold pads with drill holes, square for Pin 1).
*/
protected void drawSolderPads(Graphics2D g2d, boolean outlineMode, IDrawingObserver drawingObserver) {
drawPcbSolderPads(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver);
}
@@ -540,18 +520,15 @@ public class Teensy extends AbstractMakerBoard {
g2d.setColor(TEENSY_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(5, 2, width - 10, height - 4, 3, 3));
// GPIO Solder Pads in icon
g2d.setColor(PAD_COLOR);
for (int y = 5; y <= height - 6; y += 3) {
g2d.fillRect(6, y, 2, 2);
g2d.fillRect(width - 8, y, 2, 2);
}
// MCU chip
g2d.setColor(IC_BODY_COLOR);
g2d.fillRect(10, 8, 12, 8);
// USB connector at top
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect(12, 2, 8, 3);
@@ -560,4 +537,21 @@ public class Teensy extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "TEENSY", width / 2, height / 2 + 8,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum TeensyVersion {
Teensy_3_2("Teensy 3.2"),
Teensy_4_0("Teensy 4.0"),
Teensy_4_1("Teensy 4.1");
private final String label;
TeensyVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -59,6 +59,8 @@ public class WemosD1Mini extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
private static final int PINS_PER_ROW = 8;
public static Size BOARD_WIDTH = new Size(1.0d, SizeUnit.in);
public static Size BOARD_LENGTH = new Size(1.34d, SizeUnit.in);
public static Size ROW_SPACING = new Size(0.9d, SizeUnit.in);
@@ -92,14 +94,6 @@ public class WemosD1Mini extends AbstractMakerBoard {
"5V", "G (GND)", "D4 (GPIO2)", "D3 (GPIO0)", "D2 (GPIO4)", "D1 (GPIO5)", "RX (GPIO3)", "TX (GPIO1)"
};
public static final String[] SILK_PIN_NAMES_LEFT = new String[] {
"RST", "A0", "D0", "D5", "D6", "D7", "D8", "3V3"
};
public static final String[] SILK_PIN_NAMES_RIGHT = new String[] {
"TX", "RX", "D1", "D2", "D3", "D4", "G", "5V"
};
protected boolean headers = false;
public WemosD1Mini() {
@@ -126,23 +120,28 @@ public class WemosD1Mini extends AbstractMakerBoard {
return "Pin " + (index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double[][] relativeOffsets = new double[16][2];
for (int i = 0; i < 8; i++) {
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
// Left row (pins 0..7, top to bottom)
for (int i = 0; i < PINS_PER_ROW; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
}
for (int i = 0; i < 8; i++) {
relativeOffsets[8 + i][0] = rowSpacing;
relativeOffsets[8 + i][1] = (7 - i) * spacing;
// Right row (pins 8..15, bottom to top, which is the order the board numbers them in)
for (int i = 0; i < PINS_PER_ROW; i++) {
relativeOffsets[PINS_PER_ROW + i][0] = rowSpacing;
relativeOffsets[PINS_PER_ROW + i][1] = (PINS_PER_ROW - 1 - i) * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@Override
@@ -287,18 +286,19 @@ public class WemosD1Mini extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "D1 mini", x + rowSpacing / 2.0, labelY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Silkscreen: Pin labels
// Silkscreen: pin names, lying flat beside their row as the board prints them rather than
// standing on end like drawPinLabels' -- the 0.9" row spacing leaves room for them. Both the
// text and the position come from the control point, so the right row's bottom-to-top order
// is followed rather than mirrored by a second array kept in step by hand.
g2d.setFont(SILK_FONT_TINY);
double spacing = PIN_SPACING.convertToPixels();
double labelOffset = new Size(1.4d, SizeUnit.mm).convertToPixels();
for (int i = 0; i < 8; i++) {
double pinY = y + i * spacing;
// Left pin labels
StringUtils.drawCenteredText(g2d, SILK_PIN_NAMES_LEFT[i], x + labelOffset, pinY,
HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
// Right pin labels
StringUtils.drawCenteredText(g2d, SILK_PIN_NAMES_RIGHT[i], x + rowSpacing - labelOffset, pinY,
HorizontalAlignment.RIGHT, VerticalAlignment.CENTER);
double[][] offsets = getRelativeOffsets();
for (int i = 0; i < offsets.length; i++) {
boolean leftRow = i < PINS_PER_ROW;
StringUtils.drawCenteredText(g2d, getSilkPinLabel(i),
x + offsets[i][0] + (leftRow ? labelOffset : -labelOffset), y + offsets[i][1],
leftRow ? HorizontalAlignment.LEFT : HorizontalAlignment.RIGHT,
VerticalAlignment.CENTER);
}
}
@@ -216,7 +216,7 @@ public class MT3608BoostConverter extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw square/rounded rectangular tinned solder pads using LIGHT_METAL_COLOR
drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -188,7 +188,7 @@ public class TP4056Charger extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw tinned solder pads
drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -88,7 +88,10 @@ public class DHTSensor extends AbstractMakerBoard {
public void setModel(DHTModel model) {
this.model = model;
this.bodyColor = (model == DHTModel.DHT11) ? DHT11_BLUE : DHT22_WHITE;
// Follows the model only while the colour is still a default of ours; see ArduinoUno.
if (DHT11_BLUE.equals(bodyColor) || DHT22_WHITE.equals(bodyColor)) {
this.bodyColor = model == DHTModel.DHT11 ? DHT11_BLUE : DHT22_WHITE;
}
invalidateCache();
}
@@ -0,0 +1,129 @@
/*
*
* 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.transform;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.Orientation;
import org.diylc.components.displays.LEDBarGraph;
import org.diylc.core.IDIYComponent;
/**
* Rotation and mirroring for {@link LEDBarGraph}, modelled on {@link SIL_ICTransformer}. A sibling
* rather than a reuse of {@link DIL_ICTransformer}: that one guards on
* {@code getClass().equals(DIL_IC.class)} and casts to {@code DIL_IC}, which is the convention
* across this package, so it would refuse a bar graph rather than rotate it.
*
* @author Branislav Stojkovic
*/
public class LEDBarGraphTransformer implements IComponentTransformer {
@Override
public boolean canRotate(IDIYComponent<?> component) {
return component.getClass().equals(LEDBarGraph.class);
}
@Override
public boolean canMirror(IDIYComponent<?> component) {
return component.getClass().equals(LEDBarGraph.class);
}
/**
* Mirroring reverses which end carries the first segment, so it reorders the pins without
* changing what any of them is connected to. That is unlike a DIP IC, where the function of a
* given pin number moves, which is why this returns false where {@code DIL_ICTransformer} does
* not.
*/
@Override
public boolean mirroringChangesCircuit() {
return false;
}
@Override
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
AffineTransform rotate =
AffineTransform.getRotateInstance(Math.PI / 2 * direction, center.getX(), center.getY());
for (int index = 0; index < component.getControlPointCount(); index++) {
Point2D p = new Point2D.Double();
rotate.transform(component.getControlPoint(index), p);
component.setControlPoint(p, index);
}
LEDBarGraph bar = (LEDBarGraph) component;
Orientation o = bar.getOrientation();
int oValue = o.ordinal();
oValue += direction;
if (oValue < 0) {
oValue = Orientation.values().length - 1;
}
if (oValue >= Orientation.values().length) {
oValue = 0;
}
bar.setOrientation(Orientation.values()[oValue]);
}
@SuppressWarnings("incomplete-switch")
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
LEDBarGraph bar = (LEDBarGraph) component;
double dx = center.getX() - bar.getControlPoint(0).getX();
double dy = center.getY() - bar.getControlPoint(0).getY();
// The orientation has to turn with the points: the body is drawn from it, so reflecting the
// pins alone would leave the package facing the way it did before.
if (direction == IComponentTransformer.HORIZONTAL) {
Orientation o = bar.getOrientation();
switch (o) {
case _90:
o = Orientation._270;
break;
case _270:
o = Orientation._90;
}
for (int i = 0; i < bar.getControlPointCount(); i++) {
Point2D p = bar.getControlPoint(i);
bar.setControlPoint(new Point2D.Double(p.getX() + 2 * dx, p.getY()), i);
}
bar.setOrientation(o);
} else {
Orientation o = bar.getOrientation();
switch (o) {
case DEFAULT:
o = Orientation._180;
break;
case _180:
o = Orientation.DEFAULT;
}
for (int i = 0; i < bar.getControlPointCount(); i++) {
Point2D p = bar.getControlPoint(i);
bar.setControlPoint(new Point2D.Double(p.getX(), p.getY() + 2 * dy), i);
}
bar.setOrientation(o);
}
}
}
@@ -74,7 +74,7 @@ public class Jumbo4PinTubeSocket extends AbstractAngledComponent<String> {
public static Size PIN_DIAMETER = new Size(1d, SizeUnit.mm);
public static Size HOLE_SIZE = new Size(5d, SizeUnit.mm);
public static Size OCTAL_TICK_SIZE = new Size(2d, SizeUnit.mm);
private static String[] electrodeLabels = new String[] {"G", "F", "P", "F"};
private static String[] electrodeLabels = new String[] {"G", "F1", "P", "F2"};
private String type = "";