mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
display enhancement
This commit is contained in:
@@ -167,6 +167,10 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
|
||||
// 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)};
|
||||
|
||||
@@ -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;
|
||||
@@ -298,4 +301,77 @@ public class MakerBoardPainter {
|
||||
g2d.rotate(-rotation, cx, cy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a display's own description on its lit area, wrapped to the area and centred in it.
|
||||
*
|
||||
* <p>What it prints is what the component already answers for the BOM, so a variant is stated
|
||||
* once and the glass cannot fall out of step with the part list. The point of it is
|
||||
* identification: six OLED boards are the same dark letterbox on the same blue PCB, and the
|
||||
* property editor, the BOM and the Explorer pane are all off the canvas.
|
||||
*
|
||||
* <p>Nothing is drawn unless the whole block fits, so a panel too small to hold 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,6 +49,7 @@ 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", category = "Displays & Outputs",
|
||||
@@ -75,6 +77,10 @@ public class CharacterLCD extends AbstractMakerBoard {
|
||||
public static Color PCB_GREEN = Color.decode("#1B5E20");
|
||||
public static Color SCREEN_BG = Color.decode("#1E88E5");
|
||||
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 ones, so which is used follows the screen colour rather than being fixed.
|
||||
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[] {
|
||||
@@ -94,6 +100,7 @@ public class CharacterLCD extends AbstractMakerBoard {
|
||||
private LCDSize lcdSize = LCDSize._16x2;
|
||||
private LCDInterface lcdInterface = LCDInterface.I2C_Backpack;
|
||||
private Color screenColor = SCREEN_BG;
|
||||
private Display screen = Display.VALUE;
|
||||
|
||||
public CharacterLCD() {
|
||||
super();
|
||||
@@ -123,6 +130,21 @@ public class CharacterLCD extends AbstractMakerBoard {
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* What the lit area prints. The two sizes differ in outline, but which of them a module is and
|
||||
* which interface it carries are what a reader needs and neither is legible from the drawing;
|
||||
* {@code NONE} leaves the screen blank.
|
||||
*/
|
||||
@EditableProperty
|
||||
public Display getScreen() {
|
||||
return screen == null ? Display.VALUE : screen;
|
||||
}
|
||||
|
||||
public void setScreen(Display screen) {
|
||||
this.screen = screen;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
// Both properties decide which part you buy, so the BOM carries them together.
|
||||
@Override
|
||||
protected String getVariantLabel() {
|
||||
@@ -168,6 +190,11 @@ public class CharacterLCD extends AbstractMakerBoard {
|
||||
: 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();
|
||||
}
|
||||
@@ -289,6 +316,10 @@ public class CharacterLCD extends AbstractMakerBoard {
|
||||
g2d.setColor(screenColor);
|
||||
g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
|
||||
|
||||
MakerBoardPainter.drawScreenText(g2d,
|
||||
new Rectangle2D.Double(screenX, screenY, screenW, screenH), getScreenInk(), getScreen(),
|
||||
getName(), getValueForDisplay());
|
||||
|
||||
// Only the parallel row has anywhere to print. Its names go in the strip between the row and
|
||||
// the bezel, which is 2.9 mm deep. The backpack's four pins stand in a column 2.5 mm from
|
||||
// the left edge with the bezel beginning 1.4 mm further in, so a label beside them would lie
|
||||
|
||||
@@ -31,6 +31,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;
|
||||
@@ -43,6 +44,7 @@ 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;
|
||||
@@ -63,6 +65,7 @@ public class Nokia5110LCD extends AbstractMakerBoard {
|
||||
public static Color BEZEL_BORDER_COLOR = LIGHT_METAL_COLOR.darker();
|
||||
public static Color LCD_COLOR = Color.decode("#6B7668");
|
||||
public static Color LCD_BORDER_COLOR = LCD_COLOR.darker();
|
||||
public static Color LCD_INK_COLOR = 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);
|
||||
@@ -91,12 +94,38 @@ public class Nokia5110LCD extends AbstractMakerBoard {
|
||||
public static final String[] PIN_NAMES =
|
||||
new String[] {"RST", "CE", "DC", "DIN", "CLK", "VCC", "BL", "GND"};
|
||||
|
||||
private Display screen = Display.VALUE;
|
||||
|
||||
public Nokia5110LCD() {
|
||||
super();
|
||||
this.bodyColor = PCB_BLUE;
|
||||
updateControlPoints();
|
||||
}
|
||||
|
||||
/**
|
||||
* What the lit area prints. {@code NONE} leaves the glass blank, which is what the module looks
|
||||
* like unpowered.
|
||||
*/
|
||||
@EditableProperty
|
||||
public Display getScreen() {
|
||||
return screen == null ? Display.VALUE : screen;
|
||||
}
|
||||
|
||||
public void setScreen(Display screen) {
|
||||
this.screen = screen;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* There is only one of these modules, so unlike the boards 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,
|
||||
* and it is what the glass prints.
|
||||
*/
|
||||
@Override
|
||||
protected String getVariantLabel() {
|
||||
return "84x48 PCD8544";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
if (index >= 0 && index < PIN_NAMES.length) {
|
||||
@@ -217,6 +246,10 @@ public class Nokia5110LCD extends AbstractMakerBoard {
|
||||
g2d.draw(new RoundRectangle2D.Double(displayX, displayY, displayW, displayH, displayRadius,
|
||||
displayRadius));
|
||||
|
||||
MakerBoardPainter.drawScreenText(g2d,
|
||||
new Rectangle2D.Double(displayX, displayY, displayW, displayH), LCD_INK_COLOR,
|
||||
getScreen(), getName(), getValueForDisplay());
|
||||
|
||||
// The module prints its pin names in the strip below the header. They lie flat along the row
|
||||
// rather than standing on end across it the way the Arduino boards print theirs, because the
|
||||
// strip is 3.5 mm deep and a label stood on end needs 4.7 mm: one label offset plus the
|
||||
|
||||
@@ -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;
|
||||
@@ -71,6 +72,7 @@ public class OLEDDisplay extends AbstractMakerBoard {
|
||||
|
||||
private Version version = Version.SSD1306_0_96;
|
||||
private OLEDInterface oledInterface = OLEDInterface.I2C_4Pin;
|
||||
private Display screen = Display.VALUE;
|
||||
|
||||
public OLEDDisplay() {
|
||||
super();
|
||||
@@ -100,6 +102,21 @@ public class OLEDDisplay extends AbstractMakerBoard {
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* What the lit area prints. Six boards share this outline and this colour, so the glass carries
|
||||
* the size, the controller and the interface to tell them apart; {@code NONE} leaves the panel
|
||||
* dark for a drawing that wants the unpowered part.
|
||||
*/
|
||||
@EditableProperty
|
||||
public Display getScreen() {
|
||||
return screen == null ? Display.VALUE : screen;
|
||||
}
|
||||
|
||||
public void setScreen(Display screen) {
|
||||
this.screen = screen;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
// Both properties decide which part you buy, so the BOM carries them together.
|
||||
@Override
|
||||
protected String getVariantLabel() {
|
||||
@@ -251,9 +268,14 @@ public class OLEDDisplay extends AbstractMakerBoard {
|
||||
// than of the board it is mounted on, so the two 0.91" boards share them.
|
||||
double activeW = px(version.getActiveWidthMm());
|
||||
double activeH = px(version.getActiveLengthMm());
|
||||
double activeX = glassX + px(version.getActiveLeftInPanelMm());
|
||||
double activeY = glassY + (glassH - activeH) / 2.0;
|
||||
g2d.setColor(ACTIVE_AREA_COLOR);
|
||||
g2d.fill(new RoundRectangle2D.Double(glassX + px(version.getActiveLeftInPanelMm()),
|
||||
glassY + (glassH - activeH) / 2.0, activeW, activeH, 2, 2));
|
||||
g2d.fill(new RoundRectangle2D.Double(activeX, activeY, activeW, activeH, 2, 2));
|
||||
|
||||
MakerBoardPainter.drawScreenText(g2d,
|
||||
new Rectangle2D.Double(activeX, activeY, activeW, activeH), PIXEL_BLUE, getScreen(),
|
||||
getName(), getValueForDisplay());
|
||||
}
|
||||
|
||||
g2d.setTransform(oldTx);
|
||||
|
||||
@@ -34,6 +34,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;
|
||||
@@ -76,10 +77,12 @@ public class TFTDisplay extends AbstractMakerBoard {
|
||||
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 CORNER_RADIUS = new Size(1.5d, SizeUnit.mm);
|
||||
|
||||
private Controller controller = Controller.ILI9341_2_8;
|
||||
private Display screen = Display.VALUE;
|
||||
|
||||
public TFTDisplay() {
|
||||
super();
|
||||
@@ -98,6 +101,21 @@ public class TFTDisplay extends AbstractMakerBoard {
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* What the lit area prints. The four controllers differ in diagonal, resolution and shape but
|
||||
* not in colour, and the 1.8" and 1.54" boards are within 2 mm of each other, so the panel
|
||||
* carries the description; {@code NONE} leaves it dark.
|
||||
*/
|
||||
@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();
|
||||
@@ -246,7 +264,7 @@ public class TFTDisplay extends AbstractMakerBoard {
|
||||
|
||||
double screenW = px(controller.getScreenWidthMm());
|
||||
double screenH = px(controller.getScreenLengthMm());
|
||||
Shape screen;
|
||||
Shape panel;
|
||||
|
||||
if (controller.isRound()) {
|
||||
// the lit circle is centred on the disc, which sits above the tab rather than on the
|
||||
@@ -254,7 +272,7 @@ public class TFTDisplay extends AbstractMakerBoard {
|
||||
double discR = boardW / 2.0;
|
||||
double discCx = boardX + discR;
|
||||
double discCy = boardY + discR;
|
||||
screen = new Ellipse2D.Double(discCx - screenW / 2.0, discCy - screenH / 2.0, screenW,
|
||||
panel = new Ellipse2D.Double(discCx - screenW / 2.0, discCy - screenH / 2.0, screenW,
|
||||
screenH);
|
||||
} else {
|
||||
double bandCentre = (bandTop + bandBottom) / 2.0;
|
||||
@@ -268,15 +286,26 @@ public class TFTDisplay extends AbstractMakerBoard {
|
||||
} else {
|
||||
screenY = bandCentre - screenH / 2.0;
|
||||
}
|
||||
screen = new Rectangle2D.Double(boardX + (boardW - screenW) / 2.0, screenY, screenW,
|
||||
panel = new Rectangle2D.Double(boardX + (boardW - screenW) / 2.0, screenY, screenW,
|
||||
screenH);
|
||||
}
|
||||
|
||||
g2d.setColor(SCREEN_BG);
|
||||
g2d.fill(screen);
|
||||
g2d.fill(panel);
|
||||
g2d.setColor(Color.DARK_GRAY);
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(screen);
|
||||
g2d.draw(panel);
|
||||
|
||||
// Only the square inscribed in a round panel is safely inside it, so the disc hands over
|
||||
// that rather than the bounding box every other variant can use whole.
|
||||
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());
|
||||
|
||||
// The names go in the strip between the pin row and the panel, lying flat along the row as
|
||||
// the Nokia's do: the strip is 3.5 mm deep on the two smallest boards and a label stood on
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
*
|
||||
* 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 org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.components.displays.CharacterLCD.LCDInterface;
|
||||
import org.diylc.components.displays.CharacterLCD.LCDSize;
|
||||
import org.diylc.components.maker.MakerBoardTestSupport;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* What the character LCD prints on its screen. The module's two sizes and two interfaces are the
|
||||
* whole of what a buyer chooses between, and neither is legible from a green board with a blue
|
||||
* window, so the lit area carries them.
|
||||
*/
|
||||
public class CharacterLCDTest {
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp() {
|
||||
try {
|
||||
ConfigurationManager.getInstance().initialize("diylc");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static CharacterLCD of(LCDSize lcdSize, LCDInterface lcdInterface) {
|
||||
CharacterLCD display = new CharacterLCD();
|
||||
display.setLcdSize(lcdSize);
|
||||
display.setLcdInterface(lcdInterface);
|
||||
return display;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyScreenPrintsItsSizeAndInterface() {
|
||||
for (LCDSize lcdSize : LCDSize.values()) {
|
||||
for (LCDInterface lcdInterface : LCDInterface.values()) {
|
||||
CharacterLCD printed = of(lcdSize, lcdInterface);
|
||||
CharacterLCD blank = of(lcdSize, lcdInterface);
|
||||
blank.setScreen(Display.NONE);
|
||||
|
||||
Assert.assertEquals(Display.VALUE, printed.getScreen());
|
||||
Assert.assertEquals(lcdSize + ", " + lcdInterface, printed.getValueForDisplay());
|
||||
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Both inks exist on real modules -- the blue-backlit one shows light characters and the
|
||||
* yellow-green one dark -- so the text has to survive a screen colour the user picks. Two
|
||||
* backlights at opposite ends of the range must not render the same, or one of them is printing
|
||||
* its text in the backlight's own colour.
|
||||
*/
|
||||
@Test
|
||||
public void theInkFollowsTheBacklight() {
|
||||
CharacterLCD dark = new CharacterLCD();
|
||||
dark.setScreenColor(Color.decode("#1E88E5"));
|
||||
CharacterLCD pale = new CharacterLCD();
|
||||
pale.setScreenColor(Color.decode("#CDDC39"));
|
||||
|
||||
CharacterLCD darkBlank = new CharacterLCD();
|
||||
darkBlank.setScreenColor(Color.decode("#1E88E5"));
|
||||
darkBlank.setScreen(Display.NONE);
|
||||
CharacterLCD paleBlank = new CharacterLCD();
|
||||
paleBlank.setScreenColor(Color.decode("#CDDC39"));
|
||||
paleBlank.setScreen(Display.NONE);
|
||||
|
||||
MakerBoardTestSupport.assertScreenTextIsDrawn(dark, darkBlank);
|
||||
MakerBoardTestSupport.assertScreenTextIsDrawn(pale, paleBlank);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drawsCleanlyInEveryState() {
|
||||
for (LCDSize lcdSize : LCDSize.values()) {
|
||||
for (LCDInterface lcdInterface : LCDInterface.values()) {
|
||||
for (Display screen : Display.values()) {
|
||||
CharacterLCD display = of(lcdSize, lcdInterface);
|
||||
display.setScreen(screen);
|
||||
MakerBoardTestSupport.assertDrawsCleanly(display);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,9 @@ import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.components.AbstractMakerBoard;
|
||||
import org.diylc.components.maker.MakerBoardTestSupport;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
@@ -149,4 +151,20 @@ public class Nokia5110LCDTest {
|
||||
AbstractMakerBoard.PIN_ROW_FLAT_LABEL_OFFSET.convertToPixels() > AbstractMakerBoard.PIN_SIZE
|
||||
.convertToPixels());
|
||||
}
|
||||
|
||||
/**
|
||||
* The one part in the package with no variant property still has a resolution and a controller
|
||||
* worth printing, which is why it answers {@code getValueForDisplay} at all: without it the
|
||||
* glass would be blank and the BOM's value column empty.
|
||||
*/
|
||||
@Test
|
||||
public void theGlassPrintsTheResolutionAndController() {
|
||||
Nokia5110LCD printed = new Nokia5110LCD();
|
||||
Nokia5110LCD blank = new Nokia5110LCD();
|
||||
blank.setScreen(Display.NONE);
|
||||
|
||||
Assert.assertEquals("84x48 PCD8544", printed.getValueForDisplay());
|
||||
Assert.assertEquals(Display.VALUE, printed.getScreen());
|
||||
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,12 @@ import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.components.AbstractMakerBoard;
|
||||
import org.diylc.components.displays.OLEDDisplay.Layout;
|
||||
import org.diylc.components.displays.OLEDDisplay.OLEDInterface;
|
||||
import org.diylc.components.displays.OLEDDisplay.Version;
|
||||
import org.diylc.components.maker.MakerBoardTestSupport;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
@@ -434,4 +436,23 @@ public class OLEDDisplayTest {
|
||||
Assert.assertEquals("every board should get its own BOM row",
|
||||
Version.values().length * OLEDInterface.values().length, values.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* The six boards are the same dark letterbox on the same blue PCB, so the lit area carries the
|
||||
* description that tells them apart. It is rendered rather than asked for, because a screen that
|
||||
* found no room for the text would still answer the getter.
|
||||
*/
|
||||
@Test
|
||||
public void everyPanelPrintsItsDescription() {
|
||||
for (Version version : Version.values()) {
|
||||
for (OLEDInterface oledInterface : OLEDInterface.values()) {
|
||||
OLEDDisplay printed = of(version, oledInterface);
|
||||
OLEDDisplay blank = of(version, oledInterface);
|
||||
blank.setScreen(Display.NONE);
|
||||
|
||||
Assert.assertEquals(Display.VALUE, printed.getScreen());
|
||||
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.components.displays.TFTDisplay.Controller;
|
||||
import org.diylc.components.maker.MakerBoardTestSupport;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.junit.Assert;
|
||||
@@ -350,4 +352,20 @@ public class TFTDisplayTest {
|
||||
mm(fromEdge), 0.01d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every panel prints its description, the round one included -- it is the only variant whose
|
||||
* text has to fit a circle, and it does so by being given the square inscribed in the glass.
|
||||
*/
|
||||
@Test
|
||||
public void everyPanelPrintsItsDescription() {
|
||||
for (Controller controller : Controller.values()) {
|
||||
TFTDisplay printed = of(controller);
|
||||
TFTDisplay blank = of(controller);
|
||||
blank.setScreen(Display.NONE);
|
||||
|
||||
Assert.assertEquals(Display.VALUE, printed.getScreen());
|
||||
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -25,6 +25,7 @@ import java.awt.Graphics2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.diylc.common.Orientation;
|
||||
import org.diylc.components.AbstractMakerBoard;
|
||||
@@ -47,6 +48,10 @@ public class MakerBoardTestSupport {
|
||||
|
||||
public static final double PIN_SPACING = new Size(0.1d, SizeUnit.in).convertToPixels();
|
||||
|
||||
// large enough for the longest board in the package, the 86 mm 2.8" TFT, with its origin in the
|
||||
// middle
|
||||
private static final int RENDER_SIZE = 1400;
|
||||
|
||||
private static final Project PROJECT = new Project();
|
||||
|
||||
private static final IDrawingObserver OBSERVER = new IDrawingObserver() {
|
||||
@@ -123,6 +128,34 @@ public class MakerBoardTestSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the board alone at a fixed position and returns the pixels. Two renderings of the same
|
||||
* board that differ in one property can then be compared, which is the only way to catch a
|
||||
* drawing change that moves no geometry -- what a board prints on its screen, for instance.
|
||||
*/
|
||||
public static int[] renderPixels(AbstractMakerBoard board) {
|
||||
BufferedImage image = new BufferedImage(RENDER_SIZE, RENDER_SIZE, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
board.setControlPoint(new Point2D.Double(RENDER_SIZE / 2.0, RENDER_SIZE / 2.0), 0);
|
||||
try {
|
||||
board.draw(g2d, ComponentState.NORMAL, false, PROJECT, OBSERVER);
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
return image.getRGB(0, 0, RENDER_SIZE, RENDER_SIZE, null, 0, RENDER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a board prints its description on its lit area, and that turning the property off
|
||||
* takes it away again. Comparing two renderings is what makes this a check on the drawing rather
|
||||
* than on the property: a screen that silently refused to print would pass the getter.
|
||||
*/
|
||||
public static void assertScreenTextIsDrawn(AbstractMakerBoard withText,
|
||||
AbstractMakerBoard withoutText) {
|
||||
Assert.assertFalse("The lit area should carry the description, and does not",
|
||||
Arrays.equals(renderPixels(withText), renderPixels(withoutText)));
|
||||
}
|
||||
|
||||
/** 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);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
*
|
||||
* 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.maker;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import org.diylc.common.Display;
|
||||
import org.diylc.components.MakerBoardPainter;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* What {@code MakerBoardPainter.drawScreenText} promises, independently of the four displays that
|
||||
* call it. The rule worth pinning down is that it prints nothing at all rather than a crop: a
|
||||
* panel too small for its own description is left dark, the same way a pin name with nowhere to go
|
||||
* is left off the silkscreen.
|
||||
*/
|
||||
public class ScreenTextTest {
|
||||
|
||||
private static final String VALUE = "0.96\" SSD1306 128x64, I2C";
|
||||
|
||||
private static int inkedPixels(Rectangle2D area, Display display, String name, String value) {
|
||||
BufferedImage image = new BufferedImage(600, 400, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
try {
|
||||
MakerBoardPainter.drawScreenText(g2d, area, Color.WHITE, display, name, value);
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
int count = 0;
|
||||
for (int rgb : image.getRGB(0, 0, 600, 400, null, 0, 600)) {
|
||||
if ((rgb >>> 24) != 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPanelWithRoomPrintsItsDescription() {
|
||||
Assert.assertTrue(
|
||||
inkedPixels(new Rectangle2D.Double(10, 10, 400, 200), Display.VALUE, "DISP1", VALUE) > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPanelWithoutRoomPrintsNothing() {
|
||||
Assert.assertEquals(0,
|
||||
inkedPixels(new Rectangle2D.Double(10, 10, 24, 10), Display.VALUE, "DISP1", VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPanelTooShallowForTheBlockPrintsNothing() {
|
||||
// wide enough for every line, but with room for one of the two
|
||||
Assert.assertEquals(0,
|
||||
inkedPixels(new Rectangle2D.Double(10, 10, 400, 16), Display.VALUE, "DISP1", VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noneLeavesTheScreenDark() {
|
||||
Assert.assertEquals(0,
|
||||
inkedPixels(new Rectangle2D.Double(10, 10, 400, 200), Display.NONE, "DISP1", VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPartWithNoVariantPrintsNothingForItsValue() {
|
||||
Assert.assertEquals(0,
|
||||
inkedPixels(new Rectangle2D.Double(10, 10, 400, 200), Display.VALUE, "DISP1", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eachSettingPrintsSomethingDifferent() {
|
||||
Rectangle2D area = new Rectangle2D.Double(10, 10, 400, 200);
|
||||
int name = inkedPixels(area, Display.NAME, "DISP1", VALUE);
|
||||
int value = inkedPixels(area, Display.VALUE, "DISP1", VALUE);
|
||||
int both = inkedPixels(area, Display.BOTH, "DISP1", VALUE);
|
||||
|
||||
Assert.assertTrue(name > 0);
|
||||
Assert.assertTrue(value > name);
|
||||
Assert.assertTrue(both > value);
|
||||
}
|
||||
}
|
||||
@@ -840,9 +840,9 @@ for the lit colour. Category `Displays & Outputs` even though the neighbours in
|
||||
- **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.**
|
||||
|
||||
### 6.8 On-screen identification — the glass prints its own spec
|
||||
### 6.8 On-screen identification — the glass prints its own spec — **done**
|
||||
|
||||
**Decided, not yet built.** The four displays that have a lit area — `OLEDDisplay`, `TFTDisplay`,
|
||||
**Done.** The four displays that have a lit area — `OLEDDisplay`, `TFTDisplay`,
|
||||
`CharacterLCD`, `Nokia5110LCD` — draw their variant string inside it, in the panel's own lit colour,
|
||||
on by default and switchable off per component. This amends D4 for text that *identifies the part*
|
||||
and leaves D4's ban on invented content standing.
|
||||
@@ -908,9 +908,9 @@ measure, in pixels:
|
||||
| 16x2 LCD | 508 × 114 |
|
||||
| 20x4 LCD | 606 × 198 |
|
||||
|
||||
The tightest is the 0.91" OLED, and two lines of the 11 px `SILK_FONT` need 22 px of its 44. So the
|
||||
drop-rather-than-squeeze rule holds here as it does for silkscreen, but nothing in the package
|
||||
currently triggers it. The round GC9A01 is the one case where width is not the board's width: text
|
||||
The tightest is the 0.91" OLED: two lines of the 11 px `SILK_FONT` need 28 px of its 44, measured
|
||||
rather than estimated. So the drop-rather-than-squeeze rule holds here as it does for silkscreen,
|
||||
but nothing in the package triggers it — every variant was rendered and looked at. The round GC9A01 is the one case where width is not the board's width: text
|
||||
must fit the inscribed square of the 256 px disc, not its diameter.
|
||||
|
||||
**Ink colour is each class's to pass.** The OLED's `PIXEL_BLUE` on its near-black active area, white
|
||||
@@ -940,6 +940,36 @@ before it.
|
||||
**Effort.** ~0.5 day for the helper, the property on four classes and the tests. ~0.5 day more for
|
||||
the character-grid refinement, which is independent and can follow.
|
||||
|
||||
**What the build settled.** Four things were decided at the keyboard rather than here:
|
||||
|
||||
1. *The line break is the comma, not the width.* The variant string reads "size, interface", so
|
||||
breaking it there gives one property per line. Left to wrap on width alone every current variant
|
||||
came out as a single line filling 95% of the glass — the 0.96" OLED's description is 153 px
|
||||
against 162 px of usable panel — which is legible but reads as a line of text crammed into a
|
||||
window rather than a readout.
|
||||
2. *The width check happens after wrapping, not before.* `StringUtils.wrap` cannot break a single
|
||||
word wider than the limit and hands one back oversized, so the helper measures each wrapped line
|
||||
and bails if any still overflows. Without it the drop rule would have had a hole in exactly the
|
||||
case it exists for.
|
||||
3. *The Character LCD's ink is chosen by luminance,* through `CalcUtils.calculateLuminance` on the
|
||||
screen colour. Both inks are real parts — the blue-backlit module shows light characters, the
|
||||
yellow-green one dark — so this is faithful rather than merely defensive, and it is what stops a
|
||||
pale backlight from being printed on in white.
|
||||
4. *`Nokia5110LCD` gained the `getVariantLabel()` this section asked for,* returning
|
||||
`84x48 PCD8544`, which also fills the BOM value column §11 item 1 left empty on it.
|
||||
|
||||
The character-grid refinement is **not** done; it remains the open item above.
|
||||
|
||||
**Tests.** `ScreenTextTest` covers the helper's own contract away from any component — that a panel
|
||||
with room prints, that one too narrow or too shallow prints nothing at all, that `NONE` and an
|
||||
empty variant leave the glass dark, and that the three printing settings differ. Each of the four
|
||||
display tests then renders its panels twice, once printing and once with `Display.NONE`, and fails
|
||||
if the two renderings match; rendering is what makes it a check on the drawing rather than on the
|
||||
getter, since a screen that silently found no room would answer the getter either way. The shared
|
||||
`MakerBoardTestSupport.renderPixels` added for this is reusable by anything else that needs to
|
||||
catch a drawing change that moves no geometry. `CharacterLCDTest` is new — the class had no test at
|
||||
all — and also pins down the two inks.
|
||||
|
||||
### 6.9 Deliberately not in this slice
|
||||
|
||||
E-paper (1.54" / 2.9" SSD1680) — growing but still niche, and the flexible-cable mounting is real
|
||||
|
||||
Reference in New Issue
Block a user