From eeca9f112ddf727f0960f21bbbcf7a38c76448b5 Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Wed, 11 Nov 2026 15:46:29 +0100 Subject: [PATCH] New parts can be placed in a board's Underside Display; a part placed there goes on the bottom of the board --- .../org/diylc/core/IBoardSideComponent.java | 7 ++ .../org/diylc/presenter/DrawingManager.java | 41 +++++++----- .../org/diylc/presenter/MirrorPlacement.java | 66 +++++++++++++++++++ .../java/org/diylc/presenter/Presenter.java | 43 ++++++++++-- .../org/diylc/presenter/UndersideMirror.java | 20 ++++++ .../diylc-core/src/main/resources/update.xml | 2 +- .../diylc/core/gerber/GerberExporterTest.java | 5 ++ .../diylc/presenter/DrawingManagerTest.java | 5 ++ .../diylc/presenter/MirrorPlacementTest.java | 63 ++++++++++++++++++ .../connectivity/PCBTerminalBlock.java | 1 + .../electromechanical/CliffJack1_4.java | 1 + .../electromechanical/CliffJack1_8.java | 1 + .../electromechanical/ClosedJack1_4.java | 1 + .../electromechanical/MiniRelay.java | 1 + .../electromechanical/MiniToggleSwitch.java | 1 + .../electromechanical/PlasticDCJack.java | 1 + .../RotarySelectorSwitch.java | 1 + .../electromechanical/RotarySwitchOpen.java | 1 + .../electromechanical/RotarySwitchSealed.java | 1 + .../electromechanical/SlideSwitch.java | 1 + .../electromechanical/TactileMicroSwitch.java | 1 + .../passive/AbstractFilmCapacitor.java | 1 + .../passive/AbstractPotentiometer.java | 1 + .../passive/AbstractRadialComponent.java | 1 + .../components/passive/AudioTransformer.java | 1 + .../passive/AxialElectrolyticCapacitor.java | 1 + .../components/passive/CrystalOscillator.java | 1 + .../components/passive/HeatSinkResistor.java | 1 + .../passive/MultiSectionCapacitor.java | 1 + .../diylc/components/passive/Resistor.java | 1 + .../components/passive/ToroidalInductor.java | 1 + .../AbstractTransistorPackage.java | 1 + .../semiconductors/BridgeRectifier.java | 1 + .../components/semiconductors/DIL_IC.java | 1 + .../components/semiconductors/DiodeGlass.java | 1 + .../semiconductors/DiodePlastic.java | 1 + .../semiconductors/MultiwattIC.java | 1 + .../components/semiconductors/SIL_IC.java | 1 + .../components/smd/PassiveSMDComponent.java | 1 + .../org/diylc/components/tube/TubeSocket.java | 1 + .../test/java/org/diylc/PresenterTests.java | 45 +++++++++++++ diylc/docs/plans/bottom-side-components.md | 34 ++++++---- 42 files changed, 329 insertions(+), 33 deletions(-) create mode 100644 diylc/diylc-core/src/main/java/org/diylc/presenter/MirrorPlacement.java create mode 100644 diylc/diylc-core/src/test/java/org/diylc/presenter/MirrorPlacementTest.java 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.