mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
Components can be selected by clicking them in the Underside Display mirror of a board
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ Let a component be mounted on the bottom of a PCB, so that its silkscreen goes t
|
||||
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. No component implements it yet; the next steps below
|
||||
remain.
|
||||
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.
|
||||
|
||||
## 1. Current state
|
||||
|
||||
@@ -88,18 +88,20 @@ through the mirror's reflection before the existing logic runs. A reflection is
|
||||
so drawing and hit-testing use the same transform. Each stage is useful by itself and ships in
|
||||
order.
|
||||
|
||||
- **Shared mirror geometry.** Move the transform built in `DrawingManager.drawMirroredComponent`
|
||||
into a helper in `org.diylc.presenter` (not into `Presenter`). It answers two questions: the
|
||||
reflection for a given `IBoard`, and which board's mirror region, if any, contains a model point.
|
||||
The region is the board rectangle reflected about the pivot. `drawMirroredComponent` uses the
|
||||
helper, so drawing and hit-testing cannot drift apart.
|
||||
- **Stage 1: selecting in the mirror.** In `Presenter.findComponentsAt` and the click and hover
|
||||
paths, a point that falls in a mirror region and hits nothing in main-view space is reflected
|
||||
and tested again, limited to the components that board mirrors (those whose control point lies
|
||||
inside it, as in `drawMirroredComponent`). 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 control points are drawn on both copies.
|
||||
- **Stage 2: dragging in the mirror.** A drag that starts in a mirror region reflects every
|
||||
- **Shared mirror geometry (done).** `org.diylc.presenter.UndersideMirror` holds the reflection
|
||||
for a board, the region its mirror occupies (the board rectangle reflected about the pivot), and
|
||||
which components a mirror shows: the board and every component with a control point on it.
|
||||
`DrawingManager` draws the mirror with it, so drawing and hit-testing cannot drift apart.
|
||||
- **Stage 1: selecting in the mirror (done).** `DrawingManager.findMirroredComponentsAt` reflects a
|
||||
point that falls in a mirror region and tests it again, limited to the components that board
|
||||
mirrors. `Presenter.findComponentsAtScaled` falls back to it when nothing selectable is hit in
|
||||
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.
|
||||
@@ -137,8 +139,9 @@ order.
|
||||
output is byte-identical to before.
|
||||
- Export a board with one part on each side and open the files in a Gerber viewer: the bottom part
|
||||
appears only in `.gbo`, mirrored, and in the right position.
|
||||
- Unit tests for the mirror geometry helper: the region for each `BoardUndersideDisplay` direction
|
||||
and offset, and that reflecting a point twice returns it.
|
||||
- `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.
|
||||
- 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.
|
||||
@@ -151,9 +154,6 @@ 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. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`.
|
||||
6. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-4 and can ship
|
||||
early.
|
||||
7. Mirror stage 2: dragging through the mirror.
|
||||
8. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
|
||||
9. Run the regression suite.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user