This commit is contained in:
Bane Stojkovic
2026-10-02 14:35:41 +02:00
parent d31fb76a48
commit f997ed57fb
6 changed files with 594 additions and 7 deletions
@@ -260,6 +260,21 @@ public class MakerBoardPainter {
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
Color litColor) {
drawAddressableLed(g2d, cx, cy, size, litColor, 0);
}
/**
* As above, with the package turned by {@code rotation} radians about its own centre. Parts laid
* out on a circle are mounted facing out from the middle rather than square to the board, so each
* one turns with its own radius. The package is square, so the turn only tells modulo a quarter
* turn -- which is 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) {
if (rotation != 0) {
g2d.rotate(rotation, cx, cy);
}
double half = size / 2.0;
g2d.setColor(AbstractMakerBoard.RGB_LED_BODY_COLOR);
@@ -277,5 +292,10 @@ public class MakerBoardPainter {
double dieSize = Math.max(2.0, lensR * 0.4);
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));
// turned back rather than saving the transform, so nothing is allocated per LED
if (rotation != 0) {
g2d.rotate(-rotation, cx, cy);
}
}
}
@@ -0,0 +1,280 @@
/*
*
* 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.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.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. It shares the part family and the
// pad footprint but not the footprint family, which is what decides this: the Jewel is a solid disc
// with no inner rim, six of its seven LEDs sit on a circle with the seventh in the middle, its pads
// sit inside that circle instead of out by the rim, and it carries two mounting holes no ring has.
// Expressing that through RingSize would have taken a centre-LED flag plus three per-variant
// dimensions the rings derive from their rims, so it goes its own way.
@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 AbstractMakerBoard {
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, not derived: with no inner rim there is no midpoint to put the LEDs on, which is how
// the rings place theirs.
public static Size LED_CIRCLE_DIAMETER = new Size(16.0d, SizeUnit.mm);
// The pads sit 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 same 5050-family pads the rings carry.
public static Size PAD_WIDTH = new Size(1.6d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.3d, SizeUnit.mm);
public static Size PAD_HOLE = new Size(0.7d, 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, counted from the first pad.
// OUT is in the gap anticlockwise of the top LED and the rest follow it clockwise one LED apart,
// 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
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PAD_NAMES.length) {
return PAD_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected Size getSolderPadWidth() {
return PAD_WIDTH;
}
@Override
protected Size getSolderPadLength() {
return PAD_LENGTH;
}
@Override
protected Size getSolderPadHoleSize() {
return PAD_HOLE;
}
private double getOuterRadius() {
return OUTER_DIAMETER.convertToPixels() / 2.0;
}
/**
* This has to be the one place the pad radius is decided: {@link #updateControlPoints()} offsets
* every pad from the first one by it, and {@link #getCenter()} works backwards from the first pad
* to find the middle of the disc, so the two disagreeing would leave the body drawn away from its
* own control points.
*/
private double getPadRadius() {
return PAD_CIRCLE_DIAMETER.convertToPixels() / 2.0;
}
/**
* Pads are not grouped: each sits in a gap between two consecutive ring LEDs, the gaps being the
* ones {@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();
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);
}
// 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]);
// 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.
}
g2d.setTransform(oldTx);
drawSolderPads(g2d, 0, PAD_NAMES.length, 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 at toolbox size.
g2d.setColor(Color.WHITE);
g2d.fill(new Ellipse2D.Double(cx - dotR, cy - dotR, dotR * 2, dotR * 2));
}
}
@@ -295,11 +295,13 @@ public class WS2812BRing extends AbstractMakerBoard {
double ledSize = RGB_LED_SIZE.convertToPixels();
Color[] ledColors = getLedColors();
// Draw all LEDs around the circular ring
// 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
for (int i = 0; i < ledCount; i++) {
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]);
cy + ledR * Math.sin(angle), ledSize, ledColors[i % ledColors.length],
angle + Math.PI / 2.0);
}
// Pad names are left to the node tooltips and the netlist: a pad sits in the gap between two
@@ -0,0 +1,266 @@
/*
*
* 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.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the Jewel. The arrangement is the part worth pinning down rather than the
* dimensions: five pads share six gaps between the ring LEDs, so one gap is empty, and getting the
* wrong one empty would still place five pads on the right circle and pass any count or radius
* check. The pad-against-LED collision tests are here for the same reason they are on the strip --
* the numbers can all be right while the drawing has a pad under a package.
*/
public class WS2812BJewelTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static double mmToPx(double value) {
return new Size(value, SizeUnit.mm).convertToPixels();
}
private static Point2D centerOf(WS2812BJewel jewel) {
Rectangle2D bounds = jewel.getBodyShape().getBounds2D();
return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
}
/** The ring LED centres, recomputed from the constants the way {@code draw} places them. */
private static Point2D[] ringLeds(WS2812BJewel jewel) {
Point2D center = centerOf(jewel);
double ledR = WS2812BJewel.LED_CIRCLE_DIAMETER.convertToPixels() / 2.0;
Point2D[] leds = new Point2D[WS2812BJewel.RING_LED_COUNT];
for (int i = 0; i < leds.length; i++) {
double angle = 2 * Math.PI * i / WS2812BJewel.RING_LED_COUNT - Math.PI / 2.0;
leds[i] = new Point2D.Double(center.getX() + ledR * Math.cos(angle),
center.getY() + ledR * Math.sin(angle));
}
return leds;
}
private static Rectangle2D ledRect(Point2D led) {
double half = WS2812BJewel.RGB_LED_SIZE.convertToPixels() / 2.0;
double size = half * 2;
return new Rectangle2D.Double(led.getX() - half, led.getY() - half, size, size);
}
/** Axis-aligned, as {@code drawSolderPads} draws it -- pads are not turned to face the centre. */
private static Rectangle2D padRect(Point2D pad) {
double w = WS2812BJewel.PAD_WIDTH.convertToPixels();
double h = WS2812BJewel.PAD_LENGTH.convertToPixels();
return new Rectangle2D.Double(pad.getX() - w / 2.0, pad.getY() - h / 2.0, w, h);
}
@Test
public void fivePadsAreTheControlPoints() {
Assert.assertEquals(5, new WS2812BJewel().getControlPointCount());
}
@Test
public void padNamesAreTheClockwiseOrderFromOut() {
WS2812BJewel jewel = new WS2812BJewel();
String[] expected = new String[] {"OUT", "G_1", "G_2", "PWR", "IN"};
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pad " + i, expected[i], jewel.getControlPointNodeName(i));
}
}
/** The two grounds are distinct nodes; the silk would read both as GND. */
@Test
public void theTwoGroundsAreSeparateNodes() {
WS2812BJewel jewel = new WS2812BJewel();
Assert.assertNotEquals(jewel.getControlPointNodeName(1), jewel.getControlPointNodeName(2));
}
@Test
public void bodyIsATwentyThreeMillimetreDisc() {
Rectangle2D bounds = new WS2812BJewel().getBodyShape().getBounds2D();
Assert.assertEquals("diameter across", 23.0d, mm(bounds.getWidth()), 0.01d);
Assert.assertEquals("a disc, not an ellipse", bounds.getWidth(), bounds.getHeight(), 0.01d);
}
/**
* Every pad the same distance from the centre of the body is what ties {@code getPadRadius} to
* {@code getCenter}: if those two disagreed the disc would be drawn off its own control points,
* which is the failure the ring's comment warns about.
*/
@Test
public void everyPadSitsOnTheNineMillimetreCircle() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Assert.assertEquals("pad " + i + " off the pad circle", 4.5d,
mm(center.distance(jewel.getControlPoint(i))), 0.01d);
}
}
/**
* The arrangement test. Pads belong in the gaps between consecutive ring LEDs, and with five pads
* in six gaps the one left empty must be the one due left, where the left mounting hole is.
*/
@Test
public void padsFillEveryGapButTheOneDueLeft() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
boolean[] occupied = new boolean[WS2812BJewel.RING_LED_COUNT];
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Point2D pad = jewel.getControlPoint(i);
double angle = Math.atan2(pad.getY() - center.getY(), pad.getX() - center.getX());
// gap g is centred half a step past LED g, LED 0 being at the top
double steps = (angle + Math.PI / 2.0) / (2 * Math.PI / WS2812BJewel.RING_LED_COUNT) - 0.5;
long whole = Math.round(steps);
Assert.assertEquals("pad " + i + " does not sit in the middle of a gap", whole, steps,
0.001d);
int gap = (int) (((whole % WS2812BJewel.RING_LED_COUNT) + WS2812BJewel.RING_LED_COUNT)
% WS2812BJewel.RING_LED_COUNT);
Assert.assertFalse("two pads in gap " + gap, occupied[gap]);
occupied[gap] = true;
}
// gap 4 spans LED 4 to LED 5, so on a hexagon anchored at the top it is the one pointing left
for (int gap = 0; gap < occupied.length; gap++) {
if (gap == 4) {
Assert.assertFalse("the empty gap should be the one due left", occupied[gap]);
} else {
Assert.assertTrue("gap " + gap + " has no pad", occupied[gap]);
}
}
}
/** Confirms the empty gap really does point at a mounting hole and not somewhere arbitrary. */
@Test
public void theEmptyGapPointsAtAMountingHole() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
// the left hole sits at pi from the centre, and no pad may lie along that radius
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Point2D pad = jewel.getControlPoint(i);
double angle = Math.atan2(pad.getY() - center.getY(), pad.getX() - center.getX());
Assert.assertTrue("pad " + i + " lies along the left mounting hole's radius",
Math.PI - Math.abs(angle) > 0.1d);
}
}
/** No pad may sit under a package, the centre LED included: it is nearest to the pads. */
@Test
public void noPadSitsUnderAnLed() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
Point2D[] leds = ringLeds(jewel);
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Rectangle2D pad = padRect(jewel.getControlPoint(i));
Assert.assertFalse("pad " + i + " sits under the centre LED",
pad.intersects(ledRect(center)));
for (int l = 0; l < leds.length; l++) {
Assert.assertFalse("pad " + i + " sits under ring LED " + l,
pad.intersects(ledRect(leds[l])));
}
}
}
@Test
public void everyPadAndLedSitsOnTheDisc() {
WS2812BJewel jewel = new WS2812BJewel();
Shape disc = jewel.getBodyShape();
Point2D center = centerOf(jewel);
double outerR = WS2812BJewel.OUTER_DIAMETER.convertToPixels() / 2.0;
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Assert.assertTrue("pad " + i + " hangs off the disc",
disc.contains(padRect(jewel.getControlPoint(i))));
}
for (Point2D led : ringLeds(jewel)) {
Assert.assertTrue("an LED package hangs off the rim",
center.distance(led) + WS2812BJewel.RGB_LED_SIZE.convertToPixels() / 2.0 <= outerR);
}
}
/**
* The holes are 19 mm apart on a 23 mm disc, so each one clears the rim by only 0.5 mm. Note that
* they also come within a fraction of a millimetre of the neighbouring LED packages; that is what
* the measurements give and it is not an error, but it is why this asserts containment rather
* than any comfortable clearance.
*/
@Test
public void mountingHolesSitInsideTheDisc() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
double holeOffset = WS2812BJewel.MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
double holeR = WS2812BJewel.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
double outerR = WS2812BJewel.OUTER_DIAMETER.convertToPixels() / 2.0;
Assert.assertEquals("holes 19 mm apart", 19.0d, mm(holeOffset * 2), 0.01d);
Assert.assertTrue("mounting holes breach the rim", holeOffset + holeR <= outerR);
Assert.assertTrue("a hole is not inside the disc",
jewel.getBodyShape().contains(center.getX() - holeOffset - holeR, center.getY() - holeR,
holeR * 2, holeR * 2));
}
/** Seven LEDs get seven colours, so the centre one is lit rather than left dark. */
@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]);
}
}
/** A single-variant board has no variant to put in the BOM's value column. */
@Test
public void bomValueIsEmpty() {
Assert.assertEquals("", new WS2812BJewel().getValueForDisplay());
}
@Test
public void padsAreAllDistinctPositions() {
WS2812BJewel jewel = new WS2812BJewel();
for (int i = 0; i < jewel.getControlPointCount(); i++) {
for (int j = i + 1; j < jewel.getControlPointCount(); j++) {
Assert.assertTrue("pads " + i + " and " + j + " coincide",
jewel.getControlPoint(i).distance(jewel.getControlPoint(j)) > mmToPx(0.5d));
}
}
}
}
@@ -77,10 +77,12 @@ public class MakerComponentsTest {
OLEDDisplay.class,
SevenSegmentDisplay.class,
LEDMatrix.class,
LEDBarGraph.class,
TFTDisplay.class,
WS2812BStick.class,
WS2812BRing.class,
WS2812BStrip.class,
WS2812BJewel.class,
// Electro-Mechanical
BatteryHolder18650.class
+22 -5
View File
@@ -563,7 +563,7 @@ for the lit colour. Category `Displays & Outputs` even though the neighbours in
has spent its time removing.
- **`WS2812BRing` 7-LED Jewel** — still wanted, but **not the two-line append this item assumed**,
and the hour this section originally budgeted for it was wrong. Two things break:
and the hour this section originally budgeted for it was wrong. Two things broke:
1. *It is a disc, not an annulus.* `draw()` subtracts the inner circle from the outer
unconditionally. An inner diameter of zero would draw the right outline, but `getMidRadius()`
@@ -573,10 +573,27 @@ for the lit colour. Category `Displays & Outputs` even though the neighbours in
`2*pi*i/ledCount`, which would put all seven on one circle and leave the middle empty. That is
an arrangement, not a dimension, so no measurement fixes it.
The cheaper of the two fixes is a flag on `RingSize` -- something like "has a centre LED" -- with
the annulus and the LED loop each branching on it; `RingSize` already carries per-variant
structure in its pad gaps, so this is in keeping. A separate class is the alternative. Blocked on
that decision plus the Jewel's pad arrangement, in the same form the other rings carry it.
Both were settled by making it a separate class, `WS2812BJewel`, which is what decision D2 already
prescribes: a new class where the footprint family is not shared. The flag looked cheaper only
until the measurements arrived, at which point it would have needed company -- an explicit LED
circle (16 mm, with no inner rim for it to be the midpoint of), an explicit pad circle (9 mm, where
the rings inset theirs from the outer rim), a mounting-hole pattern no ring has, and a gap
assertion counting ring LEDs rather than all of them. Five variant-specific structures describe a
different part, not a variant.
Maintainer-supplied figures: a 23 mm disc with no hole; six LEDs on a 16 mm circle with the seventh
in the centre; two 3 mm mounting holes 19 mm apart horizontally; and five pads on a 9 mm circle,
`OUT` in the gap anticlockwise of the top LED, then clockwise `GND`, `GND`, `PWR`, `IN`. Five pads
share six gaps, so one gap carries none; it works out to the gap pointing due left, which is where
a mounting hole sits. `WS2812BJewelTest` pins that down, because every count and radius check
passes whichever gap is left empty -- the arrangement is the part that can silently be wrong.
One change reached back into the rings while this was built. `MakerBoardPainter`'s addressable-LED
helper gained a rotation parameter, so packages laid out on a circle are drawn turned to face along
their own radius the way they are mounted, rather than square to the board. It applies to all three
ring sizes as well as the Jewel; the Jewel's centre package has no radius and stays square, and the
linear strip and stick are unaffected. The package is square, so the turn only shows modulo a
quarter turn, and no geometry assertion catches it -- it is visible only in a render.
- **NeoPixel panel 8x8** as its own class, reusing `drawAddressableLed` and the ring's pad handling.
Worth doing only after §6.1, whose helper it depends on. **~0.5 day.**