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;
}
}
}