diff --git a/diylc/catalog_full.json b/diylc/catalog_full.json
index 30cdbd8b1..b75878eeb 100644
--- a/diylc/catalog_full.json
+++ b/diylc/catalog_full.json
@@ -99,6 +99,7 @@
"namePrefix" : "BB",
"description" : "David Haillant's eurorack format stripboard for prototyping and building simple modules",
"creationMethod" : "SINGLE_CLICK",
+ "notes" : [ "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'" ],
"properties" : [ {
"name" : "Board Color",
"type" : "Color",
@@ -127,6 +128,21 @@
"type" : "Color",
"format" : "hex",
"defaultValue" : "#FFC593"
+ }, {
+ "name" : "Underside Display",
+ "type" : "BoardUndersideDisplay",
+ "possibleValues" : [ "NONE", "ABOVE", "BELOW", "LEFT", "RIGHT" ],
+ "defaultValue" : "NONE"
+ }, {
+ "name" : "Underside Offset",
+ "type" : "Size",
+ "format" : "(value)(unit)",
+ "possibleUnits" : [ "px", "mm", "cm", "m", "in", "ft", "yd" ],
+ "defaultValue" : "0.1in"
+ }, {
+ "name" : "Underside Transparency",
+ "type" : "Boolean",
+ "defaultValue" : "true"
} ]
}, {
"name" : "Eyelet Board",
@@ -322,7 +338,7 @@
"namePrefix" : "BB",
"description" : "A prototyping board for modular synths, central bus with V+ GND and V-",
"creationMethod" : "SINGLE_CLICK",
- "notes" : [ "The control point is the top-left corner of the 2in x 4in body and 'Orientation' rotates the board around it", "With 'Orientation' DEFAULT, pad row n (1-33) is (0.4 + n x 0.1)in below the control point. Each row is split into separately connected groups, labeled by column: 1-2, 3-5, 6-8, 9-11, 12-14 and 15-16, at 0.1-0.2, 0.4-0.6, 0.7-0.9, 1.2-1.4, 1.5-1.7 and 1.8-1.9in to the right of it" ],
+ "notes" : [ "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'", "The control point is the top-left corner of the 2in x 4in body and 'Orientation' rotates the board around it", "With 'Orientation' DEFAULT, pad row n (1-33) is (0.4 + n x 0.1)in below the control point. Each row is split into separately connected groups, labeled by column: 1-2, 3-5, 6-8, 9-11, 12-14 and 15-16, at 0.1-0.2, 0.4-0.6, 0.7-0.9, 1.2-1.4, 1.5-1.7 and 1.8-1.9in to the right of it" ],
"properties" : [ {
"name" : "Board Color",
"type" : "Color",
@@ -351,6 +367,17 @@
"type" : "Color",
"format" : "hex",
"defaultValue" : "#FFC593"
+ }, {
+ "name" : "Underside Display",
+ "type" : "BoardUndersideDisplay",
+ "possibleValues" : [ "NONE", "ABOVE", "BELOW", "LEFT", "RIGHT" ],
+ "defaultValue" : "NONE"
+ }, {
+ "name" : "Underside Offset",
+ "type" : "Size",
+ "format" : "(value)(unit)",
+ "possibleUnits" : [ "px", "mm", "cm", "m", "in", "ft", "yd" ],
+ "defaultValue" : "0.1in"
}, {
"name" : "Underside Pad Color",
"type" : "Color",
@@ -361,6 +388,10 @@
"type" : "Color",
"format" : "hex",
"defaultValue" : "#D7B2FF"
+ }, {
+ "name" : "Underside Transparency",
+ "type" : "Boolean",
+ "defaultValue" : "true"
} ]
}, {
"name" : "Perf Board w/ Pads",
@@ -465,6 +496,7 @@
"namePrefix" : "BB",
"description" : "Prototyping board similar to Radio Shack 276-168B or MPJA 33304",
"creationMethod" : "SINGLE_CLICK",
+ "notes" : [ "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'" ],
"properties" : [ {
"name" : "Board Color",
"type" : "Color",
@@ -498,6 +530,21 @@
"type" : "ProtoBoard780Type",
"possibleValues" : [ "RS", "MPJA" ],
"defaultValue" : "MPJA"
+ }, {
+ "name" : "Underside Display",
+ "type" : "BoardUndersideDisplay",
+ "possibleValues" : [ "NONE", "ABOVE", "BELOW", "LEFT", "RIGHT" ],
+ "defaultValue" : "NONE"
+ }, {
+ "name" : "Underside Offset",
+ "type" : "Size",
+ "format" : "(value)(unit)",
+ "possibleUnits" : [ "px", "mm", "cm", "m", "in", "ft", "yd" ],
+ "defaultValue" : "0.1in"
+ }, {
+ "name" : "Underside Transparency",
+ "type" : "Boolean",
+ "defaultValue" : "true"
} ]
}, {
"name" : "Tag Strip",
diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/IBoard.java b/diylc/diylc-core/src/main/java/org/diylc/core/IBoard.java
index 6d1f72229..d38f504f9 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/core/IBoard.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/core/IBoard.java
@@ -1,10 +1,13 @@
package org.diylc.core;
import org.diylc.core.measures.Size;
+import org.diylc.core.measures.SizeUnit;
import java.awt.geom.Rectangle2D;
public interface IBoard {
+
+ Size DEFAULT_UNDERSIDE_OFFSET = new Size(0.1, SizeUnit.in);
Rectangle2D getBoardRectangle();
diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml
index 8288001ac..c3c583685 100644
--- a/diylc/diylc-core/src/main/resources/update.xml
+++ b/diylc/diylc-core/src/main/resources/update.xml
@@ -669,6 +669,10 @@
IMPROVEMENT
The 'Trace' layer in the Layers menu is split into 'Bottom Traces' and 'Top Traces', so the copper of one side can be hidden, locked or selected on its own; vias, eyelets, turrets and plated pads go through the board and belong to both, and jumpers are on the top. The layer shortcuts follow the menu order now, Ctrl+1 to Ctrl+7. Projects with the old 'Trace' layer hidden or locked open with both sides hidden or locked
+
+ IMPROVEMENT
+ 'P-0+o (Proto) Board', 'Proto Board 780 Holes' and 'Eurorack Stripboard' now have an 'Underside Display' like the other boards, so their solder side can be shown and worked on next to them; the P-0+o's top traces are faded there
+
BUG_FIX
Rotated labels in pasted components and building blocks were drawn in the wrong place and at the wrong angle while following the mouse, until the components were placed
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/AbstractBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/AbstractBoard.java
index 47a485a5b..bd7f0c10b 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/AbstractBoard.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/AbstractBoard.java
@@ -50,7 +50,6 @@ import org.diylc.core.measures.SizeUnit;
public abstract class AbstractBoard extends AbstractTransparentComponent 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();
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/AbstractSolderedProtoBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/AbstractSolderedProtoBoard.java
new file mode 100644
index 000000000..77ae3e792
--- /dev/null
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/AbstractSolderedProtoBoard.java
@@ -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 .
+
+*/
+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;
+ }
+}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/EurorackStripboard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/EurorackStripboard.java
index fb29f2a41..bb80161b4 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/EurorackStripboard.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/EurorackStripboard.java
@@ -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);
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard.java
index 9398ea69a..77ad08c94 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard.java
@@ -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);
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard780.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard780.java
index 8d140b0b7..1c082f962 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard780.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard780.java
@@ -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();
diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/boards/SolderedProtoBoardTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/boards/SolderedProtoBoardTest.java
new file mode 100644
index 000000000..d5a66fdef
--- /dev/null
+++ b/diylc/diylc-library/src/test/java/org/diylc/components/boards/SolderedProtoBoardTest.java
@@ -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 .
+
+*/
+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(""));
+
+ EurorackStripboard loaded = (EurorackStripboard) createXStream().fromXML(xml
+ .replaceAll("\\s*[^<]*", "")
+ .replaceAll("\\s*", "")
+ .replaceAll("\\s*[^<]*", ""));
+ 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);
+ }
+}
diff --git a/diylc/docs/plans/copper-side-view.md b/diylc/docs/plans/copper-side-view.md
index 6314bfb01..59b89a3d8 100644
--- a/diylc/docs/plans/copper-side-view.md
+++ b/diylc/docs/plans/copper-side-view.md
@@ -32,7 +32,8 @@ were regenerated for the faded copper. The open questions below remain.
| Paint a component does not declare is component side, except a board's own paint in the mirror, which is drawn as near side | Components need no changes. A board's substrate is seen from both sides, so it must not fade in the mirror. |
| Holes and trace cuts are visible from both sides (`startDrawingBothSides()`) and never fade; a board's own holes are already solid as board paint | A faded hole reads as copper, and a faded cut hides the break the user is looking for. |
| `Jumper`, `Eyelet`, `Turret`, `Top`-layer copper and `EyeletBoard` eyelets are component side | Jumpers run over the board; eyelets and turrets are visible from the top. |
-| `ProtoBoard780` declares its traces and pads bottom side, like `PerfBoard`. `ProtoBoard` declares only the traces and pads its code paints in the underside colours. `EurorackStripboard` declares nothing | The 780-hole boards carry copper on the bottom only. `ProtoBoard` is plated through with some traces on top. Which side `EurorackStripboard`'s traces are on is not sourced. |
+| `ProtoBoard780` declares its traces and pads bottom side, like `PerfBoard`. `ProtoBoard` declares the traces and pads its code paints in the underside colours bottom side, its other traces and the pads without a hole top side, and leaves its plated pads undeclared. `EurorackStripboard` declares nothing | The 780-hole boards carry copper on the bottom only. `ProtoBoard` is plated through with some traces on top, which must fade in its mirror like any top copper. Which side `EurorackStripboard`'s traces are on is not sourced; its holes are plated, so its pads are solid in both views. |
+| `ProtoBoard780`, `ProtoBoard` and `EurorackStripboard` have an Underside Display, through `AbstractSolderedProtoBoard`; `Breadboard` does not | Parts are soldered to them, so their copper side is worth working on. A solderless breadboard has nothing on its bottom. |
| `Dot` has a Board Side property, Top by default | Dots are junctions in schematics and wiring diagrams, which must not fade, and solder joints on stripboard, which are on the copper side. Setting a solder joint's side to Bottom fades it in the main view and draws it solid in the mirror; a dot placed in the mirror goes to the bottom on its own (`bottom-side-components.md`). |
| `Breadboard`, `BlankBoard`, `MarshallPerfBoard`, `TagStrip`, `TerminalStrip` and the maker boards declare nothing | They have no bottom copper to show. |