Components can be dragged and their points stretched through the Underside Display mirror of a board

This commit is contained in:
Bane Stojkovic
2026-11-11 15:05:28 +01:00
parent 90e10f4597
commit b76d315f44
4 changed files with 206 additions and 51 deletions
@@ -751,9 +751,8 @@ public class DrawingManager {
public List<IDIYComponent<?>> findMirroredComponentsAt(Point2D point, Project project) {
List<IDIYComponent<?>> components = new ArrayList<IDIYComponent<?>>();
for (IBoard board : UndersideMirror.findBoardsMirroredAt(project, point)) {
Point2D reflected = UndersideMirror.createTransform(board).transform(point, null);
for (IDIYComponent<?> component : findComponentsAt(reflected, project)) {
if (UndersideMirror.isMirrored(board, component) && !components.contains(component)) {
for (IDIYComponent<?> component : findMirroredComponentsAt(point, project, board)) {
if (!components.contains(component)) {
components.add(component);
}
}
@@ -761,6 +760,30 @@ public class DrawingManager {
return components;
}
/**
* Finds the components that the underside mirror of the specified board shows at the specified
* point, top first, or none when the point is not in that mirror.
*
* @param point
* @param project
* @param board
* @return
*/
public List<IDIYComponent<?>> findMirroredComponentsAt(Point2D point, Project project,
IBoard board) {
List<IDIYComponent<?>> components = new ArrayList<IDIYComponent<?>>();
AffineTransform mirrorTx = UndersideMirror.createTransform(board);
if (mirrorTx == null || !UndersideMirror.getMirrorRegion(board).contains(point)) {
return components;
}
for (IDIYComponent<?> component : findComponentsAt(mirrorTx.transform(point, null), project)) {
if (UndersideMirror.isMirrored(board, component)) {
components.add(component);
}
}
return components;
}
public double getExtraSpace(Project project) {
double width = project.getWidth().convertToPixels();
double height = project.getHeight().convertToPixels();
@@ -172,6 +172,11 @@ public class Presenter implements IPlugInPort {
private Point2D dragStartPoint = null;
private boolean dragShiftDown = false;
private AxisLock lockedAxis = null;
// The reflection of the Underside Display mirror a drag started in. Every point of that drag goes
// through it, so the drag acts on the components themselves; null for a drag in the main view.
private AffineTransform dragMirrorTx = null;
// the reflection of the mirror that the control points in controlPointMap were found through
private AffineTransform hoverMirrorTx = null;
private DIYTest test = null;
@@ -279,18 +284,10 @@ public class Presenter implements IPlugInPort {
if (controlPointMap != null && !controlPointMap.isEmpty()) {
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
}
for (IDIYComponent<?> component : currentProject.getComponents()) {
if (!isComponentLocked(component) && isComponentVisible(component)
&& operationMode != OperationMode.HIGHLIGHT_CONNECTED_AREAS) {
ComponentArea area = drawingManager.getComponentArea(component);
// the bounds rule most components out without merging their outline into an area
if (area != null && scaledPoint != null && area.getOutlineBounds().contains(scaledPoint)
&& area.getOutlineArea().contains(scaledPoint)) {
if (scaledPoint != null && !findComponentsAtScaled(scaledPoint, false).isEmpty()) {
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
}
}
}
}
return Cursor.getDefaultCursor();
}
@@ -569,8 +566,8 @@ public class Presenter implements IPlugInPort {
return findComponentsAtScaled(point, includeLocked, true);
}
// a drag does not look through the underside mirror, because moving the mouse there moves the
// part the other way along the mirrored axis
// a drag reflects its points out of the underside mirror itself, so its hit tests must not look
// through the mirror a second time
private List<IDIYComponent<?>> findComponentsAtScaled(Point2D point, boolean includeLocked,
boolean throughMirror) {
List<IDIYComponent<?>> components = drawingManager.findComponentsAt(point, currentProject);
@@ -950,6 +947,42 @@ public class Presenter implements IPlugInPort {
}
}
/**
* Finds the control points of the selected, ungrouped components that can be dragged from the
* specified point, one per component. When the point was reflected out of the underside mirror of
* a board, only the components that mirror shows are considered.
*/
private Map<IDIYComponent<?>, Set<Integer>> findControlPointsAt(Point2D point, IBoard mirrorBoard) {
Map<IDIYComponent<?>, Set<Integer>> components = new HashMap<IDIYComponent<?>, Set<Integer>>();
// Go backwards so we take the highest z-order components first.
for (int i = currentProject.getComponents().size() - 1; i >= 0; i--) {
IDIYComponent<?> component = currentProject.getComponents().get(i);
if (mirrorBoard != null && selectedComponents.contains(component)
&& !UndersideMirror.isMirrored(mirrorBoard, component)) {
continue;
}
for (int pointIndex = 0; pointIndex < component.getControlPointCount(); pointIndex++) {
Point2D controlPoint = component.getControlPoint(pointIndex);
// Only consider selected components that are not grouped.
if (selectedComponents.contains(component) && component.canPointMoveFreely(pointIndex)
&& findAllGroupedComponents(component).size() == 1) {
try {
if (point.distance(controlPoint) < DrawingManager.CONTROL_POINT_SIZE &&
component.getControlPointVisibilityPolicy(pointIndex) != VisibilityPolicy.NEVER) {
Set<Integer> indices = new HashSet<Integer>();
indices.add(pointIndex);
components.put(component, indices);
break;
}
} catch (Exception e) {
LOG.warn("Error reading control point for component of type: " + component.getClass().getName());
}
}
}
}
return components;
}
@Override
public void mouseMoved(Point point, boolean ctrlDown, boolean shiftDown, boolean altDown) {
if (point == null)
@@ -996,26 +1029,16 @@ public class Presenter implements IPlugInPort {
messageDispatcher.dispatchMessage(EventType.REPAINT);
}
} else {
// Go backwards so we take the highest z-order components first.
for (int i = currentProject.getComponents().size() - 1; i >= 0; i--) {
IDIYComponent<?> component = currentProject.getComponents().get(i);
for (int pointIndex = 0; pointIndex < component.getControlPointCount(); pointIndex++) {
Point2D controlPoint = component.getControlPoint(pointIndex);
// Only consider selected components that are not grouped.
if (selectedComponents.contains(component) && component.canPointMoveFreely(pointIndex)
&& findAllGroupedComponents(component).size() == 1) {
try {
if (previousScaledPoint.distance(controlPoint) < DrawingManager.CONTROL_POINT_SIZE &&
component.getControlPointVisibilityPolicy(pointIndex) != VisibilityPolicy.NEVER) {
Set<Integer> indices = new HashSet<Integer>();
indices.add(pointIndex);
components.put(component, indices);
components = findControlPointsAt(previousScaledPoint, null);
hoverMirrorTx = null;
for (IBoard board : UndersideMirror.findBoardsMirroredAt(currentProject, previousScaledPoint)) {
if (!components.isEmpty()) {
break;
}
} catch (Exception e) {
LOG.warn("Error reading control point for component of type: " + component.getClass().getName());
}
}
AffineTransform mirrorTx = UndersideMirror.createTransform(board);
components = findControlPointsAt(mirrorTx.transform(previousScaledPoint, null), board);
if (!components.isEmpty()) {
hoverMirrorTx = mirrorTx;
}
}
}
@@ -1364,6 +1387,14 @@ public class Presenter implements IPlugInPort {
this.dragAction = dragAction;
this.preDragProject = currentProject.clone();
Point2D scaledPoint = scalePoint(point);
if (controlPointMap != null && !controlPointMap.isEmpty()) {
this.dragMirrorTx = hoverMirrorTx;
} else if (forceSelectionRect || !findComponentsAtScaled(scaledPoint, false, false).isEmpty()) {
this.dragMirrorTx = null;
} else {
this.dragMirrorTx = findMirrorWithComponentAt(scaledPoint);
}
scaledPoint = toDragSpace(scaledPoint);
this.previousDragPoint = scaledPoint;
this.dragStartPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY());
this.dragShiftDown = shiftDown;
@@ -1586,7 +1617,7 @@ public class Presenter implements IPlugInPort {
boolean resizeInProgress = false;
Point2D scaledPoint = scalePoint(point);
Point2D scaledPoint = toDragSpace(scalePoint(point));
if (controlPointMap != null && !controlPointMap.isEmpty()) {
// We're dragging control point(s).
Point2D targetPoint = constrainToAxis(dragStartPoint, scaledPoint, dragShiftDown);
@@ -2004,7 +2035,7 @@ public class Presenter implements IPlugInPort {
test.addStep(DIYTest.DRAG_END, params);
}
Point2D scaledPoint = scalePoint(point);
Point2D scaledPoint = toDragSpace(scalePoint(point));
if (!dragInProgress && instantiationManager.getComponentSlot() == null) {
return;
@@ -2056,6 +2087,7 @@ public class Presenter implements IPlugInPort {
dragStartPoint = null;
dragShiftDown = false;
lockedAxis = null;
dragMirrorTx = null;
}
@Override
@@ -3155,6 +3187,27 @@ public class Presenter implements IPlugInPort {
return p;
}
private Point2D toDragSpace(Point2D scaledPoint) {
return dragMirrorTx == null || scaledPoint == null ? scaledPoint
: dragMirrorTx.transform(scaledPoint, null);
}
/**
* @return the reflection of the first underside mirror that shows a selectable component at the
* specified point, or null
*/
private AffineTransform findMirrorWithComponentAt(Point2D scaledPoint) {
for (IBoard board : UndersideMirror.findBoardsMirroredAt(currentProject, scaledPoint)) {
List<IDIYComponent<?>> components =
drawingManager.findMirroredComponentsAt(scaledPoint, currentProject, board);
removeUnselectable(components, false);
if (!components.isEmpty()) {
return UndersideMirror.createTransform(board);
}
}
return null;
}
@Override
public void saveSelectionAsBlock(String blockName) {
buildingBlockManager.saveSelectionAsBlock(blockName, this.getSelectedComponents(), this.currentProject.getComponents());
@@ -1,8 +1,10 @@
package org.diylc;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
@@ -17,14 +19,19 @@ import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
import org.diylc.clipboard.ComponentTransferable;
import org.diylc.common.ComponentType;
import org.diylc.common.DrawOption;
import org.diylc.common.EventType;
import org.diylc.common.IPlugIn;
import org.diylc.common.IPlugInPort;
import org.diylc.components.boards.BlankBoard;
import org.diylc.components.chassis.ChassisPanel;
import org.diylc.components.composite.CompositeComponent;
import org.diylc.core.BoardUndersideDisplay;
import org.diylc.core.IBoard;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Template;
import org.diylc.presenter.Presenter;
import org.diylc.presenter.UndersideMirror;
import org.diylc.swing.ActionFactory;
import org.diylc.swing.actions.ComponentCategoryAction;
import org.diylc.swing.actions.ResetCategoriesAction;
@@ -281,6 +288,75 @@ public class PresenterTests extends TestBase {
presenter.dragEnded(new Point(1300, 1100));
}
@Test
public void testStretchThroughUndersideMirror() {
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
IDIYComponent<?> resistor = presenter.getCurrentProject().getComponents().get(0);
IBoard board = boardUnder(resistor);
Point2D start = new Point2D.Double(resistor.getControlPoint(1).getX(), resistor.getControlPoint(1).getY());
Point inMirror = toScreen(UndersideMirror.createTransform(board).transform(start, null));
presenter.mouseMoved(inMirror, false, false, false);
presenter.dragStarted(inMirror, java.awt.dnd.DnDConstants.ACTION_MOVE, false, false);
presenter.dragOver(new Point(inMirror.x + 100, inMirror.y + 100));
presenter.dragEnded(new Point(inMirror.x + 100, inMirror.y + 100));
// the mirror is flipped vertically, so moving down in it moves the point up on the board
Assert.assertTrue(resistor.getControlPoint(1).getX() > start.getX());
Assert.assertTrue(resistor.getControlPoint(1).getY() < start.getY());
}
@Test
public void testDragThroughUndersideMirror() {
instantiateTwoClick("Passive", "Resistor", new Point(1000, 1000), new Point(1200, 1000));
IDIYComponent<?> resistor = presenter.getCurrentProject().getComponents().get(0);
IBoard board = boardUnder(resistor);
Point2D start0 = new Point2D.Double(resistor.getControlPoint(0).getX(), resistor.getControlPoint(0).getY());
Point2D start1 = new Point2D.Double(resistor.getControlPoint(1).getX(), resistor.getControlPoint(1).getY());
// the hit test reads the areas recorded while drawing
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
presenter.draw(g2d, EnumSet.of(DrawOption.ZOOM), null, null, null, null);
g2d.dispose();
Point2D middle = new Point2D.Double((start0.getX() + start1.getX()) / 2, start0.getY());
Point inMirror = toScreen(UndersideMirror.createTransform(board).transform(middle, null));
presenter.mouseMoved(inMirror, false, false, false);
presenter.dragStarted(inMirror, java.awt.dnd.DnDConstants.ACTION_MOVE, false, false);
presenter.dragOver(new Point(inMirror.x + 100, inMirror.y + 100));
presenter.dragEnded(new Point(inMirror.x + 100, inMirror.y + 100));
Assert.assertTrue(resistor.getControlPoint(0).getX() > start0.getX());
Assert.assertTrue(resistor.getControlPoint(0).getY() < start0.getY());
Assert.assertEquals(resistor.getControlPoint(0).getY(), resistor.getControlPoint(1).getY());
Assert.assertEquals(start1.getX() - start0.getX(),
resistor.getControlPoint(1).getX() - resistor.getControlPoint(0).getX());
}
// 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) {
Point2D first = component.getControlPoint(0);
Point2D last = component.getControlPoint(component.getControlPointCount() - 1);
BlankBoard board = new BlankBoard();
board.setControlPoint(new Point2D.Double(first.getX() - 200, first.getY() - 200), 0);
board.setControlPoint(new Point2D.Double(last.getX() + 200, last.getY() + 200), 1);
board.setUndersideDisplay(BoardUndersideDisplay.BELOW);
presenter.getCurrentProject().getComponents().add(0, board);
return board;
}
// the inverse of the zoom and extra-space scaling the presenter applies to mouse points
private Point toScreen(Point2D modelPoint) {
double zoom = presenter.getZoomLevel();
double extraSpace = InMemoryConfigurationManager.getInstance()
.readBoolean(IPlugInPort.EXTRA_SPACE_KEY, true)
? presenter.getDrawingManager().getExtraSpace(presenter.getCurrentProject())
: 0;
return new Point((int) Math.round((modelPoint.getX() + extraSpace) * zoom),
(int) Math.round((modelPoint.getY() + extraSpace) * zoom));
}
@Test
public void testShiftLocksAxisWhilePlacing() {
Map<String, List<ComponentType>> componentTypes = presenter.getComponentTypes();
+16 -13
View File
@@ -5,7 +5,7 @@ Gerber layers and the canvas shows it as a part on the far side of the board.
Status: the core Gerber side is done, with tests: `IBoardSideComponent` exists and the export
sends a bottom-side part to the bottom layers, but no component implements it yet. Parts can be
selected through the Underside Display mirror. The next steps below remain.
selected, dragged and stretched through the Underside Display mirror. The next steps below remain.
## 1. Current state
@@ -98,14 +98,16 @@ order.
the main view; locked and hidden parts are filtered out first, so a locked part under the mirror
does not block it. Clicking a part in the mirror selects the original, so the property editor,
delete, copy and the popup menu all work unchanged. Selection highlighting and the selected
control points are drawn on both copies. A drag still tests the main view only, and so do the
hand cursor and control-point hover, because they promise dragging; all three move to stage 2.
- **Stage 2: dragging in the mirror.** Dragging, the hand cursor and control-point hover use the
mirror. A drag that starts in a mirror region reflects every
subsequent mouse point before it reaches the existing drag code. Movement along the mirrored axis
is therefore reversed, which is what the user sees. Snapping applies to the reflected point. Moving
the board, or changing its Underside Offset, cancels nothing; the mirror simply follows.
Control-point drags (stretching a trace) go through the same mapping.
control points are drawn on both copies.
- **Stage 2: dragging in the mirror (done).** A drag that starts on a part in a mirror, or on a
control point of a selected part there, keeps that mirror's reflection in
`Presenter.dragMirrorTx` and reflects every mouse point of the drag before it reaches the existing
drag code. Movement along the mirrored axis is therefore reversed, which is what the user sees,
and snapping and the shift axis lock apply to the reflected point. Control-point hover
(`Presenter.findControlPointsAt`) and the hand cursor look through the mirror when the main view
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) and mirrored, as Flip to Other Side would do. A `PCBLayer` component is created on
@@ -141,7 +143,9 @@ order.
appears only in `.gbo`, mirrored, and in the right position.
- `UndersideMirrorTest` covers the mirror region for each `BoardUndersideDisplay` direction and
offset, that reflecting a point twice returns it, and which components a mirror shows.
`DrawingManagerTest.testComponentFoundThroughUndersideMirror` covers the hit test.
`DrawingManagerTest.testComponentFoundThroughUndersideMirror` covers the hit test, and
`PresenterTests.testStretchThroughUndersideMirror` and `testDragThroughUndersideMirror` check
that a drag through a mirror below the board moves the part up on the board.
- 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, mirrored, faded on the main view and solid in the mirror.
@@ -154,6 +158,5 @@ order.
2. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`.
3. Add the Flip to Other Side action.
4. Split the silkscreen print-out by side.
5. Mirror stage 2: dragging through the mirror.
6. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
7. Run the regression suite.
5. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
6. Run the regression suite.