From 9765f04e3c6f250459296f7a98b3874290be1ef9 Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Wed, 11 Nov 2026 15:20:06 +0100 Subject: [PATCH] Parts placed on the bottom of the board are drawn faded on the canvas and solid in the Underside Display mirror --- .../org/diylc/presenter/DrawingCache.java | 5 ++ .../org/diylc/presenter/DrawingManager.java | 17 +++- .../java/org/diylc/presenter/G2DWrapper.java | 13 +++ .../diylc/presenter/DrawingManagerTest.java | 89 +++++++++++++++++++ diylc/docs/plans/bottom-side-components.md | 33 ++++--- 5 files changed, 145 insertions(+), 12 deletions(-) diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingCache.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingCache.java index 48b3d8e32..717c20b6c 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingCache.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingCache.java @@ -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(); diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingManager.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingManager.java index e9e04212a..57daa956b 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingManager.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/DrawingManager.java @@ -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); diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/G2DWrapper.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/G2DWrapper.java index af5f59f85..23c3ba77f 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/G2DWrapper.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/G2DWrapper.java @@ -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. */ diff --git a/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java b/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java index f943f2b5a..923b66a65 100644 --- a/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java +++ b/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java @@ -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; diff --git a/diylc/docs/plans/bottom-side-components.md b/diylc/docs/plans/bottom-side-components.md index 684cdd5d6..61a44025a 100644 --- a/diylc/docs/plans/bottom-side-components.md +++ b/diylc/docs/plans/bottom-side-components.md @@ -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 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 -Side action does not exist; the next steps below remain. +through the Underside Display mirror. The canvas draws a bottom-side part faded in the main view and +solid in the mirror. The Flip to Other Side action does not exist yet; the next steps below +remain. ## 1. Current state @@ -50,9 +51,15 @@ Side action does not exist; the next steps below remain. ### diylc-core -- **Canvas rendering**: bottom-side components use the far-side fade from `copper-side-view.md`, - wrapping their painting in `startDrawingSide(getBoardSide())`. They are then faded in the main - view and solid in the Underside Display mirror, the same as bottom copper. +- **Canvas rendering (done)**: bottom-side parts use the far-side fade from `copper-side-view.md`, + set centrally rather than in each component. Wherever a component is drawn - the main loop in + `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 @@ -132,6 +139,10 @@ order. - **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 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 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. @@ -140,6 +151,9 @@ order. - `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components, 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 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 @@ -156,8 +170,7 @@ order. ## 6. Next steps -1. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. -2. Add the Flip to Other Side action. -3. Split the silkscreen print-out by side. -4. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. -5. Run the regression suite. +1. Add the Flip to Other Side action. +2. Split the silkscreen print-out by side. +3. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. +4. Run the regression suite.