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; }