Components can be selected by clicking them in the Underside Display mirror of a board

This commit is contained in:
Bane Stojkovic
2026-11-11 14:56:02 +01:00
parent 3506c97403
commit 90e10f4597
6 changed files with 399 additions and 118 deletions
@@ -193,37 +193,12 @@ public class DrawingManager {
configureRenderingHints(g2d, drawOptions);
// group components with boards that contain them
Map<IBoard, List<IDIYComponent<?>>> boardMap = project.getComponents().stream()
.filter(IBoard.class::isInstance)
.map(IBoard.class::cast)
.filter(b -> b.getUndersideDisplay() != null && b.getUndersideDisplay() != BoardUndersideDisplay.NONE)
.collect(Collectors.toMap(x -> x, x -> new ArrayList<IDIYComponent<?>>()));
// group components with the boards whose underside mirror shows them
Map<IDIYComponent<?>, Set<IBoard>> componentBoardMap = new HashMap<IDIYComponent<?>, Set<IBoard>>();
for (IBoard board : boardMap.keySet()) {
Rectangle2D boardRect = board.getBoardRectangle();
if (boardRect == null) {
continue;
}
double minX = Math.min(boardRect.getMinX(), boardRect.getMaxX());
double maxX = Math.max(boardRect.getMinX(), boardRect.getMaxX());
double minY = Math.min(boardRect.getMinY(), boardRect.getMaxY());
double maxY = Math.max(boardRect.getMinY(), boardRect.getMaxY());
Rectangle2D normalizedBoardRect = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
List<IBoard> mirroringBoards = UndersideMirror.getMirroringBoards(project);
for (IBoard board : mirroringBoards) {
for (IDIYComponent<?> c : project.getComponents()) {
if (c == board) {
continue;
}
boolean include = false;
for (int i = 0; i < c.getControlPointCount(); i++) {
if (normalizedBoardRect.contains(c.getControlPoint(i))) {
include = true;
break;
}
}
if (include) {
boardMap.get(board).add(c);
if (c != board && UndersideMirror.isMirrored(board, c)) {
componentBoardMap.computeIfAbsent(c, key -> new HashSet<IBoard>()).add(board);
}
}
@@ -427,15 +402,14 @@ public class DrawingManager {
|| component.getControlPointVisibilityPolicy(i) == VisibilityPolicy.ALWAYS)) {
Point2D controlPoint = component.getControlPoint(i);
int pointSize = CONTROL_POINT_SIZE;
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR.darker());
g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - pointSize / 2),
(int) Math.round(controlPoint.getY() - pointSize / 2), pointSize, pointSize);
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR);
g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - CONTROL_POINT_SIZE / 2 + 1),
(int) Math.round(controlPoint.getY() - CONTROL_POINT_SIZE / 2 + 1), CONTROL_POINT_SIZE - 2,
CONTROL_POINT_SIZE - 2);
drawSelectedControlPoint(g2dWrapper, controlPoint);
// the part can be selected through its mirrored copy, which then shows its points too
for (IBoard board : mirroringBoards) {
if (UndersideMirror.isMirrored(board, component)) {
drawSelectedControlPoint(g2dWrapper,
UndersideMirror.createTransform(board).transform(controlPoint, null));
}
}
}
}
}
@@ -553,9 +527,20 @@ public class DrawingManager {
return failedComponents;
}
private static void drawSelectedControlPoint(G2DWrapper g2dWrapper, Point2D controlPoint) {
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR.darker());
g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - CONTROL_POINT_SIZE / 2),
(int) Math.round(controlPoint.getY() - CONTROL_POINT_SIZE / 2), CONTROL_POINT_SIZE,
CONTROL_POINT_SIZE);
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR);
g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - CONTROL_POINT_SIZE / 2 + 1),
(int) Math.round(controlPoint.getY() - CONTROL_POINT_SIZE / 2 + 1), CONTROL_POINT_SIZE - 2,
CONTROL_POINT_SIZE - 2);
}
private static void drawMirroredComponent(Project project, IBoard board, IDIYComponent<?> component,
G2DWrapper g2dWrapper, ComponentState state, boolean outlineMode) {
AffineTransform mirrorTx = createMirrorTransform(board);
AffineTransform mirrorTx = UndersideMirror.createTransform(board);
if (mirrorTx == null) {
return;
}
@@ -595,61 +580,6 @@ public class DrawingManager {
}
}
/**
* Returns the reflection that maps the board onto its underside mirror, or null when the board
* has no underside display.
*/
private static AffineTransform createMirrorTransform(IBoard board) {
if (board.getUndersideDisplay() == null || board.getUndersideDisplay() == BoardUndersideDisplay.NONE) {
return null;
}
double offset = board.getUndersideOffset() == null ? 0 : board.getUndersideOffset().convertToPixels();
Rectangle2D boardRectangle = board.getBoardRectangle();
if (boardRectangle == null) {
return null;
}
double minX = Math.min(boardRectangle.getMinX(), boardRectangle.getMaxX());
double maxX = Math.max(boardRectangle.getMinX(), boardRectangle.getMaxX());
double minY = Math.min(boardRectangle.getMinY(), boardRectangle.getMaxY());
double maxY = Math.max(boardRectangle.getMinY(), boardRectangle.getMaxY());
AffineTransform mirrorTx = new AffineTransform();
switch (board.getUndersideDisplay()) {
case ABOVE: {
double pivotY = minY - offset / 2;
mirrorTx.translate(0, pivotY);
mirrorTx.scale(1.0, -1.0);
mirrorTx.translate(0, -pivotY);
break;
}
case BELOW: {
double pivotY = maxY + offset / 2;
mirrorTx.translate(0, pivotY);
mirrorTx.scale(1.0, -1.0);
mirrorTx.translate(0, -pivotY);
break;
}
case LEFT: {
double pivotX = minX - offset / 2;
mirrorTx.translate(pivotX, 0);
mirrorTx.scale(-1.0, 1.0);
mirrorTx.translate(-pivotX, 0);
break;
}
case RIGHT: {
double pivotX = maxX + offset / 2;
mirrorTx.translate(pivotX, 0);
mirrorTx.scale(-1.0, 1.0);
mirrorTx.translate(-pivotX, 0);
break;
}
default:
return null;
}
return mirrorTx;
}
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor,
Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state,
double zoom, int i, boolean outlineMode, boolean cacheable) {
@@ -810,6 +740,27 @@ public class DrawingManager {
return components;
}
/**
* Finds the components that the underside mirror of a board shows at the specified point, top
* first, or none when the point is not in a mirror.
*
* @param point
* @param project
* @return
*/
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)) {
components.add(component);
}
}
}
return components;
}
public double getExtraSpace(Project project) {
double width = project.getWidth().convertToPixels();
double height = project.getHeight().convertToPixels();
@@ -566,7 +566,23 @@ public class Presenter implements IPlugInPort {
* @return
*/
public List<IDIYComponent<?>> findComponentsAtScaled(Point2D point, boolean includeLocked) {
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
private List<IDIYComponent<?>> findComponentsAtScaled(Point2D point, boolean includeLocked,
boolean throughMirror) {
List<IDIYComponent<?>> components = drawingManager.findComponentsAt(point, currentProject);
removeUnselectable(components, includeLocked);
if (throughMirror && components.isEmpty()) {
components = drawingManager.findMirroredComponentsAt(point, currentProject);
removeUnselectable(components, includeLocked);
}
return components;
}
private void removeUnselectable(List<IDIYComponent<?>> components, boolean includeLocked) {
Iterator<IDIYComponent<?>> iterator = components.iterator();
while (iterator.hasNext()) {
IDIYComponent<?> component = iterator.next();
@@ -574,7 +590,6 @@ public class Presenter implements IPlugInPort {
iterator.remove();
}
}
return components;
}
@Override
@@ -1353,7 +1368,8 @@ public class Presenter implements IPlugInPort {
this.dragStartPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY());
this.dragShiftDown = shiftDown;
this.lockedAxis = null;
List<IDIYComponent<?>> components = forceSelectionRect ? null : findComponentsAtScaled(scaledPoint, false);
List<IDIYComponent<?>> components =
forceSelectionRect ? null : findComponentsAtScaled(scaledPoint, false, false);
if (controlPointMap != null && !this.controlPointMap.isEmpty()) {
// If we're dragging control points reset selection.
updateSelection(new ArrayList<IDIYComponent<?>>(this.controlPointMap.keySet()));
@@ -0,0 +1,167 @@
/*
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.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import org.diylc.core.BoardUndersideDisplay;
import org.diylc.core.IBoard;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
/**
* Geometry of the Underside Display mirror, the reflected copy of a board and the components on it
* drawn next to the board. The mirror is a second drawing of the same components, which keep their
* main-view coordinates, so the same reflection draws a component into the mirror and maps a point
* in the mirror back onto the component. A reflection is its own inverse.
*/
public class UndersideMirror {
private UndersideMirror() {
}
/**
* @return the boards of the project that show an underside mirror, in z-order
*/
public static List<IBoard> getMirroringBoards(Project project) {
List<IBoard> boards = new ArrayList<IBoard>();
for (IDIYComponent<?> component : project.getComponents()) {
if (component instanceof IBoard && createTransform((IBoard) component) != null) {
boards.add((IBoard) component);
}
}
return boards;
}
/**
* @return the reflection that maps the board onto its underside mirror, or null when the board
* has no underside display
*/
public static AffineTransform createTransform(IBoard board) {
if (board.getUndersideDisplay() == null
|| board.getUndersideDisplay() == BoardUndersideDisplay.NONE) {
return null;
}
Rectangle2D bounds = getBoardBounds(board);
if (bounds == null) {
return null;
}
double offset =
board.getUndersideOffset() == null ? 0 : board.getUndersideOffset().convertToPixels();
AffineTransform mirrorTx = new AffineTransform();
switch (board.getUndersideDisplay()) {
case ABOVE: {
double pivotY = bounds.getMinY() - offset / 2;
mirrorTx.translate(0, pivotY);
mirrorTx.scale(1.0, -1.0);
mirrorTx.translate(0, -pivotY);
break;
}
case BELOW: {
double pivotY = bounds.getMaxY() + offset / 2;
mirrorTx.translate(0, pivotY);
mirrorTx.scale(1.0, -1.0);
mirrorTx.translate(0, -pivotY);
break;
}
case LEFT: {
double pivotX = bounds.getMinX() - offset / 2;
mirrorTx.translate(pivotX, 0);
mirrorTx.scale(-1.0, 1.0);
mirrorTx.translate(-pivotX, 0);
break;
}
case RIGHT: {
double pivotX = bounds.getMaxX() + offset / 2;
mirrorTx.translate(pivotX, 0);
mirrorTx.scale(-1.0, 1.0);
mirrorTx.translate(-pivotX, 0);
break;
}
default:
return null;
}
return mirrorTx;
}
/**
* @return the part of the canvas the board's mirror occupies, or null when the board has no
* underside display
*/
public static Rectangle2D getMirrorRegion(IBoard board) {
AffineTransform mirrorTx = createTransform(board);
if (mirrorTx == null) {
return null;
}
return mirrorTx.createTransformedShape(getBoardBounds(board)).getBounds2D();
}
/**
* @return true when the board's mirror shows the component: the board itself, or a component
* with a control point on the board
*/
public static boolean isMirrored(IBoard board, IDIYComponent<?> component) {
if (component == board) {
return true;
}
Rectangle2D bounds = getBoardBounds(board);
if (bounds == null) {
return false;
}
for (int i = 0; i < component.getControlPointCount(); i++) {
if (bounds.contains(component.getControlPoint(i))) {
return true;
}
}
return false;
}
/**
* @return the boards whose mirror contains the point, in z-order
*/
public static List<IBoard> findBoardsMirroredAt(Project project, Point2D point) {
List<IBoard> boards = new ArrayList<IBoard>();
for (IBoard board : getMirroringBoards(project)) {
if (getMirrorRegion(board).contains(point)) {
boards.add(board);
}
}
return boards;
}
// the board rectangle may be stored with its corners in either order
private static Rectangle2D getBoardBounds(IBoard board) {
Rectangle2D rect = board.getBoardRectangle();
if (rect == null) {
return null;
}
double minX = Math.min(rect.getMinX(), rect.getMaxX());
double minY = Math.min(rect.getMinY(), rect.getMaxY());
return new Rectangle2D.Double(minX, minY, Math.max(rect.getMinX(), rect.getMaxX()) - minX,
Math.max(rect.getMinY(), rect.getMaxY()) - minY);
}
}
@@ -761,6 +761,29 @@ public class DrawingManagerTest {
canvas.dispose();
}
@Test
public void testComponentFoundThroughUndersideMirror() {
Project project = new Project();
MockBoard board = new MockBoard();
board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300));
board.setUndersideDisplay(BoardUndersideDisplay.BELOW);
board.setUndersideOffset(new Size(20.0, SizeUnit.px));
SidePaintingComponent part =
new SidePaintingComponent(new Rectangle2D.Double(150, 150, 10, 10), null);
project.getComponents().add(board);
project.getComponents().add(part);
DrawingManager drawingManager = createDrawingManager();
drawVisibleRect(drawingManager, project, null);
// reflection across y = 410 puts the square at y 660..670
Point2D inMirror = new Point2D.Double(155, 665);
assertTrue(drawingManager.findComponentsAt(inMirror, project).isEmpty());
assertEquals(Collections.singletonList(part),
drawingManager.findMirroredComponentsAt(inMirror, project));
assertTrue(drawingManager
.findMirroredComponentsAt(new Point2D.Double(155, 155), project).isEmpty());
}
@Test
public void testFarSideFadeLeavesContinuityUnchanged() {
Area faded = drawBottomConductor(EnumSet.of(DrawOption.FADE_FAR_SIDE));
@@ -0,0 +1,124 @@
/*
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 static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Collections;
import org.diylc.core.BoardUndersideDisplay;
import org.diylc.core.Project;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.presenter.DrawingManagerTest.MockBoard;
import org.diylc.presenter.DrawingManagerTest.SidePaintingComponent;
import org.junit.Test;
public class UndersideMirrorTest {
// the board spans x 100..300 and y 100..400, with a 20px gap to its mirror
@Test
public void testMirrorRegionBelow() {
assertEquals(new Rectangle2D.Double(100, 420, 200, 300),
UndersideMirror.getMirrorRegion(board(BoardUndersideDisplay.BELOW)));
}
@Test
public void testMirrorRegionAbove() {
assertEquals(new Rectangle2D.Double(100, -220, 200, 300),
UndersideMirror.getMirrorRegion(board(BoardUndersideDisplay.ABOVE)));
}
@Test
public void testMirrorRegionLeft() {
assertEquals(new Rectangle2D.Double(-120, 100, 200, 300),
UndersideMirror.getMirrorRegion(board(BoardUndersideDisplay.LEFT)));
}
@Test
public void testMirrorRegionRight() {
assertEquals(new Rectangle2D.Double(320, 100, 200, 300),
UndersideMirror.getMirrorRegion(board(BoardUndersideDisplay.RIGHT)));
}
@Test
public void testMirrorRegionFollowsOffset() {
MockBoard board = board(BoardUndersideDisplay.BELOW);
board.setUndersideOffset(new Size(60.0, SizeUnit.px));
assertEquals(new Rectangle2D.Double(100, 460, 200, 300), UndersideMirror.getMirrorRegion(board));
}
@Test
public void testNoMirrorWithoutUndersideDisplay() {
MockBoard board = board(BoardUndersideDisplay.NONE);
assertNull(UndersideMirror.createTransform(board));
assertNull(UndersideMirror.getMirrorRegion(board));
}
@Test
public void testReflectingTwiceReturnsPoint() {
for (BoardUndersideDisplay display : Arrays.asList(BoardUndersideDisplay.ABOVE,
BoardUndersideDisplay.BELOW, BoardUndersideDisplay.LEFT, BoardUndersideDisplay.RIGHT)) {
AffineTransform mirrorTx = UndersideMirror.createTransform(board(display));
Point2D point = new Point2D.Double(123, 456);
assertEquals(point, mirrorTx.transform(mirrorTx.transform(point, null), null));
}
}
@Test
public void testMirroredComponentsAreOnTheBoard() {
MockBoard board = board(BoardUndersideDisplay.BELOW);
assertTrue(UndersideMirror.isMirrored(board, board));
assertTrue(UndersideMirror.isMirrored(board,
new SidePaintingComponent(new Rectangle2D.Double(150, 150, 10, 10), null)));
assertTrue(!UndersideMirror.isMirrored(board,
new SidePaintingComponent(new Rectangle2D.Double(450, 150, 10, 10), null)));
}
@Test
public void testFindBoardsMirroredAt() {
Project project = new Project();
MockBoard below = board(BoardUndersideDisplay.BELOW);
MockBoard none = board(BoardUndersideDisplay.NONE);
project.getComponents().add(below);
project.getComponents().add(none);
assertEquals(Collections.singletonList(below),
UndersideMirror.findBoardsMirroredAt(project, new Point2D.Double(150, 500)));
assertTrue(UndersideMirror.findBoardsMirroredAt(project, new Point2D.Double(150, 300)).isEmpty());
}
private static MockBoard board(BoardUndersideDisplay display) {
MockBoard board = new MockBoard();
board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300));
board.setUndersideDisplay(display);
board.setUndersideOffset(new Size(20.0, SizeUnit.px));
return board;
}
}