P-0+o (Proto) Board, Proto Board 780 Holes and Eurorack Stripboard now have an Underside Display like the other boards, with the P-0+o's top copper faded in the mirror; the P-0+o credits no longer run off the board and show the designer's name correctly

This commit is contained in:
Bane Stojkovic
2026-11-12 23:13:35 +01:00
parent fbbd468989
commit 2b37bd172e
10 changed files with 345 additions and 7 deletions
@@ -50,7 +50,6 @@ import org.diylc.core.measures.SizeUnit;
public abstract class AbstractBoard extends AbstractTransparentComponent<String> implements IBoard {
private static final long serialVersionUID = 1L;
public static final Size DEFAULT_UNDERSIDE_OFFSET = new Size(0.1, SizeUnit.in);
public static Color BORDER_COLOR = PHENOLIC_COLOR.darker();
public static Color COORDINATE_COLOR = Color.gray.brighter();
@@ -0,0 +1,111 @@
/*
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.boards;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.core.BoardUndersideDisplay;
import org.diylc.core.IBoard;
import org.diylc.core.annotations.AiNote;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.measures.Size;
/**
* A prototyping board that parts are soldered to, which can show its underside next to it like the
* boards that extend {@link AbstractBoard}. The underside properties are declared with the same
* names as there, so both kinds of boards offer them in the same way.
*
* @author Branislav Stojkovic
*/
@AiNote("Set 'Underside Display' only if explicitly asked to. The values other than 'NONE' render the mirror image of the board in the provided direction with the offset controlled by 'Underside Offset'")
public abstract class AbstractSolderedProtoBoard extends AbstractProtoBoard implements IBoard {
private static final long serialVersionUID = 1L;
protected BoardUndersideDisplay boardUndersideDisplay = BoardUndersideDisplay.NONE;
protected Size undersideOffset = IBoard.DEFAULT_UNDERSIDE_OFFSET;
private Boolean undersideTransparency = true;
/**
* @return the body of the board as drawn before it is turned to its orientation
*/
protected abstract Rectangle2D getUnrotatedBody();
/**
* @return the point the board is turned around to its orientation
*/
protected abstract Point2D getRotationCenter();
@Override
public Rectangle2D getBoardRectangle() {
Point2D center = getRotationCenter();
AffineTransform rotation = AffineTransform.getRotateInstance(getOrientation().toRadians(),
center.getX(), center.getY());
return rotation.createTransformedShape(getUnrotatedBody()).getBounds2D();
}
@EditableProperty(name = "Underside Display")
@Override
public BoardUndersideDisplay getUndersideDisplay() {
if (boardUndersideDisplay == null) {
boardUndersideDisplay = BoardUndersideDisplay.NONE;
}
return boardUndersideDisplay;
}
public void setUndersideDisplay(BoardUndersideDisplay boardUndersideDisplay) {
this.boardUndersideDisplay = boardUndersideDisplay;
}
@EditableProperty(name = "Underside Offset")
@Override
public Size getUndersideOffset() {
if (undersideOffset == null) {
undersideOffset = IBoard.DEFAULT_UNDERSIDE_OFFSET;
}
return undersideOffset;
}
public void setUndersideOffset(Size undersideOffset) {
this.undersideOffset = undersideOffset;
}
@EditableProperty(name = "Underside Transparency")
@Override
public Boolean getUndersideTransparency() {
if (undersideTransparency == null) {
undersideTransparency = true;
}
return undersideTransparency;
}
public void setUndersideTransparency(Boolean undersideTransparency) {
this.undersideTransparency = undersideTransparency;
}
@Override
public boolean shouldExportToGerber() {
return false;
}
}
@@ -55,7 +55,7 @@ import org.diylc.core.measures.SizeUnit;
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, enableCache = true,
transformer = ProtoBoardTransformer.class)
public class EurorackStripboard extends AbstractProtoBoard {
public class EurorackStripboard extends AbstractSolderedProtoBoard {
private static final long serialVersionUID = 1L;
@@ -720,6 +720,18 @@ public class EurorackStripboard extends AbstractProtoBoard {
return null;
}
@Override
protected Rectangle2D getUnrotatedBody() {
int bodyOffsetY = (int) BODY_OFFSET_Y.convertToPixels();
return new Rectangle2D.Double(point.getX(), point.getY() - bodyOffsetY,
(int) WIDTH_SIZE.convertToPixels(), (int) LENGTH_SIZE.convertToPixels());
}
@Override
protected Point2D getRotationCenter() {
return point;
}
@Override
public Rectangle2D getCachingBounds() {
Point2D finalSecondPoint = getControlPoint(1);
@@ -53,7 +53,7 @@ import org.diylc.core.measures.SizeUnit;
description = "A prototyping board for modular synths, central bus with V+ GND and V-", instanceNamePrefix = "BB",
zOrder = IDIYComponent.BOARD, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, enableCache = true, transformer = ProtoBoardTransformer.class)
public class ProtoBoard extends AbstractProtoBoard {
public class ProtoBoard extends AbstractSolderedProtoBoard {
private static final long serialVersionUID = 1L;
@@ -146,6 +146,7 @@ public class ProtoBoard extends AbstractProtoBoard {
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(padSize / 4));
drawingObserver.startTrackingContinuityArea(true);
drawingObserver.startDrawingSide(PCBLayer._2);
// draw segment connectors
for (int y = 0; y < ROW_COUNT; y++) {
@@ -170,6 +171,7 @@ public class ProtoBoard extends AbstractProtoBoard {
(int) (point.getX() + (1 + SEGMENTS_SUM[3] + 2.5) * spacing), (int) (point.getY() + (yOffset + y - 0.25d) * spacing + 1));
}
drawingObserver.stopDrawingSide();
drawingObserver.startDrawingSide(PCBLayer._1);
g2d.setColor(undersideTraceColor);
@@ -187,6 +189,7 @@ public class ProtoBoard extends AbstractProtoBoard {
drawingObserver.stopDrawingSide();
g2d.setColor(traceColor);
drawingObserver.startDrawingSide(PCBLayer._2);
// traces to top and bottom 3 pads
g2d.drawPolyline(new int[] {
@@ -223,6 +226,7 @@ public class ProtoBoard extends AbstractProtoBoard {
(int) (point.getY() + (yOffset + ROW_COUNT) * spacing),
(int) (point.getY() + (yOffset + ROW_COUNT - 0.35) * spacing)}, 3);
drawingObserver.stopDrawingSide();
drawingObserver.stopTrackingContinuityArea();
int miniPadSize = getClosestOdd((2d * spacing - 6) / 5);
@@ -231,12 +235,14 @@ public class ProtoBoard extends AbstractProtoBoard {
g2d.setColor(traceColor);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(padSize / 4));
drawingObserver.startTrackingContinuityArea(true);
drawingObserver.startDrawingSide(PCBLayer._2);
g2d.drawLine((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 0.5) * spacing), (int) Math.round(point.getY() + 1 * spacing),
(int) (point.getX() + (1 + SEGMENTS_SUM[3] + 0.5) * spacing), (int) Math.round(point.getY() + 3.5 * spacing));
g2d.drawLine((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing + miniPadSize / 2), (int) Math.round(point.getY() + 1 * spacing),
(int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing + miniPadSize / 2), (int) Math.round(point.getY() + 3.5 * spacing));
g2d.drawLine((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 2.5) * spacing), (int) Math.round(point.getY() + 1 * spacing),
(int) (point.getX() + (1 + SEGMENTS_SUM[3] + 2.5) * spacing), (int) Math.round(point.getY() + 3.5 * spacing));
drawingObserver.stopDrawingSide();
drawingObserver.stopTrackingContinuityArea();
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
@@ -354,8 +360,10 @@ public class ProtoBoard extends AbstractProtoBoard {
drawingObserver.startTrackingContinuityArea(true);
// Draw two horizontal traces in the top-right section
drawingObserver.startDrawingSide(PCBLayer._2);
g2d.drawLine((int) Math.round(point.getX() + width - 4 * spacing), (int) Math.round(point.getY() + 0.5 * spacing), (int) (point.getX() + width - (2) * spacing), (int) Math.round(point.getY() + 0.5 * spacing));
g2d.drawLine((int) Math.round(point.getX() + width - 4 * spacing), (int) Math.round(point.getY() + 1.5 * spacing), (int) (point.getX() + width - (2) * spacing), (int) Math.round(point.getY() + 1.5 * spacing));
drawingObserver.stopDrawingSide();
// Draw top-right pads
for (int x = 0; x < 6; x++) {
@@ -363,7 +371,9 @@ public class ProtoBoard extends AbstractProtoBoard {
g2d.setColor(traceColor);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(padSize / 4));
drawingObserver.startDrawingSide(PCBLayer._2);
g2d.drawLine(padX, (int) Math.round(point.getY() + 0.5 * spacing), padX, (int) Math.round(point.getY() + 3 * spacing));
drawingObserver.stopDrawingSide();
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
@@ -473,7 +483,7 @@ public class ProtoBoard extends AbstractProtoBoard {
g2d.setColor(textColor);
g2d.setFont(LABEL_FONT.deriveFont(COORDINATE_FONT_SIZE + 2));
StringUtils.drawCenteredText(g2d, "P-0+o by:\nKristian Bl�sol 2019", (int) Math.round(point.getX() + width * 0.70d), (int) (point.getY() + (yOffset + ROW_COUNT + 0.9) * spacing),
StringUtils.drawCenteredText(g2d, "P-0+o by:\nKristian Bl\u00e5sol 2019", (int) Math.round(point.getX() + spacing / 2), (int) (point.getY() + (yOffset + ROW_COUNT + 0.9) * spacing),
HorizontalAlignment.LEFT, VerticalAlignment.TOP);
// draw central mini pads
@@ -481,6 +491,7 @@ public class ProtoBoard extends AbstractProtoBoard {
g2d.setColor(padColor);
drawingObserver.startTrackingContinuityArea(true);
drawingObserver.startDrawingSide(PCBLayer._2);
// top row
g2d.fillRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 0.5) * spacing) + 1, (int) (point.getY() + (yOffset + y - 0.5d) * spacing + 2), miniPadSize, miniPadSize);
@@ -496,6 +507,7 @@ public class ProtoBoard extends AbstractProtoBoard {
g2d.setColor(padColor.darker());
g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 0.5) * spacing) + 1, (int) (point.getY() + (yOffset + y - 0.5d) * spacing + 2), miniPadSize, miniPadSize);
g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 0.5) * spacing) + miniPadSize + 3, (int) (point.getY() + (yOffset + y - 0.5d) * spacing + 2), miniPadSize, miniPadSize);
drawingObserver.stopDrawingSide();
drawingObserver.startDrawingSide(PCBLayer._1);
g2d.setColor(undersidePadColor);
@@ -696,6 +708,17 @@ public class ProtoBoard extends AbstractProtoBoard {
return null;
}
@Override
protected Rectangle2D getUnrotatedBody() {
return new Rectangle2D.Double(point.getX(), point.getY(), (int) WIDTH_SIZE.convertToPixels(),
(int) LENGTH_SIZE.convertToPixels());
}
@Override
protected Point2D getRotationCenter() {
return point;
}
@Override
public Rectangle2D getCachingBounds() {
Point2D finalSecondPoint = getControlPoint(1);
@@ -50,7 +50,7 @@ import org.diylc.core.measures.SizeUnit;
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, enableCache = true,
transformer = ProtoBoardTransformer.class)
public class ProtoBoard780 extends AbstractProtoBoard {
public class ProtoBoard780 extends AbstractSolderedProtoBoard {
private static final long serialVersionUID = 1L;
@@ -519,6 +519,29 @@ public class ProtoBoard780 extends AbstractProtoBoard {
return null;
}
@Override
protected Rectangle2D getUnrotatedBody() {
return new Rectangle2D.Double(point.getX(), point.getY(), (int) WIDTH_SIZE.convertToPixels(),
(int) LENGTH_SIZE.convertToPixels());
}
@Override
protected Point2D getRotationCenter() {
return point;
}
@Override
public Rectangle2D getBoardRectangle() {
Rectangle2D rect = super.getBoardRectangle();
if (!translateToGrid) {
return rect;
}
// the whole board is drawn shifted onto the grid, after it is turned
rect.setRect(rect.getX() - X_OFFSET.convertToPixels(),
rect.getY() + SPACING.convertToPixels() / 2, rect.getWidth(), rect.getHeight());
return rect;
}
@Override
public Rectangle2D getCachingBounds() {
double spacing = SPACING.convertToPixels();
@@ -0,0 +1,115 @@
/*
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.boards;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import org.diylc.common.Orientation;
import org.diylc.core.BoardUndersideDisplay;
import org.diylc.core.IBoard;
import org.diylc.serialization.ProjectFileManager;
import org.junit.Assert;
import org.junit.Test;
/**
* The prototyping boards parts are soldered to show their underside like other boards, and their
* board rectangle, which places the mirror, follows the board as it is turned.
*/
public class SolderedProtoBoardTest {
private static final double DELTA = 1e-6;
private static XStream createXStream() {
XStream xStream = new XStream(new DomDriver("UTF-8"));
ProjectFileManager.configure(xStream);
return xStream;
}
@Test
public void solderlessBreadboardHasNoUnderside() {
Assert.assertFalse(IBoard.class.isAssignableFrom(Breadboard.class));
}
@Test
public void boardRectangleIsTheBodyAtDefaultOrientation() {
ProtoBoard board = new ProtoBoard();
board.setControlPoint(new Point2D.Double(100, 50), 0);
assertRect(100, 50, ProtoBoard.WIDTH_SIZE.convertToPixels(),
ProtoBoard.LENGTH_SIZE.convertToPixels(), board.getBoardRectangle());
}
@Test
public void boardRectangleTurnsWithTheBoard() {
EurorackStripboard board = new EurorackStripboard();
board.setControlPoint(new Point2D.Double(1000, 50), 0);
board.setOrientation(Orientation._90);
double width = (int) EurorackStripboard.WIDTH_SIZE.convertToPixels();
double length = (int) EurorackStripboard.LENGTH_SIZE.convertToPixels();
double offset = (int) EurorackStripboard.BODY_OFFSET_Y.convertToPixels();
// a quarter turn clockwise around the control point swings the body to its left
assertRect(1000 - length + offset, 50, length, width, board.getBoardRectangle());
}
@Test
public void boardRectangleFollowsTheGridShift() {
ProtoBoard780 board = new ProtoBoard780();
board.setControlPoint(new Point2D.Double(100, 50), 0);
assertRect(100 - ProtoBoard780.X_OFFSET.convertToPixels(),
50 + ProtoBoard780.SPACING.convertToPixels() / 2,
(int) ProtoBoard780.WIDTH_SIZE.convertToPixels(),
(int) ProtoBoard780.LENGTH_SIZE.convertToPixels(), board.getBoardRectangle());
}
@Test
public void boardSavedBeforeUndersideDisplayLoadsWithoutOne() {
String xml = createXStream().toXML(new EurorackStripboard());
Assert.assertTrue(xml.contains("<boardUndersideDisplay>"));
EurorackStripboard loaded = (EurorackStripboard) createXStream().fromXML(xml
.replaceAll("\\s*<boardUndersideDisplay>[^<]*</boardUndersideDisplay>", "")
.replaceAll("\\s*<undersideOffset[\\s\\S]*?</undersideOffset>", "")
.replaceAll("\\s*<undersideTransparency>[^<]*</undersideTransparency>", ""));
Assert.assertEquals(BoardUndersideDisplay.NONE, loaded.getUndersideDisplay());
Assert.assertEquals(IBoard.DEFAULT_UNDERSIDE_OFFSET, loaded.getUndersideOffset());
Assert.assertTrue(loaded.getUndersideTransparency());
}
@Test
public void undersideDisplayIsSaved() {
ProtoBoard board = new ProtoBoard();
board.setUndersideDisplay(BoardUndersideDisplay.BELOW);
ProtoBoard loaded = (ProtoBoard) createXStream().fromXML(createXStream().toXML(board));
Assert.assertEquals(BoardUndersideDisplay.BELOW, loaded.getUndersideDisplay());
}
private static void assertRect(double x, double y, double width, double height,
Rectangle2D actual) {
Assert.assertEquals(x, actual.getX(), DELTA);
Assert.assertEquals(y, actual.getY(), DELTA);
Assert.assertEquals(width, actual.getWidth(), DELTA);
Assert.assertEquals(height, actual.getHeight(), DELTA);
}
}