diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/IBoardSideComponent.java b/diylc/diylc-core/src/main/java/org/diylc/core/IBoardSideComponent.java
index 305a0a5ee..f8357afe7 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/core/IBoardSideComponent.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/core/IBoardSideComponent.java
@@ -39,4 +39,11 @@ public interface IBoardSideComponent {
* never an inner layer or null
*/
PCBLayer getBoardSide();
+
+ /**
+ * Sets the side of the board the component is mounted on. The component is not mirrored.
+ *
+ * @param boardSide {@link PCBLayer#_2} for the top or {@link PCBLayer#_1} for the bottom
+ */
+ void setBoardSide(PCBLayer boardSide);
}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingManager.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingManager.java
index 57daa956b..9f006e3b5 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingManager.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingManager.java
@@ -118,6 +118,9 @@ public class DrawingManager {
private List proximityMarkers = null;
+ // the board whose underside mirror the component in the slot is being placed through
+ private IBoard slotMirrorBoard = null;
+
public DrawingManager(MessageDispatcher messageDispatcher,
IConfigurationManager> configManager) {
super();
@@ -418,6 +421,10 @@ public class DrawingManager {
}
}
+ // a component being placed through an underside mirror is shown there, under the cursor
+ AffineTransform slotTx =
+ slotMirrorBoard == null ? null : UndersideMirror.createTransform(slotMirrorBoard);
+
// Draw component slot in a separate composite.
if (componentSlot != null) {
for (IDIYComponent> component : componentSlot) {
@@ -425,6 +432,10 @@ public class DrawingManager {
// leaves behind (e.g. a rotated Label) is reverted before the next one is drawn
g2dWrapper.startedDrawingComponent(project.getComponents().size());
g2dWrapper.setComposite(slotComposite);
+ if (slotTx != null) {
+ g2dWrapper.transform(slotTx);
+ g2dWrapper.setMirrorDirection(UndersideMirror.getMirrorDirection(slotMirrorBoard));
+ }
try {
component.draw(g2dWrapper, ComponentState.NORMAL,
drawOptions.contains(DrawOption.OUTLINE_MODE), project, g2dWrapper);
@@ -432,6 +443,7 @@ public class DrawingManager {
LOG.error("Error drawing " + component.getName(), e);
failedComponents.add(component);
} finally {
+ g2dWrapper.setMirrorDirection(G2DWrapper.MirrorDirection.NONE);
g2dWrapper.finishedDrawingComponent();
}
}
@@ -439,7 +451,9 @@ public class DrawingManager {
// Draw control points of the component in the slot.
if (controlPointSlot != null) {
- for (Point2D point : controlPointSlot) {
+ for (Point2D slotPoint : controlPointSlot) {
+ Point2D point =
+ slotTx == null || slotPoint == null ? slotPoint : slotTx.transform(slotPoint, null);
if (point != null) {
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR.darker());
g2dWrapper.fillOval((int) Math.round(point.getX() - CONTROL_POINT_SIZE / 2),
@@ -557,20 +571,7 @@ public class DrawingManager {
return;
}
- G2DWrapper.MirrorDirection mirrorDirection;
- switch (board.getUndersideDisplay()) {
- case ABOVE:
- case BELOW:
- mirrorDirection = G2DWrapper.MirrorDirection.VERTICAL;
- break;
- case LEFT:
- case RIGHT:
- mirrorDirection = G2DWrapper.MirrorDirection.HORIZONTAL;
- break;
- default:
- mirrorDirection = G2DWrapper.MirrorDirection.NONE;
- break;
- }
+ G2DWrapper.MirrorDirection mirrorDirection = UndersideMirror.getMirrorDirection(board);
AffineTransform oldTx = g2dWrapper.getTransform();
Composite oldComposite = g2dWrapper.getComposite();
@@ -734,6 +735,16 @@ public class DrawingManager {
proximityMarkers = null;
}
+ /**
+ * Sets the board whose underside mirror the component being placed is drawn through, or null to
+ * draw it in the main view.
+ *
+ * @param slotMirrorBoard
+ */
+ public void setSlotMirrorBoard(IBoard slotMirrorBoard) {
+ this.slotMirrorBoard = slotMirrorBoard;
+ }
+
public void clearContinuityArea() {
currentContinuityAreas = null;
continuityGraphCache = null;
diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/MirrorPlacement.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/MirrorPlacement.java
new file mode 100644
index 000000000..649a1d615
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/MirrorPlacement.java
@@ -0,0 +1,66 @@
+/*
+
+ 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.presenter;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+
+import org.apache.log4j.Logger;
+
+import org.diylc.common.PCBLayer;
+import org.diylc.core.IBoardSideComponent;
+import org.diylc.core.IDIYComponent;
+
+/**
+ * Puts components placed through an Underside Display mirror on the bottom of the board. A part
+ * that is mounted on a side goes to the bottom, copper with a {@link PCBLayer} goes to the bottom
+ * layer, and anything else is left as it is. Nothing is mirrored.
+ */
+public class MirrorPlacement {
+
+ private static final Logger LOG = Logger.getLogger(MirrorPlacement.class);
+
+ private MirrorPlacement() {
+ }
+
+ public static void placeOnBottom(Collection> components) {
+ for (IDIYComponent> component : components) {
+ if (component instanceof IBoardSideComponent) {
+ ((IBoardSideComponent) component).setBoardSide(PCBLayer._1);
+ continue;
+ }
+ // traces, pads and PCB text share no interface for their layer, so it is found the same way
+ // PCBLayerFilter reads it
+ Method setLayer;
+ try {
+ setLayer = component.getClass().getMethod("setLayer", PCBLayer.class);
+ } catch (NoSuchMethodException e) {
+ continue;
+ }
+ try {
+ setLayer.invoke(component, PCBLayer._1);
+ } catch (Exception e) {
+ LOG.warn("Could not put " + component.getName() + " on the bottom layer", e);
+ }
+ }
+ }
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java
index cf6ec5936..d180ff18a 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java
@@ -177,6 +177,9 @@ public class Presenter implements IPlugInPort {
private AffineTransform dragMirrorTx = null;
// the reflection of the mirror that the control points in controlPointMap were found through
private AffineTransform hoverMirrorTx = null;
+ // the board whose Underside Display mirror the component in the slot is being placed through;
+ // null when it is placed in the main view
+ private IBoard placementMirrorBoard = null;
private DIYTest test = null;
@@ -527,6 +530,8 @@ public class Presenter implements IPlugInPort {
} else {
componentSlotToDraw = instantiationManager.getComponentSlot();
}
+ drawingManager.setSlotMirrorBoard(
+ instantiationManager.getComponentTypeSlot() == null ? null : placementMirrorBoard);
List> failedComponents =
drawingManager
.drawProject(
@@ -626,6 +631,8 @@ public class Presenter implements IPlugInPort {
messageDispatcher.dispatchMessage(EventType.REPAINT);
return;
}
+ updatePlacementMirror(scaledPoint);
+ scaledPoint = toPlacementSpace(scaledPoint);
// Keep the reference to component type for later.
ComponentType componentTypeSlot = instantiationManager.getComponentTypeSlot();
Template template = instantiationManager.getTemplate();
@@ -639,6 +646,9 @@ public class Presenter implements IPlugInPort {
}
List> componentSlot = instantiationManager.getComponentSlot();
List> newSelection = new ArrayList>(componentSlot);
+ if (placementMirrorBoard != null) {
+ MirrorPlacement.placeOnBottom(componentSlot);
+ }
addComponents(componentSlot);
// group components if there's more than one, e.g. building blocks, but not clipboard
// contents
@@ -676,6 +686,9 @@ public class Presenter implements IPlugInPort {
if (instantiationManager.getComponentSlot() == null) {
try {
instantiationManager.instatiatePointByPoint(scaledPoint, currentProject);
+ if (placementMirrorBoard != null) {
+ MirrorPlacement.placeOnBottom(instantiationManager.getComponentSlot());
+ }
} catch (Exception e) {
view.showMessage(ERROR_CREATE, ERROR, IView.ERROR_MESSAGE);
LOG.error("Could not create component", e);
@@ -1007,10 +1020,14 @@ public class Presenter implements IPlugInPort {
Map, Set> components = new HashMap, Set>();
this.previousScaledPoint = scalePoint(point);
if (instantiationManager.getComponentTypeSlot() != null) {
+ IBoard oldPlacementMirrorBoard = placementMirrorBoard;
+ updatePlacementMirror(previousScaledPoint);
+ // reflected before snapping, so that a component placed through a mirror lands on the grid
+ Point2D placementPoint = toPlacementSpace(previousScaledPoint);
if (isSnapToGrid()) {
- CalcUtils.snapPointToGrid(previousScaledPoint, currentProject.getGridSpacing());
+ CalcUtils.snapPointToGrid(placementPoint, currentProject.getGridSpacing());
} else if (isSnapToObjects()) {
- CalcUtils.snapPointToObjects(previousScaledPoint, currentProject.getGridSpacing(), null, currentProject.getComponents());
+ CalcUtils.snapPointToObjects(placementPoint, currentProject.getGridSpacing(), null, currentProject.getComponents());
}
// What the cursor does is decided by how the slot was armed rather than by the creation
// method: a slot instantiated up front carries a whole component that follows the cursor,
@@ -1019,13 +1036,13 @@ public class Presenter implements IPlugInPort {
if (instantiationManager.getComponentSlot() != null
&& instantiationManager.getFirstControlPoint() == null) {
refresh =
- instantiationManager.updateSingleClick(previousScaledPoint, isSnapToGrid(),
+ instantiationManager.updateSingleClick(placementPoint, isSnapToGrid(),
currentProject.getGridSpacing());
} else {
refresh = instantiationManager.updatePointByPoint(
- constrainToAxis(instantiationManager.getFirstControlPoint(), previousScaledPoint, shiftDown));
+ constrainToAxis(instantiationManager.getFirstControlPoint(), placementPoint, shiftDown));
}
- if (refresh) {
+ if (refresh || placementMirrorBoard != oldPlacementMirrorBoard) {
messageDispatcher.dispatchMessage(EventType.REPAINT);
}
} else {
@@ -3192,6 +3209,22 @@ public class Presenter implements IPlugInPort {
: dragMirrorTx.transform(scaledPoint, null);
}
+ // a component placed point by point stays in the mirror its first point went into
+ private void updatePlacementMirror(Point2D scaledPoint) {
+ if (instantiationManager.getFirstControlPoint() != null) {
+ return;
+ }
+ List boards = UndersideMirror.findBoardsMirroredAt(currentProject, scaledPoint);
+ placementMirrorBoard = boards.isEmpty() ? null : boards.get(0);
+ }
+
+ private Point2D toPlacementSpace(Point2D scaledPoint) {
+ AffineTransform mirrorTx =
+ placementMirrorBoard == null ? null : UndersideMirror.createTransform(placementMirrorBoard);
+ return mirrorTx == null || scaledPoint == null ? scaledPoint
+ : mirrorTx.transform(scaledPoint, null);
+ }
+
/**
* @return the reflection of the first underside mirror that shows a selectable component at the
* specified point, or null
diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/UndersideMirror.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/UndersideMirror.java
index 63ef7c797..7147d216b 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/presenter/UndersideMirror.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/UndersideMirror.java
@@ -108,6 +108,26 @@ public class UndersideMirror {
return mirrorTx;
}
+ /**
+ * @return the axis the board's mirror flips about, which text drawn into it has to be flipped
+ * back along to stay readable
+ */
+ public static G2DWrapper.MirrorDirection getMirrorDirection(IBoard board) {
+ if (board.getUndersideDisplay() == null) {
+ return G2DWrapper.MirrorDirection.NONE;
+ }
+ switch (board.getUndersideDisplay()) {
+ case ABOVE:
+ case BELOW:
+ return G2DWrapper.MirrorDirection.VERTICAL;
+ case LEFT:
+ case RIGHT:
+ return G2DWrapper.MirrorDirection.HORIZONTAL;
+ default:
+ return G2DWrapper.MirrorDirection.NONE;
+ }
+ }
+
/**
* @return the part of the canvas the board's mirror occupies, or null when the board has no
* underside display
diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml
index 846c5b83d..0e957c6a6 100644
--- a/diylc/diylc-core/src/main/resources/update.xml
+++ b/diylc/diylc-core/src/main/resources/update.xml
@@ -639,7 +639,7 @@
NEW_FEATURE
- Components can be selected, dragged and stretched by clicking them in a board's Underside Display, not only in the main view
+ Components can be selected, dragged and stretched by clicking them in a board's Underside Display, not only in the main view, and new parts can be placed there; a part placed in the Underside Display goes on the bottom of the board
BUG_FIX
diff --git a/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java b/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java
index f4148055b..190bdf579 100644
--- a/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java
+++ b/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java
@@ -173,6 +173,11 @@ public class GerberExporterTest {
public PCBLayer getBoardSide() {
return side;
}
+
+ @Override
+ public void setBoardSide(PCBLayer boardSide) {
+ this.side = boardSide;
+ }
}
public static class TextGerberComponent extends SidedGerberComponent {
diff --git a/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java b/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java
index 923b66a65..f5667e2a8 100644
--- a/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java
+++ b/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java
@@ -1073,6 +1073,11 @@ public class DrawingManagerTest {
return boardSide;
}
+ @Override
+ public void setBoardSide(PCBLayer boardSide) {
+ this.boardSide = boardSide;
+ }
+
@Override
public Rectangle2D getCachingBounds() {
return bounds;
diff --git a/diylc/diylc-core/src/test/java/org/diylc/presenter/MirrorPlacementTest.java b/diylc/diylc-core/src/test/java/org/diylc/presenter/MirrorPlacementTest.java
new file mode 100644
index 000000000..8f6023703
--- /dev/null
+++ b/diylc/diylc-core/src/test/java/org/diylc/presenter/MirrorPlacementTest.java
@@ -0,0 +1,63 @@
+/*
+
+ 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.presenter;
+
+import static org.junit.Assert.assertEquals;
+
+import java.awt.geom.Rectangle2D;
+import java.util.Arrays;
+
+import org.diylc.common.PCBLayer;
+import org.diylc.core.IDIYComponent;
+import org.diylc.presenter.DrawingManagerTest.MountedComponent;
+import org.diylc.testcomponents.MockDIYComponent;
+import org.junit.Test;
+
+public class MirrorPlacementTest {
+
+ @Test
+ public void testPlacesOnBottom() {
+ MountedComponent part = new MountedComponent(new Rectangle2D.Double(0, 0, 10, 10), PCBLayer._2);
+ LayeredComponent copper = new LayeredComponent();
+ MockDIYComponent other = new MockDIYComponent();
+
+ MirrorPlacement.placeOnBottom(Arrays.>asList(part, copper, other));
+
+ assertEquals(PCBLayer._1, part.getBoardSide());
+ assertEquals(PCBLayer._1, copper.getLayer());
+ }
+
+ public static class LayeredComponent extends MockDIYComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ private PCBLayer layer = PCBLayer._2;
+
+ public PCBLayer getLayer() {
+ return layer;
+ }
+
+ public void setLayer(PCBLayer layer) {
+ this.layer = layer;
+ }
+ }
+}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/PCBTerminalBlock.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/PCBTerminalBlock.java
index 915add0d9..fb9abf044 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/PCBTerminalBlock.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/PCBTerminalBlock.java
@@ -379,6 +379,7 @@ public class PCBTerminalBlock extends AbstractTransparentComponent implement
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/CliffJack1_8.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/CliffJack1_8.java
index 426d015c2..81a696baa 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/CliffJack1_8.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/CliffJack1_8.java
@@ -404,6 +404,7 @@ public class CliffJack1_8 extends AbstractMultiPartComponent implement
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/ClosedJack1_4.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/ClosedJack1_4.java
index b50719098..0959bddee 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/ClosedJack1_4.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/ClosedJack1_4.java
@@ -390,6 +390,7 @@ public class ClosedJack1_4 extends AbstractMultiPartComponent implemen
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/MiniRelay.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/MiniRelay.java
index 2b2ff46e1..01952a3aa 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/MiniRelay.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/MiniRelay.java
@@ -480,6 +480,7 @@ public class MiniRelay extends AbstractLabeledComponent implements IGerb
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/MiniToggleSwitch.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/MiniToggleSwitch.java
index 313a474f4..ab9f6fbca 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/MiniToggleSwitch.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/MiniToggleSwitch.java
@@ -645,6 +645,7 @@ public class MiniToggleSwitch extends AbstractTransparentComponent implements
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/RotarySelectorSwitch.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/RotarySelectorSwitch.java
index 3c54e7d98..e7bb05972 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/RotarySelectorSwitch.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/RotarySelectorSwitch.java
@@ -379,6 +379,7 @@ public class RotarySelectorSwitch extends AbstractMultiPartComponent imp
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/RotarySwitchOpen.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/RotarySwitchOpen.java
index c85d22188..040f38184 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/RotarySwitchOpen.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/RotarySwitchOpen.java
@@ -618,6 +618,7 @@ public class RotarySwitchOpen extends AbstractAngledComponent i
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/TactileMicroSwitch.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/TactileMicroSwitch.java
index 0304d1c71..47f9bfd32 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/TactileMicroSwitch.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/TactileMicroSwitch.java
@@ -526,6 +526,7 @@ public class TactileMicroSwitch extends AbstractLabeledComponent impleme
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AbstractFilmCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AbstractFilmCapacitor.java
index 6e4a75df7..8fe1f381d 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AbstractFilmCapacitor.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AbstractFilmCapacitor.java
@@ -155,6 +155,7 @@ public abstract class AbstractFilmCapacitor extends AbstractLeadedComponent extends AbstractLeadedComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AudioTransformer.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AudioTransformer.java
index 6907b4d43..3f75e160b 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AudioTransformer.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AudioTransformer.java
@@ -535,6 +535,7 @@ public class AudioTransformer extends AbstractMultiPartComponent impleme
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialElectrolyticCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialElectrolyticCapacitor.java
index 4e3546dda..fa5bf8838 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialElectrolyticCapacitor.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/AxialElectrolyticCapacitor.java
@@ -306,6 +306,7 @@ public class AxialElectrolyticCapacitor extends AbstractLeadedComponent implem
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/HeatSinkResistor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/HeatSinkResistor.java
index b01141614..bc56f2a68 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/HeatSinkResistor.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/HeatSinkResistor.java
@@ -553,6 +553,7 @@ public class HeatSinkResistor extends AbstractLeadedComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/MultiSectionCapacitor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/MultiSectionCapacitor.java
index 7ee7fa4ef..24308848f 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/MultiSectionCapacitor.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/MultiSectionCapacitor.java
@@ -551,6 +551,7 @@ public class MultiSectionCapacitor extends AbstractLabeledComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/ToroidalInductor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/ToroidalInductor.java
index e7092b918..e9937b620 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/ToroidalInductor.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/ToroidalInductor.java
@@ -175,6 +175,7 @@ public class ToroidalInductor extends AbstractLeadedComponent implem
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/AbstractTransistorPackage.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/AbstractTransistorPackage.java
index f3ca8d13c..8bfdc2ed1 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/AbstractTransistorPackage.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/AbstractTransistorPackage.java
@@ -337,6 +337,7 @@ public abstract class AbstractTransistorPackage extends AbstractLabeledComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/BridgeRectifier.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/BridgeRectifier.java
index 49d590059..f30be8cab 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/BridgeRectifier.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/BridgeRectifier.java
@@ -717,6 +717,7 @@ public class BridgeRectifier extends AbstractLabeledComponent implements
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DIL_IC.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DIL_IC.java
index 40459e341..1ba05dbac 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DIL_IC.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DIL_IC.java
@@ -704,6 +704,7 @@ public class DIL_IC extends AbstractLabeledComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodeGlass.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodeGlass.java
index b351d9548..d7c30c040 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodeGlass.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodeGlass.java
@@ -214,6 +214,7 @@ public class DiodeGlass extends AbstractLeadedComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodePlastic.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodePlastic.java
index b05670c57..c5321e9af 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodePlastic.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/DiodePlastic.java
@@ -188,6 +188,7 @@ public class DiodePlastic extends AbstractLeadedComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/MultiwattIC.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/MultiwattIC.java
index 9dd6a8687..42228a3ea 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/MultiwattIC.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/MultiwattIC.java
@@ -753,6 +753,7 @@ public class MultiwattIC extends AbstractTransparentComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/SIL_IC.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/SIL_IC.java
index 3a88864d7..d1ef01f87 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/SIL_IC.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/SIL_IC.java
@@ -561,6 +561,7 @@ public class SIL_IC extends AbstractTransparentComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/smd/PassiveSMDComponent.java b/diylc/diylc-library/src/main/java/org/diylc/components/smd/PassiveSMDComponent.java
index fea4ad001..a3e9cd2c3 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/smd/PassiveSMDComponent.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/smd/PassiveSMDComponent.java
@@ -422,6 +422,7 @@ public abstract class PassiveSMDComponent extends AbstractLabeledComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/tube/TubeSocket.java b/diylc/diylc-library/src/main/java/org/diylc/components/tube/TubeSocket.java
index db715f196..241974f9b 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/tube/TubeSocket.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/tube/TubeSocket.java
@@ -493,6 +493,7 @@ public class TubeSocket extends AbstractAngledComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
+ @Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
diff --git a/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java b/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java
index eefdace8b..82bac7ac4 100644
--- a/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java
+++ b/diylc/diylc-swing/src/test/java/org/diylc/PresenterTests.java
@@ -4,6 +4,7 @@ import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
@@ -23,8 +24,10 @@ import org.diylc.common.DrawOption;
import org.diylc.common.EventType;
import org.diylc.common.IPlugIn;
import org.diylc.common.IPlugInPort;
+import org.diylc.common.PCBLayer;
import org.diylc.components.boards.BlankBoard;
import org.diylc.components.chassis.ChassisPanel;
+import org.diylc.components.passive.Resistor;
import org.diylc.components.composite.CompositeComponent;
import org.diylc.core.BoardUndersideDisplay;
import org.diylc.core.IBoard;
@@ -333,6 +336,48 @@ public class PresenterTests extends TestBase {
resistor.getControlPoint(1).getX() - resistor.getControlPoint(0).getX());
}
+ @Test
+ public void testPlaceThroughUndersideMirror() {
+ BlankBoard board = mirroredBoard();
+ Rectangle2D region = UndersideMirror.getMirrorRegion(board);
+ Point first = toScreen(new Point2D.Double(region.getMinX() + 100, region.getMinY() + 100));
+ Point second = toScreen(new Point2D.Double(region.getMinX() + 200, region.getMinY() + 100));
+
+ instantiateTwoClick("Passive", "Resistor", first, second);
+
+ Resistor resistor = (Resistor) presenter.getCurrentProject().getComponents().get(1);
+ Assert.assertEquals(PCBLayer._1, resistor.getBoardSide());
+ // created where the mirror shows it, which is on the board itself
+ Assert.assertTrue(board.getBoardRectangle().contains(resistor.getControlPoint(0)));
+ Assert.assertTrue(board.getBoardRectangle().contains(resistor.getControlPoint(1)));
+ }
+
+ @Test
+ public void testPlaceUnsidedPartThroughUndersideMirror() {
+ BlankBoard board = mirroredBoard();
+ Rectangle2D region = UndersideMirror.getMirrorRegion(board);
+ Point inMirror = toScreen(new Point2D.Double(region.getCenterX(), region.getCenterY()));
+ ComponentType eyeletType = presenter.getComponentTypes().get("Connectivity").stream()
+ .filter(x -> x.getName().equals("Eyelet")).findFirst().get();
+
+ presenter.setNewComponentTypeSlot(eyeletType, null, null, false);
+ presenter.mouseMoved(inMirror, false, false, false);
+ presenter.mouseClicked(inMirror, MouseEvent.BUTTON1, false, false, false, 1);
+
+ IDIYComponent> eyelet = presenter.getCurrentProject().getComponents().get(1);
+ Assert.assertTrue(board.getBoardRectangle().contains(eyelet.getControlPoint(0)));
+ }
+
+ // a board with its underside mirror below it, with nothing on it yet
+ private BlankBoard mirroredBoard() {
+ BlankBoard board = new BlankBoard();
+ board.setControlPoint(new Point2D.Double(100, 100), 0);
+ board.setControlPoint(new Point2D.Double(700, 500), 1);
+ board.setUndersideDisplay(BoardUndersideDisplay.BELOW);
+ presenter.getCurrentProject().getComponents().add(board);
+ return board;
+ }
+
// a board reaching 200px past the component on every side, with its underside mirror below it;
// the component stays selected
private IBoard boardUnder(IDIYComponent> component) {
diff --git a/diylc/docs/plans/bottom-side-components.md b/diylc/docs/plans/bottom-side-components.md
index bc905478c..2cec9b6bb 100644
--- a/diylc/docs/plans/bottom-side-components.md
+++ b/diylc/docs/plans/bottom-side-components.md
@@ -6,8 +6,9 @@ Gerber layers and the canvas shows it as a part on the far side of the board.
Status: 45 parts have a Board Side property, and Gerber export sends a bottom-side part's
silkscreen to the bottom layer with its text mirrored. Parts can be selected, dragged and stretched
through the Underside Display mirror. The canvas draws a bottom-side part faded in the main view and
-solid in the mirror. The silkscreen print-out has a separate page for the bottom. The next steps
-below remain.
+solid in the mirror. The silkscreen print-out has a separate page for the bottom, and new parts
+can be placed through the mirror. Every step is done, and the regression suite passes on all 97
+projects; the open questions below remain.
## 1. Current state
@@ -44,6 +45,7 @@ below remain.
| The Gerber layer is swapped centrally, not inside each component | Components keep emitting their `*Top` layer. One place maps it to the other side. |
| Default `_2` (Top), with `null` meaning Top | Fields added to existing classes deserialize as `null` in old files. |
| Bottom-side text is mirrored centrally in the Gerber output | Parts draw their labels readable from the top, which is backwards on the bottom silkscreen once the board is turned over. While `GerberExporter.drawComponent` draws a bottom-side part, `GerberG2DWrapper.setMirrorText` mirrors each string about the centre of its layout box, so no component changes and the canvas keeps readable labels. |
+| A part without a side placed in the mirror is placed unchanged | A jumper, eyelet or wire has no side to set, so it is placed at the reflected point like any other part rather than refused. |
| A `SolderPad` opens the solder mask on its own side | It used to write `SolderMaskBot` whatever its layer, which was wrong for top-layer pads. It now writes `SolderMaskTop` for a pad on `_2`. |
## 3. Design
@@ -118,16 +120,19 @@ order.
has nothing. A drag that starts on empty mirror space still draws a selection rectangle in
screen space. Moving the board, or changing its Underside Offset, cancels nothing; the mirror
simply follows.
-- **Stage 3: placing in the mirror.** A component instantiated with its first point in a mirror
- region is created at the reflected point. If it is an `IBoardSideComponent`, it is created with
- side `_1` (Bottom), not mirrored. A `PCBLayer` component is created on `_1`. Anything else is
- placed at the reflected point unchanged.
+- **Stage 3: placing in the mirror (done).** While a component is being placed,
+ `Presenter.placementMirrorBoard` holds the board whose mirror the cursor is in; a single-click
+ part follows the cursor in and out of mirrors, and a part placed point by point stays in the
+ mirror its first point went into. The cursor is reflected before it is snapped to the grid, so
+ the part lands on the grid, and `DrawingManager.setSlotMirrorBoard` draws the part being placed
+ through that mirror, under the cursor. On placement, `MirrorPlacement.placeOnBottom` sets an
+ `IBoardSideComponent` to `_1` (Bottom) and a component with a `PCBLayer` (`setLayer`, found the
+ same way `PCBLayerFilter` reads `getLayer`) to `_1`; nothing is mirrored. Anything else, such as
+ a jumper, eyelet or wire, is placed at the reflected point unchanged. Dragging a new part from
+ the component tree onto a mirror still places it in the main view.
## 4. Open questions
-- **Components that belong to no side.** In mirror stage 3 a jumper, eyelet or wire dropped in the
- mirror has no side to set. Confirm that placing it at the reflected point unchanged is what users
- expect, or whether such parts should be refused in the mirror.
- **Locked bottom-side parts drawn from the cache.** An uncached locked part takes the stronger of
the locked alpha and the far-side alpha. A cached one is pasted at the locked alpha onto an
image that already carries the far-side fade, so it ends up fainter. The same applies to
@@ -140,6 +145,10 @@ order.
- `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components,
including a stub `IBoardSideComponent` on top, bottom and `null`.
+- `MirrorPlacementTest` covers what placing through the mirror changes, and
+ `PresenterTests.testPlaceThroughUndersideMirror` and `testPlaceUnsidedPartThroughUndersideMirror`
+ place a resistor and an eyelet in a mirror and check that they land on the board, the resistor on
+ the bottom.
- `SilkScreenFilterTest` covers which side's page each part and board outline goes on, and
`SilkScreenDrawingProviderTest` checks that the bottom page appears only for a bottom part.
- `DrawingManagerTest` checks rendered pixels for a part that reports its own side: a bottom part
@@ -157,9 +166,10 @@ order.
- In the app, with Underside Display set to each direction: select, drag and stretch a trace in
the mirror and confirm that the original follows correctly. Place a part in the mirror and confirm
that it is bottom-side, faded on the main view and solid in the mirror.
-- Run the regression suite, since rendering and serialization both change.
+- The regression suite passes on all 97 projects with the whole plan in place.
## 6. Next steps
-1. Mirror stage 3: placing in the mirror.
-2. Run the regression suite.
+1. Settle the open questions above.
+2. Check Gerber output of a board with parts on both sides in a Gerber viewer, and try the
+ Underside Display editing in each direction in the app, as listed under Verification.