neopixel improvements

This commit is contained in:
Bane Stojkovic
2026-10-07 12:58:08 +02:00
parent 0aca15c59a
commit 2a08fbe569
16 changed files with 352 additions and 95 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;
}
}
@@ -102,6 +102,10 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
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);
@@ -625,14 +629,22 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
* Called once per palette at class load rather than per repaint.
*/
protected static Color[] buildLedGradient(int count) {
return buildLedGradient(count, RGB_LED_GRADIENT);
}
/**
* As above, over a palette of the caller's choosing, for a part whose LEDs cannot emit the
* colour wheel.
*/
protected static Color[] buildLedGradient(int count, Color[] anchors) {
Color[] colors = new Color[count];
int anchorCount = RGB_LED_GRADIENT.length;
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 = RGB_LED_GRADIENT[anchor];
Color to = RGB_LED_GRADIENT[(anchor + 1) % anchorCount];
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),
@@ -924,4 +936,34 @@ 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 nothing but what the LEDs can emit and what the BOM has to
* ask for -- which is the whole reason it is a property rather than a separate component.
*
* <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; }
}
}
@@ -274,6 +274,17 @@ public class MakerBoardPainter {
*/
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 part fitted with a package other than the plain RGB one. An RGBW package is
* told from an RGB one on sight only by the white die sitting beside the colour dies, so that
* is what is drawn; WWA has the same three-die layout as RGB and is told apart instead by what
* it emits, which is the caller's palette rather than anything here.
*/
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);
}
@@ -293,8 +304,16 @@ public class MakerBoardPainter {
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 - dieSize / 2.0, cy - dieSize / 2.0, dieSize, dieSize));
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) {
@@ -32,7 +32,7 @@ import java.awt.geom.RoundRectangle2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
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;
@@ -51,7 +51,7 @@ import org.diylc.utils.Constants;
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BBreakout extends AbstractMakerBoard {
public class WS2812BBreakout extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
@@ -74,10 +74,6 @@ public class WS2812BBreakout extends AbstractMakerBoard {
public static Size MOUNTING_HOLE_SIZE = new Size(2.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(0.3d, SizeUnit.in);
// Drawn lit off the same colour wheel as the rest of the family, so a breakout on a layout reads
// as the same part as one pixel of a stick.
public static Color LED_COLOR = buildLedGradient(1)[0];
// Each edge carries ground, power and one data line. 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 = {
@@ -91,6 +87,11 @@ public class WS2812BBreakout extends AbstractMakerBoard {
updateControlPoints();
}
@Override
protected String getVariantLabel() {
return getLedType().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
@@ -196,9 +197,11 @@ public class WS2812BBreakout extends AbstractMakerBoard {
// The pad columns clear the package by 2.35 mm, but that room is between a pad and the
// package rather than beside a pad, so there is nowhere a pin name would go: this face
// carries no silkscreen, as the stick's does not.
// Drawn lit off the same wheel as the rest of the family, so a breakout on a layout 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(), LED_COLOR);
RGB_LED_SIZE.convertToPixels(), getLedColors(1)[0], 0, getLedType());
}
g2d.setTransform(oldTx);
@@ -31,7 +31,7 @@ import java.awt.geom.Point2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
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;
@@ -56,7 +56,7 @@ import org.diylc.utils.Constants;
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BJewel extends AbstractMakerBoard {
public class WS2812BJewel extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
@@ -81,16 +81,17 @@ public class WS2812BJewel extends AbstractMakerBoard {
// 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};
// Drawn lit, as the rings are: unlit it is a black disc with white specks on it, which reads
// as no particular part.
public static Color[] LED_COLORS = buildLedGradient(RING_LED_COUNT + 1);
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) {
@@ -198,17 +199,19 @@ public class WS2812BJewel extends AbstractMakerBoard {
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, LED_COLORS[i % LED_COLORS.length],
angle + Math.PI / 2.0);
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,
LED_COLORS[RING_LED_COUNT % LED_COLORS.length]);
ledColors[RING_LED_COUNT % ledColors.length], 0, ledType);
// Pad names are left to the node tooltips and the netlist: a pad sits in the gap between two
// LEDs, which is nowhere near enough room for its name.
@@ -36,7 +36,7 @@ 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;
@@ -65,7 +65,7 @@ import org.diylc.utils.Constants;
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BPanel extends AbstractMakerBoard {
public class WS2812BPanel extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
@@ -115,12 +115,6 @@ public class WS2812BPanel extends AbstractMakerBoard {
*/
private static final double CAP_HEIGHT_RATIO = 0.72d;
// Drawn lit, as every addressable part in this package is. The colour is handed out along the
// data chain rather than across the board, so the sweep shows the order the pixels are addressed
// in: this panel is wired as a progressive raster, left to right along each row and then back to
// the left of the next, not as the serpentine most cheap panels use.
public static Color[] LED_COLORS = buildLedGradient(MATRIX_ORDER * MATRIX_ORDER);
/**
* What the face of the board 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 grid reaches
@@ -143,12 +137,12 @@ public class WS2812BPanel extends AbstractMakerBoard {
}
/**
* There is only one of these panels, so unlike a board with a variant property this is not
* needed to keep the BOM's rows apart. It is still the better answer than an empty value column.
* The panel comes in one size, so unlike a board with a geometry property the grid is not what
* keeps the BOM's rows apart -- the package is.
*/
@Override
protected String getVariantLabel() {
return "8x8, 64 LEDs";
return "8x8, 64 LEDs, " + getLedType();
}
@Override
@@ -266,9 +260,16 @@ public class WS2812BPanel extends AbstractMakerBoard {
double margin = (board - (MATRIX_ORDER - 1) * pitch) / 2.0;
double ledSize = RGB_LED_SIZE.convertToPixels();
for (int i = 0; i < LED_COLORS.length; i++) {
// The colour is handed out along the data chain rather than across the board, so the sweep
// shows the order the pixels are addressed in: this panel is wired as a progressive raster,
// left to right along each row and then back to the left of the next, not as 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, LED_COLORS[i]);
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.
@@ -35,7 +35,7 @@ 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;
@@ -54,7 +54,7 @@ import org.diylc.utils.Constants;
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BRing extends AbstractMakerBoard {
public class WS2812BRing extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
@@ -68,12 +68,6 @@ public class WS2812BRing extends AbstractMakerBoard {
// stays the same if the pad footprint changes.
public static Size PAD_LABEL_GAP = new Size(0.4d, SizeUnit.mm);
// The ring is drawn lit: unlit it is a black disc with white specks on it, which reads as no
// particular part. One palette per variant, off the shared colour wheel.
public static Color[] LED_COLORS_12 = buildLedGradient(12);
public static Color[] LED_COLORS_16 = buildLedGradient(16);
public static Color[] LED_COLORS_24 = buildLedGradient(24);
private RingSize ringSize = RingSize._16_LED;
public WS2812BRing() {
@@ -93,10 +87,11 @@ public class WS2812BRing extends AbstractMakerBoard {
invalidateCache();
}
// Every ring is sold in each of the packages, so neither half identifies what to buy on its own.
@Override
protected String getVariantLabel() {
RingSize ringSize = getRingSize();
return ringSize == null ? null : ringSize.toString();
return ringSize == null ? null : ringSize + ", " + getLedType();
}
@Override
@@ -115,20 +110,6 @@ public class WS2812BRing extends AbstractMakerBoard {
return (outerR + innerR) / 2.0;
}
private Color[] getLedColors() {
switch (ringSize) {
case _12_LED:
return LED_COLORS_12;
case _16_LED:
return LED_COLORS_16;
case _24_LED:
return LED_COLORS_24;
default:
// a ring size added later still renders before it is given a palette of its own
return buildLedGradient(ringSize.getLedCount());
}
}
/**
* Pads sit out towards the rim rather than on the circle the LEDs occupy. This has to be the one
* place the radius is decided: {@link #updateControlPoints()} offsets every pad from the first
@@ -226,7 +207,8 @@ public class WS2812BRing extends AbstractMakerBoard {
int ledCount = ringSize.getLedCount();
double ledR = getMidRadius();
double ledSize = RGB_LED_SIZE.convertToPixels();
Color[] ledColors = getLedColors();
Color[] ledColors = getLedColors(ledCount);
LedType ledType = getLedType();
// Draw all LEDs around the circular ring, each package turned to face out along its own
// radius the way it is mounted rather than left square to the board
@@ -234,7 +216,7 @@ public class WS2812BRing extends AbstractMakerBoard {
double angle = 2 * Math.PI * i / ledCount - 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);
angle + Math.PI / 2.0, ledType);
}
// Pad names run along their own radius, reading out from the middle of the ring towards the
@@ -33,7 +33,7 @@ import java.awt.geom.RoundRectangle2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
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;
@@ -51,7 +51,7 @@ import org.diylc.utils.Constants;
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BStick extends AbstractMakerBoard {
public class WS2812BStick extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
@@ -75,8 +75,7 @@ public class WS2812BStick extends AbstractMakerBoard {
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);
// Drawn lit, off the same colour wheel as the ring, so the two read as the same family of part.
public static Color[] LED_COLORS_8 = buildLedGradient(8);
public static final int LED_COUNT = 8;
// Each end carries four pads in the order GND, data, power, GND. Both grounds on an end are the
// same net on the board, so they are numbered only to keep the node names distinct; the
@@ -92,6 +91,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) {
@@ -145,7 +149,7 @@ public class WS2812BStick extends AbstractMakerBoard {
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_COLORS_8.length - 1);
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
@@ -208,9 +212,12 @@ public class WS2812BStick extends AbstractMakerBoard {
// than under them. The pad names are printed on the back of the real board, so this face
// carries no silkscreen.
double ledSize = RGB_LED_SIZE.convertToPixels();
for (int i = 0; i < LED_COLORS_8.length; i++) {
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, LED_COLORS_8[i]);
MakerBoardPainter.drawAddressableLed(g2d, led.getX(), led.getY(), ledSize, ledColors[i], 0,
ledType);
}
}
@@ -35,7 +35,7 @@ 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;
@@ -56,7 +56,7 @@ import org.diylc.utils.Constants;
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BStrip extends AbstractMakerBoard {
public class WS2812BStrip extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
@@ -106,9 +106,6 @@ public class WS2812BStrip extends AbstractMakerBoard {
private Density density = Density._60;
private int ledCount = DEFAULT_LED_COUNT;
// Rebuilt only when the count changes, because draw() runs on every repaint.
private transient Color[] ledColors;
public WS2812BStrip() {
super();
this.bodyColor = TAPE_WHITE;
@@ -139,15 +136,14 @@ public class WS2812BStrip extends AbstractMakerBoard {
public void setLedCount(int ledCount) {
this.ledCount = Math.max(MIN_LED_COUNT, Math.min(MAX_LED_COUNT, ledCount));
this.ledColors = null;
updateControlPoints();
invalidateCache();
}
// Both decide what you buy and how much of it, so the BOM carries them together.
// All three decide what you buy and how much of it, so the BOM carries them together.
@Override
protected String getVariantLabel() {
return getDensity() + ", " + getLedCount() + " LEDs";
return getDensity() + ", " + getLedCount() + " LEDs, " + getLedType();
}
@Override
@@ -187,14 +183,6 @@ public class WS2812BStrip extends AbstractMakerBoard {
return (getLedCount() - 1) * getPitch() + 2 * getLeadIn();
}
private Color[] getLedColors() {
int count = getLedCount();
if (ledColors == null || ledColors.length != count) {
ledColors = buildLedGradient(count);
}
return ledColors;
}
/** The span a column of three pads occupies across the tape, at the standard pad pitch. */
private static double getPadRunLength() {
return 2 * PIN_SPACING.convertToPixels();
@@ -340,11 +328,12 @@ public class WS2812BStrip extends AbstractMakerBoard {
double pitch = getPitch();
double ledSize = RGB_LED_SIZE.convertToPixels();
double firstCenter = boardX + getLeadIn();
Color[] colors = getLedColors();
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]);
ledSize, colors[i], 0, ledType);
}
}
@@ -215,4 +215,9 @@ public class WS2812BBreakoutTest {
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new WS2812BBreakout());
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BBreakout::new);
}
}
@@ -26,6 +26,7 @@ import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.junit.Assert;
@@ -237,19 +238,15 @@ public class WS2812BJewelTest {
holeR * 2, holeR * 2));
}
/** Seven LEDs get seven colours, so the centre one is lit rather than left dark. */
/** The package it is fitted with is the only thing to tell a buyer about this one. */
@Test
public void thePaletteCoversAllSevenLeds() {
Assert.assertEquals(WS2812BJewel.RING_LED_COUNT + 1, WS2812BJewel.LED_COLORS.length);
for (int i = 0; i < WS2812BJewel.LED_COLORS.length; i++) {
Assert.assertNotNull("colour " + i, WS2812BJewel.LED_COLORS[i]);
}
public void bomValueNamesThePackage() {
Assert.assertEquals("RGB", new WS2812BJewel().getValueForDisplay());
}
/** A single-variant board has no variant to put in the BOM's value column. */
@Test
public void bomValueIsEmpty() {
Assert.assertEquals("", new WS2812BJewel().getValueForDisplay());
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BJewel::new);
}
@Test
@@ -235,7 +235,7 @@ public class WS2812BPanelTest {
@Test
public void bomValueNamesTheMatrix() {
Assert.assertEquals("8x8, 64 LEDs", new WS2812BPanel().getValueForDisplay());
Assert.assertEquals("8x8, 64 LEDs, RGB", new WS2812BPanel().getValueForDisplay());
}
/**
@@ -277,4 +277,9 @@ public class WS2812BPanelTest {
g2d.dispose();
}
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BPanel::new);
}
}
@@ -23,10 +23,12 @@ package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard.LedType;
import org.diylc.components.displays.WS2812BRing.RingSize;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
@@ -119,14 +121,80 @@ public class WS2812BRingTest {
for (RingSize ringSize : RingSize.values()) {
WS2812BRing ring = of(ringSize);
Assert.assertEquals(ringSize, ring.getRingSize());
Assert.assertEquals(ringSize.toString(), ring.getValueForDisplay());
Assert.assertTrue(ring.getValueForDisplay().startsWith(ringSize.toString()));
}
}
/** A ring saved before the property existed deserializes without one and has to read as RGB. */
@Test
public void ledTypeDefaultsToRgb() {
Assert.assertEquals(LedType.RGB, new WS2812BRing().getLedType());
WS2812BRing ring = new WS2812BRing();
ring.setLedType(null);
Assert.assertEquals(LedType.RGB, ring.getLedType());
}
/** Both halves name a different thing to buy, so the BOM's value column has to carry both. */
@Test
public void variantLabelCarriesSizeAndLedType() {
for (RingSize ringSize : RingSize.values()) {
for (LedType ledType : LedType.values()) {
WS2812BRing ring = of(ringSize);
ring.setLedType(ledType);
Assert.assertEquals(ringSize + ", " + ledType, ring.getValueForDisplay());
}
}
}
/** The packages are pin-compatible, so fitting a different one moves no pad. */
@Test
public void ledTypeLeavesTheFootprintAlone() {
for (RingSize ringSize : RingSize.values()) {
WS2812BRing reference = of(ringSize);
for (LedType ledType : LedType.values()) {
WS2812BRing ring = of(ringSize);
ring.setLedType(ledType);
MakerBoardTestSupport.assertSharedFootprint(reference, ring, ring.getControlPointCount());
}
}
}
/**
* RGBW shows the white die the RGB package does not carry, and WWA cannot emit the colour wheel
* the other two are drawn lit with. Comparing renderings is what makes this a check on the
* drawing: a draw() that ignored the property would pass the getter.
*/
@Test
public void eachPackageIsDrawnDifferently() {
int[] rgb = renderWith(LedType.RGB);
Assert.assertFalse("RGBW should show its white die, and does not",
Arrays.equals(rgb, renderWith(LedType.RGBW_WARM)));
Assert.assertFalse("A WWA ring should not be drawn lit in colour",
Arrays.equals(rgb, renderWith(LedType.WWA)));
}
/** The three RGBW tints differ only in what you buy, so they are drawn the same. */
@Test
public void theRgbwTintsShareOneDrawing() {
int[] warm = renderWith(LedType.RGBW_WARM);
Assert.assertArrayEquals(warm, renderWith(LedType.RGBW_NATURAL));
Assert.assertArrayEquals(warm, renderWith(LedType.RGBW_COOL));
}
private static int[] renderWith(LedType ledType) {
WS2812BRing ring = of(RingSize._16_LED);
ring.setLedType(ledType);
return MakerBoardTestSupport.renderPixels(ring);
}
@Test
public void drawsCleanlyInEveryState() {
for (RingSize ringSize : RingSize.values()) {
MakerBoardTestSupport.assertDrawsCleanly(of(ringSize));
for (LedType ledType : LedType.values()) {
WS2812BRing ring = of(ringSize);
ring.setLedType(ledType);
MakerBoardTestSupport.assertDrawsCleanly(ring);
}
}
}
}
@@ -194,4 +194,9 @@ public class WS2812BStickTest {
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new WS2812BStick());
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BStick::new);
}
}
@@ -29,6 +29,7 @@ import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.displays.WS2812BStrip.Density;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.junit.Assert;
@@ -288,4 +289,9 @@ public class WS2812BStripTest {
private static double mmToPx(double value) {
return new Size(value, SizeUnit.mm).convertToPixels();
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BStrip::new);
}
}
@@ -26,9 +26,12 @@ import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.function.Supplier;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractAddressableLedBoard;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.AbstractMakerBoard.LedType;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
@@ -156,6 +159,54 @@ public class MakerBoardTestSupport {
Arrays.equals(renderPixels(withText), renderPixels(withoutText)));
}
/**
* Asserts that a board built out of addressable LEDs honours the package it is fitted with.
*
* <p>Four things have to hold for every one of them, so they are asserted here rather than six
* times over: a board saved before the property existed reads as RGB, the packages are
* pin-compatible so fitting one in place of another moves no pad, RGBW is drawn with the white
* die that is the only thing telling it from RGB on sight, and WWA is drawn off the warm white
* palette rather than the colour wheel it cannot emit. The last two compare renderings, which is
* what makes them checks on the drawing: a draw() that ignored the property would pass a getter.
*
* @param factory Builds a fresh board, since each assertion needs one of its own
*/
public static void assertAddressableLedTypes(Supplier<AbstractAddressableLedBoard> factory) {
Assert.assertEquals("A board with no stored package should read as RGB", LedType.RGB,
factory.get().getLedType());
AbstractAddressableLedBoard stale = factory.get();
stale.setLedType(null);
Assert.assertEquals("A board saved before the property existed should read as RGB",
LedType.RGB, stale.getLedType());
AbstractAddressableLedBoard reference = factory.get();
for (LedType ledType : LedType.values()) {
AbstractAddressableLedBoard board = factory.get();
board.setLedType(ledType);
assertSharedFootprint(reference, board, reference.getControlPointCount());
Assert.assertTrue(ledType + " should be named in the BOM's value column",
board.getValueForDisplay().contains(ledType.toString()));
}
int[] rgb = renderWithLedType(factory, LedType.RGB);
Assert.assertFalse("RGBW should show the white die RGB does not carry",
Arrays.equals(rgb, renderWithLedType(factory, LedType.RGBW_WARM)));
Assert.assertFalse("WWA should not be drawn lit in colour",
Arrays.equals(rgb, renderWithLedType(factory, LedType.WWA)));
// the three RGBW tints name different parts to buy but the same thing to draw
int[] warm = renderWithLedType(factory, LedType.RGBW_WARM);
Assert.assertArrayEquals(warm, renderWithLedType(factory, LedType.RGBW_NATURAL));
Assert.assertArrayEquals(warm, renderWithLedType(factory, LedType.RGBW_COOL));
}
private static int[] renderWithLedType(Supplier<AbstractAddressableLedBoard> factory,
LedType ledType) {
AbstractAddressableLedBoard board = factory.get();
board.setLedType(ledType);
return renderPixels(board);
}
/** Draws the board in every state and orientation; anything that throws fails the test. */
public static void assertDrawsCleanly(AbstractMakerBoard board) {
BufferedImage image = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);