mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
jewel
This commit is contained in:
@@ -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
|
||||
|
||||
+266
@@ -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
|
||||
|
||||
@@ -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.**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user