New parts can be placed in a board's Underside Display; a part placed there goes on the bottom of the board

This commit is contained in:
Bane Stojkovic
2026-11-11 15:46:29 +01:00
parent 122a91a6f0
commit eeca9f112d
42 changed files with 329 additions and 33 deletions
@@ -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);
}
@@ -118,6 +118,9 @@ public class DrawingManager {
private List<Area> proximityMarkers = null;
// the board whose underside mirror the component in the slot is being placed through
private IBoard slotMirrorBoard = null;
public DrawingManager(MessageDispatcher<EventType> 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;
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<IDIYComponent<?>> 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);
}
}
}
}
@@ -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<IDIYComponent<?>> 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<IDIYComponent<?>> componentSlot = instantiationManager.getComponentSlot();
List<IDIYComponent<?>> newSelection = new ArrayList<IDIYComponent<?>>(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<IDIYComponent<?>, Set<Integer>> components = new HashMap<IDIYComponent<?>, Set<Integer>>();
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<IBoard> 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
@@ -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
@@ -639,7 +639,7 @@
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Components can be selected, dragged and stretched by clicking them in a board's Underside Display, not only in the main view</description>
<description>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</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
@@ -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 {
@@ -1073,6 +1073,11 @@ public class DrawingManagerTest {
return boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@Override
public Rectangle2D getCachingBounds() {
return bounds;
@@ -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 <http://www.gnu.org/licenses/>.
*/
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.<IDIYComponent<?>>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;
}
}
}
@@ -379,6 +379,7 @@ public class PCBTerminalBlock extends AbstractTransparentComponent<PCBTerminalBl
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -383,6 +383,7 @@ public class CliffJack1_4 extends AbstractMultiPartComponent<JackType> implement
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -404,6 +404,7 @@ public class CliffJack1_8 extends AbstractMultiPartComponent<JackType> implement
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -390,6 +390,7 @@ public class ClosedJack1_4 extends AbstractMultiPartComponent<JackType> implemen
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -480,6 +480,7 @@ public class MiniRelay extends AbstractLabeledComponent<String> implements IGerb
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -645,6 +645,7 @@ public class MiniToggleSwitch extends AbstractTransparentComponent<ToggleSwitchT
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -304,6 +304,7 @@ public class PlasticDCJack extends AbstractMultiPartComponent<String> implements
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -379,6 +379,7 @@ public class RotarySelectorSwitch extends AbstractMultiPartComponent<String> imp
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -618,6 +618,7 @@ public class RotarySwitchOpen extends AbstractAngledComponent<RotarySwitchOpenTy
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -367,6 +367,7 @@ public class RotarySwitchSealed extends AbstractAngledComponent<RotarySwitchSeal
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -544,6 +544,7 @@ public class SlideSwitch extends AbstractTransparentComponent<SlideSwitchType> i
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -526,6 +526,7 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -155,6 +155,7 @@ public abstract class AbstractFilmCapacitor extends AbstractLeadedComponent<Capa
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -120,6 +120,7 @@ public abstract class AbstractPotentiometer extends AbstractLabeledComponent<Res
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -84,6 +84,7 @@ public abstract class AbstractRadialComponent<T> extends AbstractLeadedComponent
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -535,6 +535,7 @@ public class AudioTransformer extends AbstractMultiPartComponent<String> impleme
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -306,6 +306,7 @@ public class AxialElectrolyticCapacitor extends AbstractLeadedComponent<Capacita
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -143,6 +143,7 @@ public class CrystalOscillator extends AbstractLeadedComponent<Frequency> implem
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -553,6 +553,7 @@ public class HeatSinkResistor extends AbstractLeadedComponent<Resistance>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -551,6 +551,7 @@ public class MultiSectionCapacitor extends AbstractLabeledComponent<Capacitance[
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -272,6 +272,7 @@ public class Resistor extends AbstractLeadedComponent<Resistance>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -175,6 +175,7 @@ public class ToroidalInductor extends AbstractLeadedComponent<Inductance> implem
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -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;
}
@@ -717,6 +717,7 @@ public class BridgeRectifier extends AbstractLabeledComponent<String> implements
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -704,6 +704,7 @@ public class DIL_IC extends AbstractLabeledComponent<String>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -214,6 +214,7 @@ public class DiodeGlass extends AbstractLeadedComponent<String>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -188,6 +188,7 @@ public class DiodePlastic extends AbstractLeadedComponent<String>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -753,6 +753,7 @@ public class MultiwattIC extends AbstractTransparentComponent<String>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -561,6 +561,7 @@ public class SIL_IC extends AbstractTransparentComponent<String>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -422,6 +422,7 @@ public abstract class PassiveSMDComponent<T> extends AbstractLabeledComponent<T>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -493,6 +493,7 @@ public class TubeSocket extends AbstractAngledComponent<String>
return boardSide == null ? PCBLayer._2 : boardSide;
}
@Override
public void setBoardSide(PCBLayer boardSide) {
this.boardSide = boardSide;
}
@@ -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) {
+22 -12
View File
@@ -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.