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) { public List<IDIYComponent<?>> findMirroredComponentsAt(Point2D point, Project project) {
List<IDIYComponent<?>> components = new ArrayList<IDIYComponent<?>>(); List<IDIYComponent<?>> components = new ArrayList<IDIYComponent<?>>();
for (IBoard board : UndersideMirror.findBoardsMirroredAt(project, point)) { for (IBoard board : UndersideMirror.findBoardsMirroredAt(project, point)) {
Point2D reflected = UndersideMirror.createTransform(board).transform(point, null); for (IDIYComponent<?> component : findMirroredComponentsAt(point, project, board)) {
for (IDIYComponent<?> component : findComponentsAt(reflected, project)) { if (!components.contains(component)) {
if (UndersideMirror.isMirrored(board, component) && !components.contains(component)) {
components.add(component); components.add(component);
} }
} }
@@ -761,6 +760,30 @@ public class DrawingManager {
return components; 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) { public double getExtraSpace(Project project) {
double width = project.getWidth().convertToPixels(); double width = project.getWidth().convertToPixels();
double height = project.getHeight().convertToPixels(); double height = project.getHeight().convertToPixels();
@@ -172,6 +172,11 @@ public class Presenter implements IPlugInPort {
private Point2D dragStartPoint = null; private Point2D dragStartPoint = null;
private boolean dragShiftDown = false; private boolean dragShiftDown = false;
private AxisLock lockedAxis = null; 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; private DIYTest test = null;
@@ -279,16 +284,8 @@ public class Presenter implements IPlugInPort {
if (controlPointMap != null && !controlPointMap.isEmpty()) { if (controlPointMap != null && !controlPointMap.isEmpty()) {
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
} }
for (IDIYComponent<?> component : currentProject.getComponents()) { if (scaledPoint != null && !findComponentsAtScaled(scaledPoint, false).isEmpty()) {
if (!isComponentLocked(component) && isComponentVisible(component) return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
&& 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)) {
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
}
}
} }
} }
@@ -569,8 +566,8 @@ public class Presenter implements IPlugInPort {
return findComponentsAtScaled(point, includeLocked, true); return findComponentsAtScaled(point, includeLocked, true);
} }
// a drag does not look through the underside mirror, because moving the mouse there moves the // a drag reflects its points out of the underside mirror itself, so its hit tests must not look
// part the other way along the mirrored axis // through the mirror a second time
private List<IDIYComponent<?>> findComponentsAtScaled(Point2D point, boolean includeLocked, private List<IDIYComponent<?>> findComponentsAtScaled(Point2D point, boolean includeLocked,
boolean throughMirror) { boolean throughMirror) {
List<IDIYComponent<?>> components = drawingManager.findComponentsAt(point, currentProject); 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 @Override
public void mouseMoved(Point point, boolean ctrlDown, boolean shiftDown, boolean altDown) { public void mouseMoved(Point point, boolean ctrlDown, boolean shiftDown, boolean altDown) {
if (point == null) if (point == null)
@@ -996,26 +1029,16 @@ public class Presenter implements IPlugInPort {
messageDispatcher.dispatchMessage(EventType.REPAINT); messageDispatcher.dispatchMessage(EventType.REPAINT);
} }
} else { } else {
// Go backwards so we take the highest z-order components first. components = findControlPointsAt(previousScaledPoint, null);
for (int i = currentProject.getComponents().size() - 1; i >= 0; i--) { hoverMirrorTx = null;
IDIYComponent<?> component = currentProject.getComponents().get(i); for (IBoard board : UndersideMirror.findBoardsMirroredAt(currentProject, previousScaledPoint)) {
for (int pointIndex = 0; pointIndex < component.getControlPointCount(); pointIndex++) { if (!components.isEmpty()) {
Point2D controlPoint = component.getControlPoint(pointIndex); break;
// Only consider selected components that are not grouped. }
if (selectedComponents.contains(component) && component.canPointMoveFreely(pointIndex) AffineTransform mirrorTx = UndersideMirror.createTransform(board);
&& findAllGroupedComponents(component).size() == 1) { components = findControlPointsAt(mirrorTx.transform(previousScaledPoint, null), board);
try { if (!components.isEmpty()) {
if (previousScaledPoint.distance(controlPoint) < DrawingManager.CONTROL_POINT_SIZE && hoverMirrorTx = mirrorTx;
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());
}
}
} }
} }
} }
@@ -1364,6 +1387,14 @@ public class Presenter implements IPlugInPort {
this.dragAction = dragAction; this.dragAction = dragAction;
this.preDragProject = currentProject.clone(); this.preDragProject = currentProject.clone();
Point2D scaledPoint = scalePoint(point); 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.previousDragPoint = scaledPoint;
this.dragStartPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY()); this.dragStartPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY());
this.dragShiftDown = shiftDown; this.dragShiftDown = shiftDown;
@@ -1586,7 +1617,7 @@ public class Presenter implements IPlugInPort {
boolean resizeInProgress = false; boolean resizeInProgress = false;
Point2D scaledPoint = scalePoint(point); Point2D scaledPoint = toDragSpace(scalePoint(point));
if (controlPointMap != null && !controlPointMap.isEmpty()) { if (controlPointMap != null && !controlPointMap.isEmpty()) {
// We're dragging control point(s). // We're dragging control point(s).
Point2D targetPoint = constrainToAxis(dragStartPoint, scaledPoint, dragShiftDown); Point2D targetPoint = constrainToAxis(dragStartPoint, scaledPoint, dragShiftDown);
@@ -2004,7 +2035,7 @@ public class Presenter implements IPlugInPort {
test.addStep(DIYTest.DRAG_END, params); test.addStep(DIYTest.DRAG_END, params);
} }
Point2D scaledPoint = scalePoint(point); Point2D scaledPoint = toDragSpace(scalePoint(point));
if (!dragInProgress && instantiationManager.getComponentSlot() == null) { if (!dragInProgress && instantiationManager.getComponentSlot() == null) {
return; return;
@@ -2056,6 +2087,7 @@ public class Presenter implements IPlugInPort {
dragStartPoint = null; dragStartPoint = null;
dragShiftDown = false; dragShiftDown = false;
lockedAxis = null; lockedAxis = null;
dragMirrorTx = null;
} }
@Override @Override
@@ -3155,6 +3187,27 @@ public class Presenter implements IPlugInPort {
return p; 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 @Override
public void saveSelectionAsBlock(String blockName) { public void saveSelectionAsBlock(String blockName) {
buildingBlockManager.saveSelectionAsBlock(blockName, this.getSelectedComponents(), this.currentProject.getComponents()); buildingBlockManager.saveSelectionAsBlock(blockName, this.getSelectedComponents(), this.currentProject.getComponents());
@@ -1,8 +1,10 @@
package org.diylc; package org.diylc;
import java.awt.Graphics2D;
import java.awt.Point; import java.awt.Point;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
@@ -17,14 +19,19 @@ import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.appframework.miscutils.InMemoryConfigurationManager; import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
import org.diylc.clipboard.ComponentTransferable; import org.diylc.clipboard.ComponentTransferable;
import org.diylc.common.ComponentType; import org.diylc.common.ComponentType;
import org.diylc.common.DrawOption;
import org.diylc.common.EventType; import org.diylc.common.EventType;
import org.diylc.common.IPlugIn; import org.diylc.common.IPlugIn;
import org.diylc.common.IPlugInPort; import org.diylc.common.IPlugInPort;
import org.diylc.components.boards.BlankBoard;
import org.diylc.components.chassis.ChassisPanel; import org.diylc.components.chassis.ChassisPanel;
import org.diylc.components.composite.CompositeComponent; 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.IDIYComponent;
import org.diylc.core.Template; import org.diylc.core.Template;
import org.diylc.presenter.Presenter; import org.diylc.presenter.Presenter;
import org.diylc.presenter.UndersideMirror;
import org.diylc.swing.ActionFactory; import org.diylc.swing.ActionFactory;
import org.diylc.swing.actions.ComponentCategoryAction; import org.diylc.swing.actions.ComponentCategoryAction;
import org.diylc.swing.actions.ResetCategoriesAction; import org.diylc.swing.actions.ResetCategoriesAction;
@@ -281,6 +288,75 @@ public class PresenterTests extends TestBase {
presenter.dragEnded(new Point(1300, 1100)); 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 @Test
public void testShiftLocksAxisWhilePlacing() { public void testShiftLocksAxisWhilePlacing() {
Map<String, List<ComponentType>> componentTypes = presenter.getComponentTypes(); 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 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 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 ## 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 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, 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 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 control points are drawn on both copies.
hand cursor and control-point hover, because they promise dragging; all three move to stage 2. - **Stage 2: dragging in the mirror (done).** A drag that starts on a part in a mirror, or on a
- **Stage 2: dragging in the mirror.** Dragging, the hand cursor and control-point hover use the control point of a selected part there, keeps that mirror's reflection in
mirror. A drag that starts in a mirror region reflects every `Presenter.dragMirrorTx` and reflects every mouse point of the drag before it reaches the existing
subsequent mouse point before it reaches the existing drag code. Movement along the mirrored axis drag code. Movement along the mirrored axis is therefore reversed, which is what the user sees,
is therefore reversed, which is what the user sees. Snapping applies to the reflected point. Moving and snapping and the shift axis lock apply to the reflected point. Control-point hover
the board, or changing its Underside Offset, cancels nothing; the mirror simply follows. (`Presenter.findControlPointsAt`) and the hand cursor look through the mirror when the main view
Control-point drags (stretching a trace) go through the same mapping. 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 - **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 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 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. appears only in `.gbo`, mirrored, and in the right position.
- `UndersideMirrorTest` covers the mirror region for each `BoardUndersideDisplay` direction and - `UndersideMirrorTest` covers the mirror region for each `BoardUndersideDisplay` direction and
offset, that reflecting a point twice returns it, and which components a mirror shows. 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 - 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 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. 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())`. 2. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`.
3. Add the Flip to Other Side action. 3. Add the Flip to Other Side action.
4. Split the silkscreen print-out by side. 4. Split the silkscreen print-out by side.
5. Mirror stage 2: dragging through the mirror. 5. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
6. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. 6. Run the regression suite.
7. Run the regression suite.