Parts placed on the bottom of the board are drawn faded on the canvas and solid in the Underside Display mirror

This commit is contained in:
Bane Stojkovic
2026-11-11 15:20:06 +01:00
parent df873df04a
commit 9765f04e3c
5 changed files with 145 additions and 12 deletions
@@ -153,12 +153,16 @@ public class DrawingCache {
if (Math.abs(1.0 - zoom) > 1e-4) {
g2d.getCanvasGraphics().scale(1 / zoom, 1 / zoom);
}
// the image was rendered with the far-side fade already applied, so pasting it must not
// fade it again
g2d.startDrawingBothSides();
try {
// draw cached image
g2d.scale(1 / scaleFactor, 1 / scaleFactor);
g2d.getCanvasGraphics().drawImage(value.getImage(), (int) ((firstPoint.getX() - value.getDx()) * zoom * scaleFactor),
(int) ((firstPoint.getY() - value.getDy())* zoom * scaleFactor), null);
} finally {
g2d.stopDrawingSide();
if (Math.abs(1.0 - zoom) > 1e-4) {
g2d.getCanvasGraphics().scale(zoom, zoom);
}
@@ -347,6 +351,7 @@ public class DrawingCache {
// initialize wrapper
wrapper.startedDrawingComponent(zOrder);
wrapper.setUndeclaredSide(DrawingManager.getMountedSide(component));
long componentStart = System.nanoTime();
@@ -303,6 +303,8 @@ public class DrawingManager {
if (lockedComponents.contains(component) && drawOptions.contains(DrawOption.LOCKED_ALPHA)) {
g2d.setComposite(lockedComposite);
}
// after the locked composite, which is set on the canvas and would replace the fade
g2dWrapper.setUndeclaredSide(getMountedSide(component));
// Draw the component through the g2dWrapper.
long componentStart = System.nanoTime();
try {
@@ -334,6 +336,7 @@ public class DrawingManager {
}
}
ComponentArea area = g2dWrapper.finishedDrawingComponent();
g2dWrapper.setUndeclaredSide(PCBLayer._2);
// after finishedDrawingComponent, which reverts the transform the clip was taken in
if (visibleClip != null) {
g2d.setClip(visibleClip);
@@ -527,6 +530,15 @@ public class DrawingManager {
return failedComponents;
}
/**
* @return the side of the board the component is mounted on; components that do not say are
* on top
*/
static PCBLayer getMountedSide(IDIYComponent<?> component) {
return component instanceof IBoardSideComponent
? ((IBoardSideComponent) component).getBoardSide() : PCBLayer._2;
}
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),
@@ -565,9 +577,10 @@ public class DrawingManager {
g2dWrapper.setMirrorDirection(mirrorDirection);
try {
g2dWrapper.transform(mirrorTx);
// the mirror is the copper side, so copper is drawn solid and the component side faded
// the mirror is the copper side, so copper and the parts mounted on it are drawn solid and
// the component side faded
g2dWrapper.setViewedSide(PCBLayer._1, Boolean.TRUE.equals(board.getUndersideTransparency()),
component instanceof IBoard ? PCBLayer._1 : PCBLayer._2);
component instanceof IBoard ? PCBLayer._1 : getMountedSide(component));
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
} catch (Exception e) {
LOG.error("Error drawing mirrored " + component.getName(), e);
@@ -186,6 +186,19 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
updateFarSideFade();
}
/**
* Sets the side assumed for paint that is not bracketed with
* {@link #startDrawingSide(PCBLayer)}, keeping the viewed side. A part mounted on the bottom of
* the board is drawn on the bottom side as a whole.
*
* @param undeclaredSide
*/
public void setUndeclaredSide(PCBLayer undeclaredSide) {
this.undeclaredSide = undeclaredSide;
this.paintedSide = undeclaredSide;
updateFarSideFade();
}
/**
* Returns to the top-side view, fading the bottom side only if the draw options ask for it.
*/
@@ -27,6 +27,7 @@ import org.diylc.common.PCBLayer;
import org.diylc.core.BoardUndersideDisplay;
import org.diylc.core.ComponentState;
import org.diylc.core.IBoard;
import org.diylc.core.IBoardSideComponent;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
@@ -683,6 +684,66 @@ public class DrawingManagerTest {
assertSolid(img, 155, 665);
}
@Test
public void testBottomMountedPartFadedOnlyWithOption() {
Project project = new Project();
project.getComponents().add(new MountedComponent(new Rectangle2D.Double(150, 150, 10, 10), PCBLayer._2));
project.getComponents().add(new MountedComponent(new Rectangle2D.Double(200, 200, 10, 10), PCBLayer._1));
BufferedImage faded = renderSides(project, EnumSet.of(DrawOption.ZOOM, DrawOption.FADE_FAR_SIDE));
assertSolid(faded, 155, 155);
assertFaded(faded, 205, 205);
BufferedImage plain = renderSides(project, EnumSet.of(DrawOption.ZOOM));
assertSolid(plain, 205, 205);
}
@Test
public void testCachedBottomMountedPartFaded() {
Project project = new Project();
project.getComponents().add(new MountedComponent(new Rectangle2D.Double(200, 200, 10, 10), PCBLayer._1));
BufferedImage img = renderSides(project,
EnumSet.of(DrawOption.ZOOM, DrawOption.FADE_FAR_SIDE, DrawOption.ENABLE_CACHING));
assertFaded(img, 205, 205);
}
@Test
public void testMirrorShowsBottomMountedPartSolid() {
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));
board.setUndersideTransparency(true);
project.getComponents().add(board);
project.getComponents().add(new MountedComponent(new Rectangle2D.Double(150, 150, 10, 10), PCBLayer._2));
project.getComponents().add(new MountedComponent(new Rectangle2D.Double(200, 200, 10, 10), PCBLayer._1));
// reflection across y = 410 puts the top part at y 660..670 and the bottom one at 610..620
BufferedImage img = renderSides(project, EnumSet.of(DrawOption.ZOOM, DrawOption.FADE_FAR_SIDE));
assertFaded(img, 155, 665);
assertSolid(img, 205, 615);
}
@Test
public void testLockedBottomMountedPartNotFadedTwice() {
Project project = new Project();
MountedComponent bottom = new MountedComponent(new Rectangle2D.Double(200, 200, 10, 10), PCBLayer._1);
project.getComponents().add(bottom);
DrawingManager drawingManager = createDrawingManager();
BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
drawingManager.drawProject(g2d, project,
EnumSet.of(DrawOption.ZOOM, DrawOption.FADE_FAR_SIDE, DrawOption.LOCKED_ALPHA), null, null,
Collections.emptyList(), Collections.singleton(bottom), Collections.emptySet(), null, null,
false, Collections.emptySet(), 1.0, 1.0, null);
g2d.dispose();
assertFaded(img, 205, 205, Math.min(0.5f, farSideAlpha()));
}
@Test
public void testLockedBottomSideNotFadedTwice() {
Project project = new Project();
@@ -990,6 +1051,34 @@ public class DrawingManagerTest {
}
// paints an opaque black square, declared on the given side or, when null, left undeclared
// paints an undeclared black square, like a part that leaves its side to the Board Side property
@ComponentDescriptor(name = "Mounted", author = "test", category = "Test", instanceNamePrefix = "M",
description = "Part mounted on one side of the board", zOrder = IDIYComponent.COMPONENT,
enableCache = true)
public static class MountedComponent extends SidePaintingComponent implements IBoardSideComponent {
private static final long serialVersionUID = 1L;
private Rectangle2D bounds;
private PCBLayer boardSide;
public MountedComponent(Rectangle2D shape, PCBLayer boardSide) {
super(shape, null);
this.bounds = shape;
this.boardSide = boardSide;
}
@Override
public PCBLayer getBoardSide() {
return boardSide;
}
@Override
public Rectangle2D getCachingBounds() {
return bounds;
}
}
public static class SidePaintingComponent extends MockDIYComponent {
private static final long serialVersionUID = 1L;