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) { if (Math.abs(1.0 - zoom) > 1e-4) {
g2d.getCanvasGraphics().scale(1 / zoom, 1 / zoom); 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 { try {
// draw cached image // draw cached image
g2d.scale(1 / scaleFactor, 1 / scaleFactor); g2d.scale(1 / scaleFactor, 1 / scaleFactor);
g2d.getCanvasGraphics().drawImage(value.getImage(), (int) ((firstPoint.getX() - value.getDx()) * zoom * scaleFactor), g2d.getCanvasGraphics().drawImage(value.getImage(), (int) ((firstPoint.getX() - value.getDx()) * zoom * scaleFactor),
(int) ((firstPoint.getY() - value.getDy())* zoom * scaleFactor), null); (int) ((firstPoint.getY() - value.getDy())* zoom * scaleFactor), null);
} finally { } finally {
g2d.stopDrawingSide();
if (Math.abs(1.0 - zoom) > 1e-4) { if (Math.abs(1.0 - zoom) > 1e-4) {
g2d.getCanvasGraphics().scale(zoom, zoom); g2d.getCanvasGraphics().scale(zoom, zoom);
} }
@@ -347,6 +351,7 @@ public class DrawingCache {
// initialize wrapper // initialize wrapper
wrapper.startedDrawingComponent(zOrder); wrapper.startedDrawingComponent(zOrder);
wrapper.setUndeclaredSide(DrawingManager.getMountedSide(component));
long componentStart = System.nanoTime(); long componentStart = System.nanoTime();
@@ -303,6 +303,8 @@ public class DrawingManager {
if (lockedComponents.contains(component) && drawOptions.contains(DrawOption.LOCKED_ALPHA)) { if (lockedComponents.contains(component) && drawOptions.contains(DrawOption.LOCKED_ALPHA)) {
g2d.setComposite(lockedComposite); 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. // Draw the component through the g2dWrapper.
long componentStart = System.nanoTime(); long componentStart = System.nanoTime();
try { try {
@@ -334,6 +336,7 @@ public class DrawingManager {
} }
} }
ComponentArea area = g2dWrapper.finishedDrawingComponent(); ComponentArea area = g2dWrapper.finishedDrawingComponent();
g2dWrapper.setUndeclaredSide(PCBLayer._2);
// after finishedDrawingComponent, which reverts the transform the clip was taken in // after finishedDrawingComponent, which reverts the transform the clip was taken in
if (visibleClip != null) { if (visibleClip != null) {
g2d.setClip(visibleClip); g2d.setClip(visibleClip);
@@ -527,6 +530,15 @@ public class DrawingManager {
return failedComponents; 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) { private static void drawSelectedControlPoint(G2DWrapper g2dWrapper, Point2D controlPoint) {
g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR.darker()); g2dWrapper.setColor(SELECTED_CONTROL_POINT_COLOR.darker());
g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - CONTROL_POINT_SIZE / 2), g2dWrapper.fillOval((int) Math.round(controlPoint.getX() - CONTROL_POINT_SIZE / 2),
@@ -565,9 +577,10 @@ public class DrawingManager {
g2dWrapper.setMirrorDirection(mirrorDirection); g2dWrapper.setMirrorDirection(mirrorDirection);
try { try {
g2dWrapper.transform(mirrorTx); 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()), 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); component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Error drawing mirrored " + component.getName(), e); LOG.error("Error drawing mirrored " + component.getName(), e);
@@ -186,6 +186,19 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
updateFarSideFade(); 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. * 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.BoardUndersideDisplay;
import org.diylc.core.ComponentState; import org.diylc.core.ComponentState;
import org.diylc.core.IBoard; import org.diylc.core.IBoard;
import org.diylc.core.IBoardSideComponent;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver; import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project; import org.diylc.core.Project;
@@ -683,6 +684,66 @@ public class DrawingManagerTest {
assertSolid(img, 155, 665); 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 @Test
public void testLockedBottomSideNotFadedTwice() { public void testLockedBottomSideNotFadedTwice() {
Project project = new Project(); 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 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 { public static class SidePaintingComponent extends MockDIYComponent {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
+23 -10
View File
@@ -5,8 +5,9 @@ Gerber layers and the canvas shows it as a part on the far side of the board.
Status: 32 parts have a Board Side property, and Gerber export sends a bottom-side part's Status: 32 parts have a Board Side property, and Gerber export sends a bottom-side part's
silkscreen to the bottom layer with its text mirrored. Parts can be selected, dragged and stretched silkscreen to the bottom layer with its text mirrored. Parts can be selected, dragged and stretched
through the Underside Display mirror. The canvas does not show the side yet, and the Flip to Other through the Underside Display mirror. The canvas draws a bottom-side part faded in the main view and
Side action does not exist; the next steps below remain. solid in the mirror. The Flip to Other Side action does not exist yet; the next steps below
remain.
## 1. Current state ## 1. Current state
@@ -50,9 +51,15 @@ Side action does not exist; the next steps below remain.
### diylc-core ### diylc-core
- **Canvas rendering**: bottom-side components use the far-side fade from `copper-side-view.md`, - **Canvas rendering (done)**: bottom-side parts use the far-side fade from `copper-side-view.md`,
wrapping their painting in `startDrawingSide(getBoardSide())`. They are then faded in the main set centrally rather than in each component. Wherever a component is drawn - the main loop in
view and solid in the Underside Display mirror, the same as bottom copper. `DrawingManager`, `DrawingCache` rendering an image, and the Underside Display mirror - the
wrapper's undeclared side is set to `DrawingManager.getMountedSide(component)` with
`G2DWrapper.setUndeclaredSide`. A bottom part is therefore faded in the main view (with Fade
Copper Under Board on) and solid in the mirror, the same as bottom copper. In the main loop the
side is set after the locked composite, which is set on the canvas directly and would otherwise
replace the fade. `DrawingCache` pastes a cached image as both-sides paint, because the fade is
already in the image.
### diylc-library ### diylc-library
@@ -132,6 +139,10 @@ order.
- **Components that belong to no side.** In mirror stage 3 a jumper, eyelet or wire dropped in the - **Components that belong to no side.** In mirror stage 3 a jumper, eyelet or wire dropped in the
mirror has no side to set. Confirm that placing it at the reflected point unchanged is what users mirror has no side to set. Confirm that placing it at the reflected point unchanged is what users
expect, or whether such parts should be refused in the mirror. expect, or whether such parts should be refused in the mirror.
- **Locked bottom-side parts drawn from the cache.** An uncached locked part takes the stronger of
the locked alpha and the far-side alpha. A cached one is pasted at the locked alpha onto an
image that already carries the far-side fade, so it ends up fainter. The same applies to
bottom-side paint inside any cached component.
- **Components inside two boards.** A component whose control points fall inside two boards with - **Components inside two boards.** A component whose control points fall inside two boards with
Underside Display on appears in both mirrors. Selecting through either is fine. Check that Underside Display on appears in both mirrors. Selecting through either is fine. Check that
dragging through one mirror while the other redraws does not confuse the user. dragging through one mirror while the other redraws does not confuse the user.
@@ -140,6 +151,9 @@ order.
- `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components, - `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components,
including a stub `IBoardSideComponent` on top, bottom and `null`. including a stub `IBoardSideComponent` on top, bottom and `null`.
- `DrawingManagerTest` checks rendered pixels for a part that reports its own side: a bottom part
is faded only with `FADE_FAR_SIDE`, also when drawn from the image cache; it is solid in the
mirror, where a top part is faded; and a locked bottom part is faded once, not twice.
- Load an old `.diy` with PCB parts and confirm every component reports `_2` (Top) and that the Gerber - Load an old `.diy` with PCB parts and confirm every component reports `_2` (Top) and that the Gerber
output is byte-identical to before. 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 - Export a board with one part on each side and open the files in a Gerber viewer: the bottom part
@@ -156,8 +170,7 @@ order.
## 6. Next steps ## 6. Next steps
1. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. 1. Add the Flip to Other Side action.
2. Add the Flip to Other Side action. 2. Split the silkscreen print-out by side.
3. Split the silkscreen print-out by side. 3. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
4. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. 4. Run the regression suite.
5. Run the regression suite.