dot matrix improvements

This commit is contained in:
Bane Stojkovic
2026-10-09 14:21:29 +02:00
parent 9922615de5
commit cd9ac32a2f
8 changed files with 624 additions and 24 deletions
@@ -343,7 +343,7 @@ public class MakerBoardPainter {
* 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) {
public static String screenText(Display display, String name, String value) {
boolean hasValue = value != null && !value.trim().isEmpty();
String variant = hasValue ? value.replace(", ", "\n") : null;
@@ -30,6 +30,7 @@ import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.List;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
@@ -304,11 +305,7 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.setColor(screenColor);
g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
Rectangle2D screenArea = new Rectangle2D.Double(screenX, screenY, screenW, screenH);
drawCharacterMatrix(g2d, screenArea);
MakerBoardPainter.drawScreenText(g2d, screenArea, getScreenInk(), getScreen(), getName(),
getValueForDisplay());
drawCharacterMatrix(g2d, new Rectangle2D.Double(screenX, screenY, screenW, screenH));
// Only the parallel row has anywhere to print. The backpack's column sits 2.5 mm from the
// left edge with the bezel 1.4 mm further in, so a label beside it would lie across the metal
@@ -327,7 +324,8 @@ public class CharacterLCD extends AbstractMakerBoard {
}
/**
* The unlit dot grid, centred in the lit area. The matrix is narrower than the glass on both
* The character grid, centred in the lit area: every cell's dots in the unlit shade first, then
* the dots the text lights over the top of them. The matrix is narrower than the glass on both
* modules, which is why it is centred rather than inset by a margin.
*/
private void drawCharacterMatrix(Graphics2D g2d, Rectangle2D screenArea) {
@@ -343,7 +341,8 @@ public class CharacterLCD extends AbstractMakerBoard {
double matrixX = screenArea.getX() + (screenArea.getWidth() - matrixWidth) / 2.0;
double matrixY = screenArea.getY() + (screenArea.getHeight() - matrixHeight) / 2.0;
// The 20x4 comes to 3200 dots, so the rectangle is reused rather than allocated per dot.
// The 20x4 comes to 3200 dots, so the rectangle is reused rather than allocated per dot, and
// the two shades are laid down in one pass each rather than a colour change per dot.
Rectangle2D.Double dot = new Rectangle2D.Double(0, 0, dotSize, dotSize);
g2d.setColor(getDotColor());
@@ -360,6 +359,33 @@ public class CharacterLCD extends AbstractMakerBoard {
}
}
}
List<String> lines = DotMatrixScreen.layOutText(getScreen(), getName(),
getValueForDisplay(), lcdSize.getColumns(), lcdSize.getRows());
if (lines.isEmpty()) {
return;
}
g2d.setColor(getScreenInk());
int firstRow = (lcdSize.getRows() - lines.size()) / 2;
for (int line = 0; line < lines.size(); line++) {
String text = lines.get(line);
int firstColumn = (lcdSize.getColumns() - text.length()) / 2;
double cellY = matrixY + (firstRow + line) * charPitchY;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
double cellX = matrixX + (firstColumn + i) * charPitchX;
for (int dotRow = 0; dotRow < DotMatrixFont.GLYPH_HEIGHT; dotRow++) {
for (int dotColumn = 0; dotColumn < DotMatrixFont.GLYPH_WIDTH; dotColumn++) {
if (DotMatrixFont.isDotLit(c, dotRow, dotColumn)) {
dot.x = cellX + dotColumn * dotPitch;
dot.y = cellY + dotRow * dotPitch;
g2d.fill(dot);
}
}
}
}
}
}
@Override
@@ -0,0 +1,158 @@
/*
*
* 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;
/**
* The glyphs an HD44780 character LCD paints in its 5x8 cell, one entry per printable ASCII
* character. A glyph is stored as five columns, one per dot column, with bit 0 the top row -- the
* order the controller itself clocks them out.
*
* <p>Most glyphs sit on a baseline in the seventh row and leave the eighth empty, which is where
* the cursor would sit. The ones that descend below the baseline on a real module -- the lower-case
* g, j, p, q and y, the comma, the semicolon and the underscore -- do use that eighth row, so they
* are drawn to the full eight.
*
* <p>The table is spelled out rather than sampled from a system font because a rendered project has
* to come out identical on every machine, and a font that may or may not be installed cannot
* promise that.
*
* @author Branislav Stojkovic
*/
public class DotMatrixFont {
public static final int GLYPH_WIDTH = 5;
public static final int GLYPH_HEIGHT = 8;
private static final char FIRST_CHAR = ' ';
private static final char LAST_CHAR = '~';
// what stands in for anything the table has no glyph for, as the controller's ROM does
private static final char FALLBACK_CHAR = '?';
private static final int[] GLYPH_COLUMNS = new int[] {
0x00, 0x00, 0x00, 0x00, 0x00, // space
0x00, 0x00, 0x5F, 0x00, 0x00, // !
0x00, 0x07, 0x00, 0x07, 0x00, // "
0x14, 0x7F, 0x14, 0x7F, 0x14, // #
0x24, 0x2A, 0x7F, 0x2A, 0x12, // $
0x23, 0x13, 0x08, 0x64, 0x62, // %
0x36, 0x49, 0x55, 0x22, 0x50, // &
0x00, 0x05, 0x03, 0x00, 0x00, // '
0x00, 0x1C, 0x22, 0x41, 0x00, // (
0x00, 0x41, 0x22, 0x1C, 0x00, // )
0x14, 0x08, 0x3E, 0x08, 0x14, // *
0x08, 0x08, 0x3E, 0x08, 0x08, // +
0x00, 0x80, 0xC0, 0x40, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, // -
0x00, 0x60, 0x60, 0x00, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, // /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
0x42, 0x61, 0x51, 0x49, 0x46, // 2
0x21, 0x41, 0x45, 0x4B, 0x31, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
0x27, 0x45, 0x45, 0x45, 0x39, // 5
0x3C, 0x4A, 0x49, 0x49, 0x30, // 6
0x01, 0x71, 0x09, 0x05, 0x03, // 7
0x36, 0x49, 0x49, 0x49, 0x36, // 8
0x06, 0x49, 0x49, 0x29, 0x1E, // 9
0x00, 0x36, 0x36, 0x00, 0x00, // :
0x80, 0x76, 0x36, 0x00, 0x00, // ;
0x00, 0x08, 0x14, 0x22, 0x41, // <
0x14, 0x14, 0x14, 0x14, 0x14, // =
0x41, 0x22, 0x14, 0x08, 0x00, // >
0x02, 0x01, 0x51, 0x09, 0x06, // ?
0x32, 0x49, 0x79, 0x41, 0x3E, // @
0x7E, 0x11, 0x11, 0x11, 0x7E, // A
0x7F, 0x49, 0x49, 0x49, 0x36, // B
0x3E, 0x41, 0x41, 0x41, 0x22, // C
0x7F, 0x41, 0x41, 0x22, 0x1C, // D
0x7F, 0x49, 0x49, 0x49, 0x41, // E
0x7F, 0x09, 0x09, 0x09, 0x01, // F
0x3E, 0x41, 0x49, 0x49, 0x7A, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
0x00, 0x41, 0x7F, 0x41, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, // J
0x7F, 0x08, 0x14, 0x22, 0x41, // K
0x7F, 0x40, 0x40, 0x40, 0x40, // L
0x7F, 0x02, 0x0C, 0x02, 0x7F, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
0x7F, 0x09, 0x09, 0x09, 0x06, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, // R
0x46, 0x49, 0x49, 0x49, 0x31, // S
0x01, 0x01, 0x7F, 0x01, 0x01, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
0x3F, 0x40, 0x38, 0x40, 0x3F, // W
0x63, 0x14, 0x08, 0x14, 0x63, // X
0x07, 0x08, 0x70, 0x08, 0x07, // Y
0x61, 0x51, 0x49, 0x45, 0x43, // Z
0x00, 0x7F, 0x41, 0x41, 0x00, // [
0x02, 0x04, 0x08, 0x10, 0x20, // \
0x00, 0x41, 0x41, 0x7F, 0x00, // ]
0x04, 0x02, 0x01, 0x02, 0x04, // ^
0x80, 0x80, 0x80, 0x80, 0x80, // _
0x00, 0x01, 0x02, 0x04, 0x00, // `
0x20, 0x54, 0x54, 0x54, 0x78, // a
0x7F, 0x48, 0x44, 0x44, 0x38, // b
0x38, 0x44, 0x44, 0x44, 0x20, // c
0x38, 0x44, 0x44, 0x48, 0x7F, // d
0x38, 0x54, 0x54, 0x54, 0x18, // e
0x08, 0x7E, 0x09, 0x01, 0x02, // f
0x18, 0xA4, 0xA4, 0xA4, 0x7C, // g
0x7F, 0x08, 0x04, 0x04, 0x78, // h
0x00, 0x44, 0x7D, 0x40, 0x00, // i
0x40, 0x80, 0x80, 0x7D, 0x00, // j
0x7F, 0x10, 0x28, 0x44, 0x00, // k
0x00, 0x41, 0x7F, 0x40, 0x00, // l
0x7C, 0x04, 0x18, 0x04, 0x78, // m
0x7C, 0x08, 0x04, 0x04, 0x78, // n
0x38, 0x44, 0x44, 0x44, 0x38, // o
0xFC, 0x24, 0x24, 0x24, 0x18, // p
0x18, 0x24, 0x24, 0x24, 0xFC, // q
0x7C, 0x08, 0x04, 0x04, 0x08, // r
0x48, 0x54, 0x54, 0x54, 0x20, // s
0x04, 0x3F, 0x44, 0x40, 0x20, // t
0x3C, 0x40, 0x40, 0x20, 0x7C, // u
0x1C, 0x20, 0x40, 0x20, 0x1C, // v
0x3C, 0x40, 0x30, 0x40, 0x3C, // w
0x44, 0x28, 0x10, 0x28, 0x44, // x
0x1C, 0xA0, 0xA0, 0xA0, 0x7C, // y
0x44, 0x64, 0x54, 0x4C, 0x44, // z
0x00, 0x08, 0x36, 0x41, 0x00, // {
0x00, 0x00, 0x7F, 0x00, 0x00, // |
0x00, 0x41, 0x36, 0x08, 0x00, // }
0x08, 0x08, 0x2A, 0x1C, 0x08, // ~
};
private DotMatrixFont() {}
/** Whether the glyph for a character lights the dot at the given row and column of its cell. */
public static boolean isDotLit(char c, int row, int column) {
if (row < 0 || row >= GLYPH_HEIGHT || column < 0 || column >= GLYPH_WIDTH) {
return false;
}
char glyph = c < FIRST_CHAR || c > LAST_CHAR ? FALLBACK_CHAR : c;
return (GLYPH_COLUMNS[(glyph - FIRST_CHAR) * GLYPH_WIDTH + column] >> row & 1) != 0;
}
}
@@ -0,0 +1,134 @@
/*
*
* 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.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.diylc.common.Display;
import org.diylc.components.MakerBoardPainter;
/**
* Text as a dot-matrix screen actually shows it: folded into the character grid the panel has room
* for, then painted one lit pixel at a time out of {@link DotMatrixFont}.
*
* <p>Laying the text out is shared by every such screen; painting it is not. A graphic panel like
* the Nokia's addresses one unbroken grid of pixels, and {@link #drawText} serves those. A
* character module instead has gaps between its cells, so it walks its own cells and only borrows
* the layout.
*
* @author Branislav Stojkovic
*/
public class DotMatrixScreen {
/** A glyph and the blank column that separates it from the next, as the panel fonts are drawn. */
public static final int CELL_WIDTH = DotMatrixFont.GLYPH_WIDTH + 1;
/** Rows butt against each other; the glyph's own last row is the gap. */
public static final int CELL_HEIGHT = DotMatrixFont.GLYPH_HEIGHT;
private DotMatrixScreen() {}
/**
* What the screen spells out, broken into grid rows. A line too wide for the grid is folded at a
* space where there is one and cut where there is not, and whatever runs past the last row is
* dropped: a real panel shows what fits and nothing more.
*/
public static List<String> layOutText(Display display, String name, String value, int columns,
int rows) {
String text = MakerBoardPainter.screenText(display, name, value);
if (text == null || columns <= 0 || rows <= 0) {
return Collections.emptyList();
}
List<String> lines = new ArrayList<String>();
for (String paragraph : text.split("\n")) {
String remaining = paragraph.trim();
while (!remaining.isEmpty() && lines.size() < rows) {
if (remaining.length() <= columns) {
lines.add(remaining);
break;
}
int fold = remaining.lastIndexOf(' ', columns);
if (fold <= 0) {
fold = columns;
}
lines.add(remaining.substring(0, fold).trim());
remaining = remaining.substring(fold).trim();
}
}
return lines;
}
/**
* Paints laid-out text onto an unbroken pixel grid, centred in it. The caller has already set the
* ink colour and placed the grid; this only decides which pixels light.
*
* <p>Centring is worked out in pixels rather than whole cells. A panel addresses single pixels,
* and a line that nearly fills the grid has no whole cell to spare, so rounding to one would pin
* it against the left edge and leave the slack -- the trailing cell's blank column, plus whatever
* the cell width does not divide into the panel -- piled up on the right.
*
* @param lines text from {@link #layOutText}
* @param pixelsX pixels the panel is wide
* @param pixelsY pixels the panel is tall
* @param x left edge of the pixel grid
* @param y top edge of the pixel grid
* @param pixelWidth width of one display pixel
* @param pixelHeight height of one display pixel, which need not match its width
*/
public static void drawText(Graphics2D g2d, List<String> lines, int pixelsX, int pixelsY,
double x, double y, double pixelWidth, double pixelHeight) {
if (lines.isEmpty()) {
return;
}
// a panel runs to thousands of pixels, so the rectangle is reused rather than allocated per dot
Rectangle2D.Double pixel = new Rectangle2D.Double(0, 0, pixelWidth, pixelHeight);
// the offsets land on whole pixels because that is all the controller can address
double firstRow = Math.floor((pixelsY - lines.size() * CELL_HEIGHT) / 2.0);
for (int line = 0; line < lines.size(); line++) {
String text = lines.get(line);
// the spacer column trailing the last glyph is not part of the text, so it is not centred
// with it
int lineWidth = text.length() * CELL_WIDTH - (CELL_WIDTH - DotMatrixFont.GLYPH_WIDTH);
double firstColumn = Math.floor((pixelsX - lineWidth) / 2.0);
double cellY = y + (firstRow + line * CELL_HEIGHT) * pixelHeight;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
double cellX = x + (firstColumn + i * CELL_WIDTH) * pixelWidth;
for (int dotRow = 0; dotRow < DotMatrixFont.GLYPH_HEIGHT; dotRow++) {
for (int dotColumn = 0; dotColumn < DotMatrixFont.GLYPH_WIDTH; dotColumn++) {
if (DotMatrixFont.isDotLit(c, dotRow, dotColumn)) {
pixel.x = cellX + dotColumn * pixelWidth;
pixel.y = cellY + dotRow * pixelHeight;
g2d.fill(pixel);
}
}
}
}
}
}
}
@@ -87,6 +87,11 @@ public class Nokia5110LCD extends AbstractMakerBoard {
public static Size CORNER_RADIUS = new Size(1.5d, SizeUnit.mm);
public static Size SILK_TEXT_GAP = new Size(1.0d, SizeUnit.mm);
// The PCD8544's own addressable grid. Its pixels are not square, the glass being wider than it
// is tall by more than 84:48, so the two pitches are worked out separately.
public static final int PIXELS_X = 84;
public static final int PIXELS_Y = 48;
public static final String[] PIN_NAMES =
new String[] {"RST", "CE", "DC", "DIN", "CLK", "VCC", "BL", "GND"};
@@ -253,9 +258,14 @@ 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), getScreenInk(),
getScreen(), getName(), getValueForDisplay());
// The panel has no other way to show text, so the label is drawn the way the module would
// put it there: lit pixels of its own grid rather than a smooth font laid over the glass.
int columns = PIXELS_X / DotMatrixScreen.CELL_WIDTH;
int rows = PIXELS_Y / DotMatrixScreen.CELL_HEIGHT;
g2d.setColor(getScreenInk());
DotMatrixScreen.drawText(g2d,
DotMatrixScreen.layOutText(getScreen(), getName(), getValueForDisplay(), columns, rows),
PIXELS_X, PIXELS_Y, displayX, displayY, displayW / PIXELS_X, displayH / PIXELS_Y);
// Names lie flat along the row: the strip is 3.5 mm deep and a label stood on end needs
// 4.7 mm.
@@ -29,7 +29,6 @@ import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
@@ -346,9 +345,16 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.setColor(ACTIVE_AREA_COLOR);
g2d.fill(new RoundRectangle2D.Double(activeX, activeY, activeW, activeH, 2, 2));
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(activeX, activeY, activeW, activeH), getPixelColor(), getScreen(),
getName(), getValueForDisplay());
// Drawn as lit pixels of the controller's own grid, which is the only way this panel can
// put text on the glass.
int pixelsX = version.getPixelsX();
int pixelsY = version.getPixelsY();
int columns = pixelsX / DotMatrixScreen.CELL_WIDTH;
int rows = pixelsY / DotMatrixScreen.CELL_HEIGHT;
g2d.setColor(getPixelColor());
DotMatrixScreen.drawText(g2d,
DotMatrixScreen.layOutText(getScreen(), getName(), getValueForDisplay(), columns, rows),
pixelsX, pixelsY, activeX, activeY, activeW / pixelsX, activeH / pixelsY);
drawPinNames(g2d, boardX, boardY);
}
@@ -405,15 +411,17 @@ public class OLEDDisplay extends AbstractMakerBoard {
// Panel and lit-area sizes belong to the display part and are shared by both of a size's
// boards. Everything belonging to a PCB is on the Layout, including where the lit area sits
// down the board, which the two 0.91" boards disagree on.
SSD1306_0_91("0.91\" SSD1306 128x32", 30.0d, 12.0d, 22.384d, 5.584d, 2.1d,
SSD1306_0_91("0.91\" SSD1306 128x32", 128, 32, 30.0d, 12.0d, 22.384d, 5.584d, 2.1d,
new Layout(35.8d, 12.0d, 1.5d, true, 0d, 0d, 5.0d, 0d, 2.35d),
new Layout(32.5d, 20.5d, 1.5d, false, 2.5d, 2.0d, 1.25d, 4.0d, 6.6d)),
SSD1306_0_96("0.96\" SSD1306 128x64", 26.7d, 19.26d, 21.74d, 11.0d, 2.48d,
SSD1306_0_96("0.96\" SSD1306 128x64", 128, 64, 26.7d, 19.26d, 21.74d, 11.0d, 2.48d,
new Layout(27.0d, 27.0d, 1.6d, false, 2.0d, 2.0d, 0.15d, 3.87d, 6.14d)),
SH1106_1_3("1.3\" SH1106 128x64", 35.4d, 23.0d, 29.42d, 14.7d, 2.99d,
SH1106_1_3("1.3\" SH1106 128x64", 128, 64, 35.4d, 23.0d, 29.42d, 14.7d, 2.99d,
new Layout(35.4d, 33.5d, 1.5d, false, 2.5d, 3.0d, 0d, 5.25d, 7.35d));
private final String label;
private final int pixelsX;
private final int pixelsY;
private final double panelWidthMm;
private final double panelLengthMm;
private final double activeWidthMm;
@@ -423,15 +431,18 @@ public class OLEDDisplay extends AbstractMakerBoard {
private final Layout spiLayout;
/** For a size sold on one board, which both headers are built onto. */
Version(String label, double panelWidthMm, double panelLengthMm, double activeWidthMm,
double activeLengthMm, double activeLeftInPanelMm, Layout layout) {
this(label, panelWidthMm, panelLengthMm, activeWidthMm, activeLengthMm, activeLeftInPanelMm,
layout, layout);
Version(String label, int pixelsX, int pixelsY, double panelWidthMm, double panelLengthMm,
double activeWidthMm, double activeLengthMm, double activeLeftInPanelMm, Layout layout) {
this(label, pixelsX, pixelsY, panelWidthMm, panelLengthMm, activeWidthMm, activeLengthMm,
activeLeftInPanelMm, layout, layout);
}
Version(String label, double panelWidthMm, double panelLengthMm, double activeWidthMm,
double activeLengthMm, double activeLeftInPanelMm, Layout i2cLayout, Layout spiLayout) {
Version(String label, int pixelsX, int pixelsY, double panelWidthMm, double panelLengthMm,
double activeWidthMm, double activeLengthMm, double activeLeftInPanelMm, Layout i2cLayout,
Layout spiLayout) {
this.label = label;
this.pixelsX = pixelsX;
this.pixelsY = pixelsY;
this.panelWidthMm = panelWidthMm;
this.panelLengthMm = panelLengthMm;
this.activeWidthMm = activeWidthMm;
@@ -444,6 +455,8 @@ public class OLEDDisplay extends AbstractMakerBoard {
@Override public String toString() { return label; }
public double getPanelWidthMm() { return panelWidthMm; }
public double getPanelLengthMm() { return panelLengthMm; }
public int getPixelsX() { return pixelsX; }
public int getPixelsY() { return pixelsY; }
public double getActiveWidthMm() { return activeWidthMm; }
public double getActiveLengthMm() { return activeLengthMm; }
public double getActiveLeftInPanelMm() { return activeLeftInPanelMm; }
@@ -0,0 +1,119 @@
/*
*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* The glyph table is 475 hand-entered figures, and a drawing that renders a character wrong looks
* like a rendering bug rather than a bad byte, so the table is checked here instead.
*/
public class DotMatrixFontTest {
// where a glyph without a descender sits; the row below it is the descender and cursor row
private static final int BASELINE_ROW = DotMatrixFont.GLYPH_HEIGHT - 2;
private static boolean[][] glyphOf(char c) {
boolean[][] dots = new boolean[DotMatrixFont.GLYPH_HEIGHT][DotMatrixFont.GLYPH_WIDTH];
for (int row = 0; row < DotMatrixFont.GLYPH_HEIGHT; row++) {
for (int column = 0; column < DotMatrixFont.GLYPH_WIDTH; column++) {
dots[row][column] = DotMatrixFont.isDotLit(c, row, column);
}
}
return dots;
}
private static boolean isBlank(char c) {
for (boolean[] row : glyphOf(c)) {
for (boolean lit : row) {
if (lit) {
return false;
}
}
}
return true;
}
/** A hole in the table would read as a blank cell on the glass, which is what this catches. */
@Test
public void everyPrintableCharacterHasAGlyph() {
for (char c = '!'; c <= '~'; c++) {
Assert.assertFalse("'" + c + "' has no glyph", isBlank(c));
}
Assert.assertTrue("a space must light nothing", isBlank(' '));
}
/**
* Spot-check against a glyph whose shape is unambiguous: a capital H is two uprights from the top
* row down to the baseline, joined by a bar across the middle. A table shifted by one entry fails
* here.
*/
@Test
public void theGlyphsAreTheRightWayUpAndRound() {
boolean[][] h = glyphOf('H');
for (int row = 0; row <= BASELINE_ROW; row++) {
Assert.assertTrue("left upright, row " + row, h[row][0]);
Assert.assertTrue("right upright, row " + row, h[row][4]);
// the bar joins them across the middle row only
Assert.assertEquals("middle column, row " + row, row == 3, h[row][2]);
}
}
/**
* The eighth row belongs to the descenders and to the cursor. A capital letter or a digit that
* reached into it would collide with the cursor on a real module, and one of the five lower-case
* descenders that did not reach it would be the squashed seven-row font this replaced.
*/
@Test
public void onlyTheDescendersReachTheBottomRow() {
String descenders = "gjpqy,;_";
for (char c = ' '; c <= '~'; c++) {
boolean reaches = false;
for (int column = 0; column < DotMatrixFont.GLYPH_WIDTH; column++) {
reaches |= DotMatrixFont.isDotLit(c, BASELINE_ROW + 1, column);
}
Assert.assertEquals("'" + c + "' in the bottom row", descenders.indexOf(c) >= 0, reaches);
}
}
@Test
public void nothingIsLitOutsideTheGlyph() {
for (char c : new char[] {' ', 'A', 'g', '0', '~'}) {
Assert.assertFalse("row past the cell",
DotMatrixFont.isDotLit(c, DotMatrixFont.GLYPH_HEIGHT, 0));
Assert.assertFalse("row above", DotMatrixFont.isDotLit(c, -1, 0));
Assert.assertFalse("column past the glyph",
DotMatrixFont.isDotLit(c, 0, DotMatrixFont.GLYPH_WIDTH));
Assert.assertFalse("column before the glyph", DotMatrixFont.isDotLit(c, 0, -1));
}
}
/** Anything the table has no glyph for still has to print something. */
@Test
public void charactersOutsideTheTableFallBack() {
for (char c : new char[] {'\u00e9', '\u0416', (char) 0x1F, (char) 0x7F}) {
Assert.assertArrayEquals("'" + c + "' should print as a question mark", glyphOf('?'),
glyphOf(c));
}
}
}
@@ -0,0 +1,140 @@
/*
*
* 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.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.List;
import org.diylc.common.Display;
import org.junit.Assert;
import org.junit.Test;
/**
* Folding the label into the character grid, which the character LCD and both graphic panels now
* share. A panel shows what fits and nothing else, so the rules about what gets dropped matter.
*/
public class DotMatrixScreenTest {
private static List<String> lines(String name, String value, int columns, int rows) {
return DotMatrixScreen.layOutText(Display.BOTH, name, value, columns, rows);
}
@Test
public void textThatFitsIsLeftAlone() {
Assert.assertEquals(List.of("LCD1", "16x2", "Parallel HD44780"),
lines("LCD1", "16x2, Parallel HD44780", 16, 4));
}
/** The value arrives as one string per property, and each starts its own row. */
@Test
public void everyPropertyStartsItsOwnRow() {
Assert.assertEquals(List.of("0.96\" SSD1306", "128x64", "I2C"),
DotMatrixScreen.layOutText(Display.VALUE, "OLED1", "0.96\" SSD1306, 128x64, I2C", 21, 8));
}
@Test
public void aLongLineFoldsAtASpace() {
Assert.assertEquals(List.of("LCD1", "Parallel", "HD44780"),
lines("LCD1", "Parallel HD44780", 8, 4));
}
/** A word with no space in it has to be cut, or it would run off the glass. */
@Test
public void aWordTooWideIsCut() {
Assert.assertEquals(List.of("ABCDE", "FGHIJ", "KL"),
DotMatrixScreen.layOutText(Display.NAME, "ABCDEFGHIJKL", null, 5, 4));
}
/** What runs past the last row is dropped rather than shrunk or scrolled. */
@Test
public void whatRunsPastTheLastRowIsDropped() {
Assert.assertEquals(List.of("LCD1", "16x2"), lines("LCD1", "16x2, Parallel HD44780", 16, 2));
}
/**
* Renders onto a one-pixel-per-pixel grid and reports the margins the text leaves either side,
* which is the only way to see where it actually landed.
*/
private static int[] margins(List<String> lines, int pixelsX, int pixelsY) {
BufferedImage image = new BufferedImage(pixelsX, pixelsY, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
g2d.setColor(Color.WHITE);
DotMatrixScreen.drawText(g2d, lines, pixelsX, pixelsY, 0, 0, 1, 1);
g2d.dispose();
int first = -1;
int last = -1;
for (int x = 0; x < pixelsX; x++) {
for (int y = 0; y < pixelsY; y++) {
if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
first = first == -1 ? x : first;
last = x;
break;
}
}
}
Assert.assertNotEquals("nothing was drawn", -1, first);
return new int[] {first, pixelsX - 1 - last};
}
/**
* A line that nearly fills the panel used to be pinned against the left edge: centring rounded to
* whole cells, and a cell is a glyph plus a blank column, so the slack all ended up on the right.
* The 0.96" OLED's own label is exactly that case, 20 characters on a 21-column panel.
*/
@Test
public void aNearlyFullLineIsStillCentred() {
int[] m = margins(List.of("0.96\" SSD1306 128x64"), 128, 64);
Assert.assertTrue("left " + m[0] + " vs right " + m[1], Math.abs(m[0] - m[1]) <= 1);
Assert.assertTrue("should not touch the edge, left margin was " + m[0], m[0] > 0);
}
/** The Nokia's label is 13 characters on a 14-column panel, the same case one column narrower. */
@Test
public void theNokiaLabelIsCentred() {
int[] m = margins(List.of("84x48 PCD8544"), 84, 48);
Assert.assertTrue("left " + m[0] + " vs right " + m[1], Math.abs(m[0] - m[1]) <= 1);
Assert.assertTrue("should not touch the edge, left margin was " + m[0], m[0] > 0);
}
/** A short line has whole cells to spare, and must come out centred just the same. */
@Test
public void aShortLineIsCentred() {
int[] m = margins(List.of("I2C"), 128, 64);
Assert.assertTrue("left " + m[0] + " vs right " + m[1], Math.abs(m[0] - m[1]) <= 1);
}
@Test
public void aScreenTurnedOffPrintsNothing() {
Assert.assertTrue(
DotMatrixScreen.layOutText(Display.NONE, "LCD1", "16x2", 16, 2).isEmpty());
}
/** A grid with no room at all must not throw its way out of a repaint. */
@Test
public void anEmptyGridPrintsNothing() {
Assert.assertTrue(lines("LCD1", "16x2", 0, 2).isEmpty());
Assert.assertTrue(lines("LCD1", "16x2", 16, 0).isEmpty());
}
}