From f73574641c84f0b0bd46b41b89d078f08105f5b0 Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Tue, 10 Nov 2026 22:50:16 +0100 Subject: [PATCH] phase 1 --- .../java/org/diylc/common/DrawOption.java | 6 +- .../java/org/diylc/common/IPlugInPort.java | 2 + .../main/java/org/diylc/common/PCBLayer.java | 6 +- .../java/org/diylc/core/IDrawingObserver.java | 24 ++ .../org/diylc/presenter/DrawingManager.java | 86 ++++--- .../java/org/diylc/presenter/G2DWrapper.java | 101 +++++++- .../diylc/presenter/DrawingManagerTest.java | 224 +++++++++++++++++- .../diylc/components/boards/PerfBoard.java | 3 + .../diylc/components/boards/ProtoBoard.java | 7 + .../components/boards/ProtoBoard780.java | 9 + .../diylc/components/boards/TriPadBoard.java | 5 + .../diylc/components/boards/VeroBoard.java | 5 + .../components/connectivity/CopperTrace.java | 10 + .../components/connectivity/CurvedTrace.java | 2 + .../components/connectivity/GroundFill.java | 2 + .../components/connectivity/SolderPad.java | 4 + .../components/connectivity/TraceCut.java | 3 + .../org/diylc/components/misc/PCBText.java | 2 + .../InnerLayerCompatibilityTest.java | 57 +++++ .../diylc/swing/gui/editor/EnumEditor.java | 24 +- .../swing/plugins/canvas/CanvasPanel.java | 3 + .../swing/plugins/config/ConfigPlugin.java | 3 + .../plugins/file/ProjectDrawingProvider.java | 3 + .../src/main/resources/lang/Deutsch.txt | 1 + .../src/main/resources/lang/English.txt | 1 + .../src/main/resources/lang/Español.txt | 1 + .../src/main/resources/lang/Français.txt | 1 + .../src/main/resources/lang/Italiano.txt | 1 + .../src/main/resources/lang/Nederlands.txt | 1 + .../src/main/resources/lang/Português.txt | 1 + .../src/main/resources/lang/Srpski.txt | 1 + .../src/main/resources/lang/हिंदी.txt | 1 + .../src/main/resources/lang/简体中文.txt | 1 + diylc/docs/plans/bottom-side-components.md | 94 ++++++-- diylc/docs/plans/copper-side-view.md | 101 ++++++++ 35 files changed, 729 insertions(+), 67 deletions(-) create mode 100644 diylc/diylc-library/src/test/java/org/diylc/serialization/InnerLayerCompatibilityTest.java create mode 100644 diylc/docs/plans/copper-side-view.md diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/DrawOption.java b/diylc/diylc-core/src/main/java/org/diylc/common/DrawOption.java index 56aba51c6..c0c6fde01 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/DrawOption.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/DrawOption.java @@ -72,5 +72,9 @@ public enum DrawOption { /** * When applied, all colors will be converted to blackest black or whitest white */ - MONOCHROMATIC; + MONOCHROMATIC, + /** + * Paint on the bottom side of a board is drawn at reduced opacity, as if seen through the board. + */ + FADE_FAR_SIDE; } diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java b/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java index 92f415b88..a707dc42e 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java @@ -110,6 +110,8 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP public static final String LOCKED_ALPHA = "lockedAlpha"; + public static final String FADE_FAR_SIDE_KEY = "fadeFarSide"; + public static final String LANGUAGE = "language"; public static final String LANGUAGE_DEFAULT = "English"; diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/PCBLayer.java b/diylc/diylc-core/src/main/java/org/diylc/common/PCBLayer.java index 08c1bef2b..98cc60b89 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/PCBLayer.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/PCBLayer.java @@ -25,7 +25,11 @@ import org.diylc.core.gerber.GerberLayer; public enum PCBLayer { - _1("Bottom"), _2("Top"), _3("Inner 1"), _4("Inner 2"), _5("Inner 3"), _6("Inner 4"), _7("Inner 5"), _8("Inner 6"); + _1("Bottom"), _2("Top"), + // DIYLC models two-sided boards only. The inner layers are kept, in order, so that files using + // them still load with the same layer ids; the editor hides them and Gerber export rejects them. + @Deprecated _3("Inner 1"), @Deprecated _4("Inner 2"), @Deprecated _5("Inner 3"), + @Deprecated _6("Inner 4"), @Deprecated _7("Inner 5"), @Deprecated _8("Inner 6"); private String label; diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/IDrawingObserver.java b/diylc/diylc-core/src/main/java/org/diylc/core/IDrawingObserver.java index fc7fc3997..c0353aeb7 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/IDrawingObserver.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/IDrawingObserver.java @@ -21,6 +21,8 @@ */ package org.diylc.core; +import org.diylc.common.PCBLayer; + /** * Interface for tracking component drawing process. Components may use it to stop or restart * tracking. Anything drawn while tracking is stopped will not be added to the mouse hot-spot area. @@ -65,4 +67,26 @@ public interface IDrawingObserver { * @param marker */ void setContinuityMarker(String marker); + + /** + * Notifies the observer that what follows is painted on the given side of the board, until + * {@link #stopDrawingSide()}. Paint that is not bracketed is on the top (component) side. The + * observer may draw paint on the side facing away from the viewer at reduced opacity, as if seen + * through the board. Inner layers count as the bottom side. + * + * @param side the layer the paint lies on + */ + default void startDrawingSide(PCBLayer side) {} + + /** + * Notifies the observer that what follows is visible from both sides of the board, such as a + * drilled hole or a cut through the copper, and is never drawn faded, until + * {@link #stopDrawingSide()}. + */ + default void startDrawingBothSides() {} + + /** + * Notifies the observer that paint is back on the top (component) side. + */ + default void stopDrawingSide() {} } 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 0a2e1890a..0902075c1 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 @@ -65,7 +65,7 @@ import org.diylc.utils.Constants; public class DrawingManager { private static final Logger LOG = Logger.getLogger(DrawingManager.class); - public static final Percentage MIRROR_ALPHA = new Percentage(50); + public static Percentage FAR_SIDE_ALPHA = new Percentage(55); public static int CONTROL_POINT_SIZE = 7; public static double EXTRA_SPACE = 0.25; @@ -536,13 +536,58 @@ public class DrawingManager { private static void drawMirroredComponent(Project project, IBoard board, IDIYComponent component, G2DWrapper g2dWrapper, ComponentState state, boolean outlineMode) { - if (board.getUndersideDisplay() == null || board.getUndersideDisplay() == BoardUndersideDisplay.NONE) { + AffineTransform mirrorTx = createMirrorTransform(board); + if (mirrorTx == null) { return; } + + G2DWrapper.MirrorDirection mirrorDirection; + switch (board.getUndersideDisplay()) { + case ABOVE: + case BELOW: + mirrorDirection = G2DWrapper.MirrorDirection.VERTICAL; + break; + case LEFT: + case RIGHT: + mirrorDirection = G2DWrapper.MirrorDirection.HORIZONTAL; + break; + default: + mirrorDirection = G2DWrapper.MirrorDirection.NONE; + break; + } + + AffineTransform oldTx = g2dWrapper.getTransform(); + Composite oldComposite = g2dWrapper.getComposite(); + g2dWrapper.setMirrorDirection(mirrorDirection); + try { + g2dWrapper.transform(mirrorTx); + // the mirror is the copper side, so copper is drawn solid and the component side faded + g2dWrapper.setViewedSide(PCBLayer._1, Boolean.TRUE.equals(board.getUndersideTransparency()), + component instanceof IBoard ? PCBLayer._1 : PCBLayer._2); + component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper); + } catch (Exception e) { + LOG.error("Error drawing mirrored " + component.getName(), e); + } finally { + g2dWrapper.setMirrorDirection(G2DWrapper.MirrorDirection.NONE); + g2dWrapper.setTransform(oldTx); + g2dWrapper.stopDrawingSide(); + g2dWrapper.resetViewedSide(); + g2dWrapper.setComposite(oldComposite); + } + } + + /** + * 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; + return null; } double minX = Math.min(boardRectangle.getMinX(), boardRectangle.getMaxX()); @@ -581,40 +626,9 @@ public class DrawingManager { break; } default: - return; - } - - G2DWrapper.MirrorDirection mirrorDirection; - switch (board.getUndersideDisplay()) { - case ABOVE: - case BELOW: - mirrorDirection = G2DWrapper.MirrorDirection.VERTICAL; - break; - case LEFT: - case RIGHT: - mirrorDirection = G2DWrapper.MirrorDirection.HORIZONTAL; - break; - default: - mirrorDirection = G2DWrapper.MirrorDirection.NONE; - break; - } - - AffineTransform oldTx = g2dWrapper.getTransform(); - Composite oldComposite = g2dWrapper.getComposite(); - g2dWrapper.setMirrorDirection(mirrorDirection); - try { - g2dWrapper.transform(mirrorTx); - if (Boolean.TRUE.equals(board.getUndersideTransparency())) { - g2dWrapper.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)(MIRROR_ALPHA.getValue().doubleValue() / 100d))); - } - component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper); - } catch (Exception e) { - LOG.error("Error drawing mirrored " + component.getName(), e); - } finally { - g2dWrapper.setMirrorDirection(G2DWrapper.MirrorDirection.NONE); - g2dWrapper.setTransform(oldTx); - g2dWrapper.setComposite(oldComposite); + return null; } + return mirrorTx; } private static void drawComponent(Project project, Set drawOptions, Double scaleFactor, 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 fa8e8e417..af5f59f85 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 @@ -17,6 +17,7 @@ */ package org.diylc.presenter; +import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; @@ -60,6 +61,7 @@ import java.util.Map.Entry; import org.apache.log4j.Logger; import org.diylc.common.DrawOption; import org.diylc.common.ObjectCache; +import org.diylc.common.PCBLayer; import org.diylc.common.ZoomableStroke; import org.diylc.core.IDrawingObserver; @@ -118,6 +120,17 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { private MirrorDirection mirrorDirection = MirrorDirection.NONE; + private PCBLayer viewedSide = PCBLayer._2; + // the side assumed for paint a component does not bracket with startDrawingSide + private PCBLayer undeclaredSide = PCBLayer._2; + private PCBLayer paintedSide = PCBLayer._2; + private boolean fadeFarSide; + // the composite the component asked for, held while far-side paint is drawn faded on top of it + private Composite unfadedComposite; + // boards switch sides once per strip or pad, so the last faded composite is reused + private Composite lastFadeInput; + private Composite lastFadeOutput; + private double zoom; private int zOrder; @@ -131,6 +144,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { super(); this.canvasGraphics = canvasGraphics; this.drawOptions = drawOptions; + this.fadeFarSide = drawOptions.contains(DrawOption.FADE_FAR_SIDE); String debugAreaPerformanceStr = System.getProperty(DEBUG_AREA_PERFORMANCE); debugAreaPerformance = "true".equalsIgnoreCase(debugAreaPerformanceStr); this.zoom = zoom; @@ -154,6 +168,31 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { return mirrorDirection; } + /** + * Sets the side of the board the viewer is looking at and whether paint on the other side is + * drawn faded. The main view looks at the top side; the underside mirror looks at the bottom. + * + * @param viewedSide + * @param fadeFarSide + * @param undeclaredSide the side assumed for paint that is not bracketed with + * {@link #startDrawingSide(PCBLayer)}; a board body is seen from both sides, so the mirror + * draws it as the viewed side + */ + public void setViewedSide(PCBLayer viewedSide, boolean fadeFarSide, PCBLayer undeclaredSide) { + this.viewedSide = viewedSide; + this.fadeFarSide = fadeFarSide; + 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. + */ + public void resetViewedSide() { + setViewedSide(PCBLayer._2, drawOptions.contains(DrawOption.FADE_FAR_SIDE), PCBLayer._2); + } + /** * Clears out the current area and caches canvas settings. * @@ -168,6 +207,8 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { this.debugOutline = new Area(); // trackingStacks.clear(); this.continuityMarker = null; + this.paintedSide = undeclaredSide; + updateFarSideFade(); this.originalStroke = canvasGraphics.getStroke(); this.originalColor = canvasGraphics.getColor(); this.originalTx = canvasGraphics.getTransform(); @@ -189,6 +230,8 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { public ComponentArea finishedDrawingComponent() { drawingComponent = false; recordingDrawnBounds = false; + paintedSide = undeclaredSide; + unfadedComposite = null; canvasGraphics.setStroke(originalStroke); canvasGraphics.setColor(originalColor); canvasGraphics.setTransform(originalTx); @@ -280,6 +323,54 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { continuityMarker = marker; } + @Override + public void startDrawingSide(PCBLayer side) { + this.paintedSide = side == null ? undeclaredSide : side; + updateFarSideFade(); + } + + @Override + public void startDrawingBothSides() { + this.paintedSide = viewedSide; + updateFarSideFade(); + } + + @Override + public void stopDrawingSide() { + this.paintedSide = undeclaredSide; + updateFarSideFade(); + } + + private void updateFarSideFade() { + boolean fade = fadeFarSide && !drawOptions.contains(DrawOption.OUTLINE_MODE) + && isBottom(paintedSide) != isBottom(viewedSide); + if (fade && unfadedComposite == null) { + unfadedComposite = canvasGraphics.getComposite(); + canvasGraphics.setComposite(fade(unfadedComposite)); + } else if (!fade && unfadedComposite != null) { + canvasGraphics.setComposite(unfadedComposite); + unfadedComposite = null; + } + } + + private static boolean isBottom(PCBLayer layer) { + return layer != PCBLayer._2; + } + + // the stronger of the two fades wins rather than multiplying, so that copper on a locked board, + // already drawn at the locked alpha, does not all but disappear + private Composite fade(Composite composite) { + if (composite != lastFadeInput || lastFadeOutput == null) { + float alpha = (float) (DrawingManager.FAR_SIDE_ALPHA.getValue().doubleValue() / 100d); + if (composite instanceof AlphaComposite ac && ac.getRule() == AlphaComposite.SRC_OVER) { + alpha = Math.min(alpha, ac.getAlpha()); + } + lastFadeInput = composite; + lastFadeOutput = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha); + } + return lastFadeOutput; + } + /** * Appends shape interior to the current component area. * @@ -669,7 +760,7 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { @Override public Composite getComposite() { - return canvasGraphics.getComposite(); + return unfadedComposite != null ? unfadedComposite : canvasGraphics.getComposite(); } @Override @@ -738,7 +829,13 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { @Override public void setComposite(Composite comp) { - if (!drawOptions.contains(DrawOption.OUTLINE_MODE)) { + if (drawOptions.contains(DrawOption.OUTLINE_MODE)) { + return; + } + if (unfadedComposite != null) { + unfadedComposite = comp; + canvasGraphics.setComposite(fade(comp)); + } else { canvasGraphics.setComposite(comp); } // FIXME: check this. 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 f2ce5fdea..67eaedb39 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 @@ -1,9 +1,12 @@ package org.diylc.presenter; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.geom.Area; @@ -20,6 +23,7 @@ import org.diylc.appframework.miscutils.InMemoryConfigurationManager; import org.diylc.appframework.simplemq.MessageDispatcher; import org.diylc.common.DrawOption; import org.diylc.common.EventType; +import org.diylc.common.PCBLayer; import org.diylc.core.BoardUndersideDisplay; import org.diylc.core.ComponentState; import org.diylc.core.IBoard; @@ -175,10 +179,10 @@ public class DrawingManagerTest { assertEquals(150.0, mirroredP.getX(), 0.001); assertEquals(670.0, mirroredP.getY(), 0.001); - // Check transparency + // the mirror fades through the wrapper, so the component still sees its own composite assertEquals(2, component.capturedAlphas.size()); assertEquals(1.0f, component.capturedAlphas.get(0), 0.01f); - assertEquals(0.5f, component.capturedAlphas.get(1), 0.01f); + assertEquals(1.0f, component.capturedAlphas.get(1), 0.01f); } @Test @@ -213,7 +217,6 @@ public class DrawingManagerTest { assertEquals(150.0, mirroredP.getX(), 0.001); assertEquals(10.0, mirroredP.getY(), 0.001); - // Transparency should not be 0.5 when undersideTransparency is false assertEquals(1.0f, component.capturedAlphas.get(1), 0.01f); } @@ -647,6 +650,177 @@ public class DrawingManagerTest { assertTrue(recorded.contains(50, 5)); } + @Test + public void testBottomSideFadedOnlyWithOption() { + Project project = new Project(); + project.getComponents().add(new SidePaintingComponent(new Rectangle2D.Double(150, 150, 10, 10), null)); + project.getComponents().add(new SidePaintingComponent(new Rectangle2D.Double(200, 200, 10, 10), PCBLayer._1)); + project.getComponents().add(new SidePaintingComponent(new Rectangle2D.Double(250, 250, 10, 10), PCBLayer._3)); + + BufferedImage faded = renderSides(project, EnumSet.of(DrawOption.ZOOM, DrawOption.FADE_FAR_SIDE)); + assertSolid(faded, 155, 155); + assertFaded(faded, 205, 205); + // inner layers count as the bottom side + assertFaded(faded, 255, 255); + + BufferedImage plain = renderSides(project, EnumSet.of(DrawOption.ZOOM)); + assertSolid(plain, 205, 205); + assertSolid(plain, 255, 255); + } + + @Test + public void testBothSidesNeverFaded() { + 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)); + project.getComponents().add(board); + project.getComponents().add(SidePaintingComponent.bothSides(new Rectangle2D.Double(150, 150, 10, 10))); + + BufferedImage img = renderSides(project, EnumSet.of(DrawOption.ZOOM, DrawOption.FADE_FAR_SIDE)); + assertSolid(img, 155, 155); + assertSolid(img, 155, 665); + } + + @Test + public void testLockedBottomSideNotFadedTwice() { + Project project = new Project(); + SidePaintingComponent bottom = new SidePaintingComponent(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(); + + // the stronger of the 50% locked alpha and the far-side alpha decides, not their product + assertFaded(img, 205, 205, Math.min(0.5f, farSideAlpha())); + } + + @Test + public void testMirrorShowsCopperSideSolid() { + 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 SidePaintingComponent(new Rectangle2D.Double(150, 150, 10, 10), null)); + project.getComponents().add(new SidePaintingComponent(new Rectangle2D.Double(200, 200, 10, 10), PCBLayer._1)); + + // reflection across y = 410 puts the top-side square 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); + + board.setUndersideTransparency(false); + img = renderSides(project, EnumSet.of(DrawOption.ZOOM, DrawOption.FADE_FAR_SIDE)); + assertSolid(img, 155, 665); + assertSolid(img, 205, 615); + } + + @Test + public void testFarSideFadeKeepsComponentComposite() { + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics2D canvas = img.createGraphics(); + G2DWrapper wrapper = new G2DWrapper(canvas, 1.0, EnumSet.of(DrawOption.FADE_FAR_SIDE)); + float farSideAlpha = farSideAlpha(); + + Composite half = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); + wrapper.setComposite(half); + wrapper.startDrawingSide(PCBLayer._1); + assertSame(half, wrapper.getComposite()); + // the stronger of the two fades wins rather than multiplying + assertEquals(Math.min(0.5f, farSideAlpha), ((AlphaComposite) canvas.getComposite()).getAlpha(), 0.001f); + + wrapper.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f)); + assertEquals(Math.min(0.25f, farSideAlpha), ((AlphaComposite) canvas.getComposite()).getAlpha(), 0.001f); + + wrapper.setComposite(half); + wrapper.stopDrawingSide(); + assertSame(half, canvas.getComposite()); + canvas.dispose(); + } + + @Test + public void testFarSideNotFadedInOutlineMode() { + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics2D canvas = img.createGraphics(); + Composite original = canvas.getComposite(); + G2DWrapper wrapper = new G2DWrapper(canvas, 1.0, + EnumSet.of(DrawOption.FADE_FAR_SIDE, DrawOption.OUTLINE_MODE)); + wrapper.startDrawingSide(PCBLayer._1); + assertSame(original, canvas.getComposite()); + wrapper.stopDrawingSide(); + canvas.dispose(); + } + + @Test + public void testFarSideFadeLeavesContinuityUnchanged() { + Area faded = drawBottomConductor(EnumSet.of(DrawOption.FADE_FAR_SIDE)); + Area plain = drawBottomConductor(EnumSet.noneOf(DrawOption.class)); + assertTrue(faded.equals(plain)); + assertEquals(new Rectangle2D.Double(10, 10, 30, 20), faded.getBounds2D()); + } + + private Area drawBottomConductor(Set drawOptions) { + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics2D canvas = img.createGraphics(); + G2DWrapper wrapper = new G2DWrapper(canvas, 1.0, drawOptions); + wrapper.startedDrawingComponent(0); + wrapper.startDrawingSide(PCBLayer._1); + wrapper.startTrackingContinuityArea(true); + wrapper.fill(new Rectangle2D.Double(10, 10, 30, 20)); + wrapper.stopTrackingContinuityArea(); + wrapper.stopDrawingSide(); + ComponentArea componentArea = wrapper.finishedDrawingComponent(); + canvas.dispose(); + Area area = new Area(); + for (Area a : componentArea.getContinuityPositiveAreas()) { + area.add(a); + } + return area; + } + + private BufferedImage renderSides(Project project, Set drawOptions) { + DrawingManager drawingManager = createDrawingManager(); + BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = img.createGraphics(); + drawingManager.drawProject(g2d, project, drawOptions, null, null, Collections.emptyList(), + Collections.emptySet(), Collections.emptySet(), null, null, false, Collections.emptySet(), + 1.0, 1.0, null); + g2d.dispose(); + return img; + } + + private static float farSideAlpha() { + return (float) (DrawingManager.FAR_SIDE_ALPHA.getValue().doubleValue() / 100d); + } + + private static int red(BufferedImage img, int x, int y) { + return new Color(img.getRGB(x, y)).getRed(); + } + + // the squares are painted black, so the red channel alone shows how much of the background is left + private static void assertSolid(BufferedImage img, int x, int y) { + assertEquals(0, red(img, x, y)); + } + + private static void assertFaded(BufferedImage img, int x, int y) { + assertFaded(img, x, y, farSideAlpha()); + } + + private static void assertFaded(BufferedImage img, int x, int y, float alpha) { + double background = red(img, 5, 5); + assertEquals(background * (1 - alpha), red(img, x, y), 3); + } + private void drawVisibleRect(DrawingManager drawingManager, Project project, Rectangle2D visibleRect) { BufferedImage img = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB); @@ -732,6 +906,50 @@ public class DrawingManagerTest { } } + // paints an opaque black square, declared on the given side or, when null, left undeclared + public static class SidePaintingComponent extends MockDIYComponent { + + private static final long serialVersionUID = 1L; + + private Rectangle2D shape; + private PCBLayer side; + private boolean bothSides; + + public SidePaintingComponent(Rectangle2D shape, PCBLayer side) { + this.shape = shape; + this.side = side; + } + + public static SidePaintingComponent bothSides(Rectangle2D shape) { + SidePaintingComponent component = new SidePaintingComponent(shape, null); + component.bothSides = true; + return component; + } + + @Override + public int getControlPointCount() { + return 1; + } + + @Override + public Point2D getControlPoint(int index) { + return new Point2D.Double(shape.getCenterX(), shape.getCenterY()); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, + Project project, IDrawingObserver drawingObserver) { + if (bothSides) { + drawingObserver.startDrawingBothSides(); + } else if (side != null) { + drawingObserver.startDrawingSide(side); + } + g2d.setColor(Color.black); + g2d.fill(shape); + drawingObserver.stopDrawingSide(); + } + } + // Simple delegating Graphics2D proxy for testing private static class Graphics2DProxy extends Graphics2D { private final Graphics2D delegate; diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/PerfBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/PerfBoard.java index 9751d432e..bb8ccd949 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/PerfBoard.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/PerfBoard.java @@ -26,6 +26,7 @@ import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.Point2D; +import org.diylc.common.PCBLayer; import org.diylc.components.transform.AbstractBoardTransformer; import org.diylc.core.ComponentState; import org.diylc.core.IDIYComponent; @@ -81,10 +82,12 @@ public class PerfBoard extends AbstractBoard implements ILayeredComponent { y += spacing; while (x < finalSecondPoint.getX() - spacing - diameter) { x += spacing; + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(padColor); g2d.fillOval((int)(x - diameter / 2), (int)(y - diameter / 2), diameter, diameter); g2d.setColor(padColor.darker()); g2d.drawOval((int)(x - diameter / 2), (int)(y - diameter / 2), diameter, diameter); + drawingObserver.stopDrawingSide(); g2d.setColor(Constants.CANVAS_COLOR); g2d.fillOval((int)(x - holeDiameter / 2), (int)(y - holeDiameter / 2), holeDiameter, holeDiameter); g2d.setColor(padColor.darker()); diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard.java index 4a0278654..9398ea69a 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard.java @@ -31,6 +31,7 @@ import java.awt.geom.Rectangle2D; import org.diylc.awt.StringUtils; import org.diylc.common.HorizontalAlignment; import org.diylc.common.ObjectCache; +import org.diylc.common.PCBLayer; import org.diylc.common.VerticalAlignment; import org.diylc.components.transform.ProtoBoardTransformer; import org.diylc.core.ComponentState; @@ -169,6 +170,7 @@ public class ProtoBoard extends AbstractProtoBoard { (int) (point.getX() + (1 + SEGMENTS_SUM[3] + 2.5) * spacing), (int) (point.getY() + (yOffset + y - 0.25d) * spacing + 1)); } + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(undersideTraceColor); // bottom row. @@ -182,6 +184,7 @@ public class ProtoBoard extends AbstractProtoBoard { // vertical ground line g2d.drawLine((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing), (int) (point.getY() + (yOffset - 1.5) * spacing), (int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing), (int) (point.getY() + (yOffset + ROW_COUNT + 0.5) * spacing)); + drawingObserver.stopDrawingSide(); g2d.setColor(traceColor); @@ -494,6 +497,7 @@ public class ProtoBoard extends AbstractProtoBoard { g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 0.5) * spacing) + 1, (int) (point.getY() + (yOffset + y - 0.5d) * spacing + 2), miniPadSize, miniPadSize); g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 0.5) * spacing) + miniPadSize + 3, (int) (point.getY() + (yOffset + y - 0.5d) * spacing + 2), miniPadSize, miniPadSize); + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(undersidePadColor); drawingObserver.startTrackingContinuityArea(true); @@ -507,15 +511,18 @@ public class ProtoBoard extends AbstractProtoBoard { g2d.fillRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing) + miniPadSize / 2 + 3, (int) (point.getY() + (yOffset + y + 0.5d) * spacing - 1 - miniPadSize), miniPadSize, miniPadSize); drawingObserver.stopTrackingContinuityArea(); + drawingObserver.stopDrawingSide(); g2d.setColor(undersidePadColor.darker()); g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing) + 3, (int) (point.getY() + (yOffset + y - 0.5d) * spacing + 2), miniPadSize, miniPadSize); g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing) + miniPadSize + 5, (int) (point.getY() + (yOffset + y - 0.5d) * spacing + 2), miniPadSize, miniPadSize); + drawingObserver.startDrawingSide(PCBLayer._1); g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing) - miniPadSize / 2, (int) (point.getY() + (yOffset + y + 0.5d) * spacing - 1 - miniPadSize), miniPadSize, miniPadSize); g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing) - miniPadSize * 3 / 2 - 2, (int) (point.getY() + (yOffset + y + 0.5d) * spacing - 1 - miniPadSize), miniPadSize, miniPadSize); g2d.drawRect((int) (point.getX() + (1 + SEGMENTS_SUM[3] + 1.5) * spacing) + miniPadSize / 2 + 3, (int) (point.getY() + (yOffset + y + 0.5d) * spacing - 1 - miniPadSize), miniPadSize, miniPadSize); + drawingObserver.stopDrawingSide(); } } diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard780.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard780.java index 613ce018a..8d140b0b7 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard780.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/ProtoBoard780.java @@ -28,6 +28,7 @@ import java.awt.geom.Rectangle2D; import org.diylc.awt.StringUtils; import org.diylc.common.HorizontalAlignment; import org.diylc.common.ObjectCache; +import org.diylc.common.PCBLayer; import org.diylc.common.VerticalAlignment; import org.diylc.components.transform.ProtoBoardTransformer; import org.diylc.core.ComponentState; @@ -151,14 +152,17 @@ public class ProtoBoard780 extends AbstractProtoBoard { g2d.setColor(traceColor); int padX = (int) Math.round(point.getX() + width / 2 - 7 * spacing + i * spacing); + drawingObserver.startDrawingSide(PCBLayer._1); drawingObserver.startTrackingContinuityArea(true); g2d.drawLine(padX, (int) Math.round(point.getY() + offsetY), padX, (int) Math.round(point.getY() + spacing + offsetY)); drawingObserver.stopTrackingContinuityArea(); + drawingObserver.stopDrawingSide(); for (int j = 0; j < 2; j++) { int padY = (int) Math.round(point.getY() + spacing * j + offsetY); + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(padColor); drawingObserver.startTrackingContinuityArea(true); @@ -171,6 +175,7 @@ public class ProtoBoard780 extends AbstractProtoBoard { g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); g2d.drawOval(padX - padSize / 2, padY - padSize / 2, padSize, padSize); + drawingObserver.stopDrawingSide(); g2d.setColor(HOLE_COLOR); g2d.fillOval(padX - holeSize / 2, padY - holeSize / 2, holeSize, holeSize); @@ -196,6 +201,7 @@ public class ProtoBoard780 extends AbstractProtoBoard { offsetY = spacing * 5.5; // draw 3-pad traces and vertical traces + drawingObserver.startDrawingSide(PCBLayer._1); drawingObserver.startTrackingContinuityArea(true); g2d.setColor(traceColor); g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(padSize - 2f)); @@ -239,6 +245,7 @@ public class ProtoBoard780 extends AbstractProtoBoard { } } drawingObserver.stopTrackingContinuityArea(); + drawingObserver.stopDrawingSide(); // draw main pads for (int i = 0; i < 25; i++) { @@ -254,6 +261,7 @@ public class ProtoBoard780 extends AbstractProtoBoard { for (int j = 0; j < 30; j++) { int padY = (int) Math.round(point.getY() + spacing * j + offsetY); + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(padColor); drawingObserver.startTrackingContinuityArea(true); @@ -266,6 +274,7 @@ public class ProtoBoard780 extends AbstractProtoBoard { g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); g2d.drawOval(padX - padSize / 2, padY - padSize / 2, padSize, padSize); + drawingObserver.stopDrawingSide(); g2d.setColor(HOLE_COLOR); g2d.fillOval(padX - holeSize / 2, padY - holeSize / 2, holeSize, holeSize); diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/TriPadBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/TriPadBoard.java index 7848ee10f..50cb81da0 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/TriPadBoard.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/TriPadBoard.java @@ -28,6 +28,7 @@ import java.awt.Shape; import java.awt.geom.Point2D; import org.diylc.common.OrientationHV; +import org.diylc.common.PCBLayer; import org.diylc.components.transform.VeroBoardTransformer; import org.diylc.core.ComponentState; import org.diylc.core.IDIYComponent; @@ -85,6 +86,7 @@ public class TriPadBoard extends AbstractVeroBoard implements ILayeredComponent spacesToDraw = (remainingSpace - stripSize) / spacing; } + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(stripColor); drawingObserver.startTrackingContinuityArea(true); g2d.fillRect((int)(x + spacing - stripSize / 2), (int)(y - stripSize / 2), (int)(spacing * (spacesToDraw - 1) + stripSize), @@ -94,6 +96,7 @@ public class TriPadBoard extends AbstractVeroBoard implements ILayeredComponent g2d.drawRect((int)(x + spacing - stripSize / 2), (int)(y - stripSize / 2), (int)(spacing * (spacesToDraw - 1) + stripSize), stripSize); + drawingObserver.stopDrawingSide(); x += spacing * spacesToDraw; } @@ -123,6 +126,7 @@ public class TriPadBoard extends AbstractVeroBoard implements ILayeredComponent spacesToDraw = (remainingSpace - stripSize) / spacing; } + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(stripColor); drawingObserver.startTrackingContinuityArea(true); g2d.fillRect((int)(x - stripSize / 2), (int)(y + spacing - stripSize / 2), stripSize, (int)(spacing * (spacesToDraw - 1) @@ -131,6 +135,7 @@ public class TriPadBoard extends AbstractVeroBoard implements ILayeredComponent g2d.setColor(stripColor.darker()); g2d.drawRect((int)(x - stripSize / 2), (int)(y + spacing - stripSize / 2), stripSize, (int)(spacing * (spacesToDraw - 1) + stripSize)); + drawingObserver.stopDrawingSide(); y += spacing * spacesToDraw; } diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/boards/VeroBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/boards/VeroBoard.java index 32ed2d016..46a0a59e7 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/boards/VeroBoard.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/boards/VeroBoard.java @@ -28,6 +28,7 @@ import java.awt.Shape; import java.awt.geom.Point2D; import org.diylc.common.OrientationHV; +import org.diylc.common.PCBLayer; import org.diylc.components.transform.VeroBoardTransformer; import org.diylc.core.ComponentState; import org.diylc.core.IDIYComponent; @@ -74,12 +75,14 @@ public class VeroBoard extends AbstractVeroBoard implements ILayeredComponent { while (y < finalSecondPoint.getY() - spacing - stripSize / 2) { x = firstPoint.getX(); y += spacing; + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(stripColor); drawingObserver.startTrackingContinuityArea(true); g2d.fillRect((int)(x + spacing / 2), (int)(y - stripSize / 2), (int) Math.round(finalSecondPoint.getX() - spacing - x), stripSize); drawingObserver.stopTrackingContinuityArea(); g2d.setColor(stripColor.darker()); g2d.drawRect((int)(x + spacing / 2), (int)(y - stripSize / 2), (int) Math.round(finalSecondPoint.getX() - spacing - x), stripSize); + drawingObserver.stopDrawingSide(); while (x < finalSecondPoint.getX() - spacing - holeSize) { x += spacing; g2d.setColor(Constants.CANVAS_COLOR); @@ -92,12 +95,14 @@ public class VeroBoard extends AbstractVeroBoard implements ILayeredComponent { while (x < finalSecondPoint.getX() - spacing) { x += spacing; y = firstPoint.getY(); + drawingObserver.startDrawingSide(PCBLayer._1); g2d.setColor(stripColor); drawingObserver.startTrackingContinuityArea(true); g2d.fillRect((int)(x - stripSize / 2), (int)(y + spacing / 2), stripSize, (int) Math.round(finalSecondPoint.getY() - spacing - y)); drawingObserver.stopTrackingContinuityArea(); g2d.setColor(stripColor.darker()); g2d.drawRect((int)(x - stripSize / 2), (int)(y + spacing / 2), stripSize, (int) Math.round(finalSecondPoint.getY() - spacing - y)); + drawingObserver.stopDrawingSide(); while (y < finalSecondPoint.getY() - spacing - holeSize) { y += spacing; g2d.setColor(Constants.CANVAS_COLOR); diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CopperTrace.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CopperTrace.java index f0ad9a36f..1fa771ee9 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CopperTrace.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CopperTrace.java @@ -34,7 +34,9 @@ import org.diylc.core.ComponentState; import org.diylc.core.CreationMethod; import org.diylc.core.IConnective; import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; import org.diylc.core.ILayeredComponent; +import org.diylc.core.Project; import org.diylc.core.VisibilityPolicy; import org.diylc.core.annotations.AiNote.Scope; import org.diylc.core.annotations.AiNote; @@ -72,6 +74,14 @@ public class CopperTrace extends AbstractLeadedComponent implements IConne this.leadColor = COLOR; } + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, + Project project, IDrawingObserver drawingObserver) { + drawingObserver.startDrawingSide(getLayer()); + super.draw(g2d, componentState, outlineMode, project, drawingObserver); + drawingObserver.stopDrawingSide(); + } + @Override public void drawIcon(Graphics2D g2d, int width, int height) { g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3)); diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CurvedTrace.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CurvedTrace.java index 36fed2e11..8ebcb54b2 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CurvedTrace.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CurvedTrace.java @@ -87,9 +87,11 @@ public class CurvedTrace extends AbstractCurvedComponent implements IConne componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR : color; g2d.setColor(curveColor); + drawingObserver.startDrawingSide(getLayer()); drawingObserver.startTrackingContinuityArea(true); g2d.draw(curve); drawingObserver.stopTrackingContinuityArea(); + drawingObserver.stopDrawingSide(); } @EditableProperty(name = "Width") diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/GroundFill.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/GroundFill.java index b0f6eb24d..36c5901f7 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/GroundFill.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/GroundFill.java @@ -83,11 +83,13 @@ public class GroundFill extends AbstractTransparentComponent xPoints[i] = (int)controlPoints[i].getX(); yPoints[i] = (int)controlPoints[i].getY(); } + drawingObserver.startDrawingSide(getLayer()); drawingObserver.startTrackingContinuityArea(true); Composite oldComposite = applyAlpha(g2d, componentState); g2d.fillPolygon(xPoints, yPoints, controlPoints.length); g2d.setComposite(oldComposite); drawingObserver.stopTrackingContinuityArea(); + drawingObserver.stopDrawingSide(); // Do not track any changes that follow because the whole board has been // tracked so far. drawingObserver.stopTracking(); diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/SolderPad.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/SolderPad.java index d1322efc8..f89c18aa0 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/SolderPad.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/SolderPad.java @@ -97,6 +97,7 @@ public class SolderPad extends AbstractTransparentComponent gerberDrawingObserver.startGerberOutput(org.diylc.core.gerber.GerberLayer.SolderMaskBot, "Material", false); } Composite oldComposite = applyAlpha(g2d, componentState); + drawingObserver.startDrawingSide(getLayer()); if (type == Type.ROUND) { g2d.fill(new Ellipse2D.Double(point.getX() - diameter / 2, point.getY() - diameter / 2, @@ -111,6 +112,7 @@ public class SolderPad extends AbstractTransparentComponent g2d.fill(new Rectangle2D.Double(point.getX() - diameter / 2, point.getY() - diameter / 2, diameter, diameter)); } + drawingObserver.stopDrawingSide(); drawingObserver.stopTracking(); drawingObserver.stopTrackingContinuityArea(); if (gerberDrawingObserver != null) @@ -121,9 +123,11 @@ public class SolderPad extends AbstractTransparentComponent GerberLayer holeLayer = HoleType.PTH.equals(getHoleType()) ? GerberLayer.DrillPlated : GerberLayer.DrillNonPlated; gerberDrawingObserver.startGerberOutput(holeLayer, GerberFunctions.COMPONENT_DRILL, false); } + drawingObserver.startDrawingBothSides(); g2d.setColor(Constants.CANVAS_COLOR); g2d.fill(new Ellipse2D.Double(point.getX() - holeDiameter / 2, point.getY() - holeDiameter / 2, holeDiameter, holeDiameter)); + drawingObserver.stopDrawingSide(); } g2d.setComposite(oldComposite); if (gerberDrawingObserver != null) diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/TraceCut.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/TraceCut.java index 62edaf0d0..b2131fa0f 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/TraceCut.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/TraceCut.java @@ -83,6 +83,8 @@ public class TraceCut extends AbstractTransparentComponent implements ILay return; } Composite oldComposite = applyAlpha(g2d, componentState); + // the cut is what tells the user a strip is broken, so it stays crisp from either side + drawingObserver.startDrawingBothSides(); g2d.setStroke(ObjectCache.getInstance().fetchZoomableStroke(1f)); int size = getClosestOdd((int) this.size.convertToPixels()); int cutWidth = getClosestOdd((int) CUT_WIDTH.convertToPixels()); @@ -109,6 +111,7 @@ public class TraceCut extends AbstractTransparentComponent implements ILay g2d.setColor(getBoardColor().darker()); g2d.drawOval((int) Math.round(point.getX() - holeSize / 2), (int) Math.round(point.getY() - holeSize / 2), holeSize, holeSize); } + drawingObserver.stopDrawingSide(); g2d.setComposite(oldComposite); } diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/misc/PCBText.java b/diylc/diylc-library/src/main/java/org/diylc/components/misc/PCBText.java index ec733d3c5..6c90abb0c 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/misc/PCBText.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/misc/PCBText.java @@ -156,7 +156,9 @@ public class PCBText extends AbstractComponent implements ILayeredComponen @Override public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, IDrawingObserver drawingObserver) { + drawingObserver.startDrawingSide(getLayer()); this.draw(g2d, componentState, outlineMode, project, drawingObserver, null); + drawingObserver.stopDrawingSide(); } @Override diff --git a/diylc/diylc-library/src/test/java/org/diylc/serialization/InnerLayerCompatibilityTest.java b/diylc/diylc-library/src/test/java/org/diylc/serialization/InnerLayerCompatibilityTest.java new file mode 100644 index 000000000..6ce60b874 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/serialization/InnerLayerCompatibilityTest.java @@ -0,0 +1,57 @@ +/* + + 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 . + +*/ +package org.diylc.serialization; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.xml.DomDriver; + +import org.diylc.common.PCBLayer; +import org.diylc.components.connectivity.CopperTrace; +import org.junit.Assert; +import org.junit.Test; + +/** + * The inner PCB layers are deprecated but must keep loading from files that use them, with their + * layer and continuity id unchanged, because XStream resolves enum constants by name. + */ +public class InnerLayerCompatibilityTest { + + private static XStream createXStream() { + XStream xStream = new XStream(new DomDriver("UTF-8")); + ProjectFileManager.configure(xStream); + return xStream; + } + + @Test + @SuppressWarnings("deprecation") + public void innerLayerTraceLoadsUnchanged() { + CopperTrace trace = new CopperTrace(); + trace.setLayer(PCBLayer._2); + String xml = createXStream().toXML(trace); + Assert.assertTrue(xml.contains("_2")); + + CopperTrace loaded = (CopperTrace) createXStream().fromXML(xml.replace("_2", "_5")); + Assert.assertEquals(PCBLayer._5, loaded.getLayer()); + Assert.assertEquals(5, loaded.getLayerId()); + Assert.assertTrue(createXStream().toXML(loaded).contains("_5")); + } +} diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/editor/EnumEditor.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/editor/EnumEditor.java index 0e6378893..d0b8e9cef 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/editor/EnumEditor.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/editor/EnumEditor.java @@ -25,6 +25,8 @@ import java.awt.Color; import java.awt.Component; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; +import java.util.ArrayList; +import java.util.List; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListCellRenderer; import javax.swing.JComboBox; @@ -44,7 +46,7 @@ public class EnumEditor extends JComboBox { public EnumEditor(final PropertyWrapper property) { this.property = property; - Object[] values = property.getType().getEnumConstants(); + Object[] values = getOfferedValues(property); if (property.isReadOnly()) setEnabled(false); setModel(new DefaultComboBoxModel(values)); @@ -76,4 +78,24 @@ public class EnumEditor extends JComboBox { setBackground(Constants.MULTI_VALUE_COLOR); } } + + // deprecated constants stay loadable from old files but are not offered, unless one is the + // current value, which must stay visible rather than be silently replaced + private static Object[] getOfferedValues(PropertyWrapper property) { + List values = new ArrayList(); + for (Object value : property.getType().getEnumConstants()) { + if (value.equals(property.getValue()) || !isDeprecated((Enum) value)) { + values.add(value); + } + } + return values.toArray(); + } + + private static boolean isDeprecated(Enum value) { + try { + return value.getDeclaringClass().getField(value.name()).isAnnotationPresent(Deprecated.class); + } catch (NoSuchFieldException e) { + return false; + } + } } diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/CanvasPanel.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/CanvasPanel.java index 4c567b053..bfa0072c5 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/CanvasPanel.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/canvas/CanvasPanel.java @@ -372,6 +372,9 @@ public class CanvasPanel extends JComponent implements Autoscroll { if (configManager.readBoolean(IPlugInPort.LOCKED_ALPHA, true)) { drawOptions.add(DrawOption.LOCKED_ALPHA); } + if (configManager.readBoolean(IPlugInPort.FADE_FAR_SIDE_KEY, true)) { + drawOptions.add(DrawOption.FADE_FAR_SIDE); + } return drawOptions; } diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java index 505ea5d0b..48bd39916 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/config/ConfigPlugin.java @@ -113,6 +113,9 @@ public class ConfigPlugin implements IPlugIn { swingUI.injectMenuAction( ActionFactory.getInstance().createConfigAction(plugInPort, "Extra Working Area", IPlugInPort.EXTRA_SPACE_KEY, true), CONFIG_MENU); + swingUI.injectMenuAction( + ActionFactory.getInstance().createConfigAction(plugInPort, "Fade Copper Under Board", + IPlugInPort.FADE_FAR_SIDE_KEY, true), CONFIG_MENU); // swingUI.injectMenuAction( // ActionFactory.getInstance().createConfigAction(plugInPort, "Hardware Acceleration", IPlugInPort.HARDWARE_ACCELERATION, false), // CONFIG_MENU); diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/file/ProjectDrawingProvider.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/file/ProjectDrawingProvider.java index 73534246d..045c2b15c 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/file/ProjectDrawingProvider.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/file/ProjectDrawingProvider.java @@ -69,6 +69,9 @@ public class ProjectDrawingProvider implements IDrawingProvider { if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.OUTLINE_KEY, false)) { drawOptions.add(DrawOption.OUTLINE_MODE); } + if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.FADE_FAR_SIDE_KEY, true)) { + drawOptions.add(DrawOption.FADE_FAR_SIDE); + } if (includeExtraSpace) { drawOptions.add(DrawOption.EXTRA_SPACE); } diff --git a/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt b/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt index 59b071231..c682215f3 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt @@ -732,3 +732,4 @@ Remove default model|Standardmodell entfernen Value Notation|Wertnotation Decimal (4.7K, 0.22uF, 6.3V)|Dezimal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|Kupfer unter der Platine abblenden diff --git a/diylc/diylc-swing/src/main/resources/lang/English.txt b/diylc/diylc-swing/src/main/resources/lang/English.txt index 8cca4be78..1be074325 100644 --- a/diylc/diylc-swing/src/main/resources/lang/English.txt +++ b/diylc/diylc-swing/src/main/resources/lang/English.txt @@ -725,3 +725,4 @@ Remove default model Value Notation Decimal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3) +Fade Copper Under Board diff --git a/diylc/diylc-swing/src/main/resources/lang/Español.txt b/diylc/diylc-swing/src/main/resources/lang/Español.txt index a863ae5ce..0a884489f 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Español.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Español.txt @@ -728,3 +728,4 @@ Remove default model|Quitar modelo predeterminado Value Notation|Notación de valores Decimal (4.7K, 0.22uF, 6.3V)|Decimal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|Atenuar cobre bajo la placa diff --git a/diylc/diylc-swing/src/main/resources/lang/Français.txt b/diylc/diylc-swing/src/main/resources/lang/Français.txt index 9640060be..fb375d62b 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Français.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Français.txt @@ -727,3 +727,4 @@ Remove default model|Retirer le modèle par défaut Value Notation|Notation des valeurs Decimal (4.7K, 0.22uF, 6.3V)|Décimale (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|Estomper le cuivre sous la carte diff --git a/diylc/diylc-swing/src/main/resources/lang/Italiano.txt b/diylc/diylc-swing/src/main/resources/lang/Italiano.txt index ef5673596..3a4a82e48 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Italiano.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Italiano.txt @@ -729,3 +729,4 @@ Remove default model|Rimuovi modello predefinito Value Notation|Notazione dei valori Decimal (4.7K, 0.22uF, 6.3V)|Decimale (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|Attenua il rame sotto la scheda diff --git a/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt b/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt index ab2b278fa..452135b6a 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt @@ -733,3 +733,4 @@ Remove default model|Standaardmodel verwijderen Value Notation|Waardenotatie Decimal (4.7K, 0.22uF, 6.3V)|Decimaal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|Koper onder de print vervagen diff --git a/diylc/diylc-swing/src/main/resources/lang/Português.txt b/diylc/diylc-swing/src/main/resources/lang/Português.txt index 83dc89779..4942b19cc 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Português.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Português.txt @@ -728,3 +728,4 @@ Remove default model|Remover modelo padrão Value Notation|Notação de valores Decimal (4.7K, 0.22uF, 6.3V)|Decimal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|Esmaecer cobre sob a placa diff --git a/diylc/diylc-swing/src/main/resources/lang/Srpski.txt b/diylc/diylc-swing/src/main/resources/lang/Srpski.txt index f6ba4f7f5..88e7ecd55 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Srpski.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Srpski.txt @@ -724,3 +724,4 @@ Remove default model|Ukloni podrazumevani model Value Notation|Zapis vrednosti Decimal (4.7K, 0.22uF, 6.3V)|Decimalni (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|Prigušen Bakar Ispod Ploče diff --git a/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt b/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt index 6698a488a..8ee3f86dc 100644 --- a/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt +++ b/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt @@ -726,3 +726,4 @@ Remove default model|डिफ़ॉल्ट मॉडल हटाएँ Value Notation|मान संकेतन Decimal (4.7K, 0.22uF, 6.3V)|दशमलव (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|बोर्ड के नीचे तांबा धुंधला करें diff --git a/diylc/diylc-swing/src/main/resources/lang/简体中文.txt b/diylc/diylc-swing/src/main/resources/lang/简体中文.txt index 1c66fc96c..17e29a8fd 100644 --- a/diylc/diylc-swing/src/main/resources/lang/简体中文.txt +++ b/diylc/diylc-swing/src/main/resources/lang/简体中文.txt @@ -726,3 +726,4 @@ Remove default model|取消默认型号 Value Notation|数值表示法 Decimal (4.7K, 0.22uF, 6.3V)|小数 (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) +Fade Copper Under Board|淡化板下铜箔 diff --git a/diylc/docs/plans/bottom-side-components.md b/diylc/docs/plans/bottom-side-components.md index dc7d2d80f..deef5be37 100644 --- a/diylc/docs/plans/bottom-side-components.md +++ b/diylc/docs/plans/bottom-side-components.md @@ -7,9 +7,13 @@ Status: not started. ## 1. Current state -- Traces, pads, PCB text and ground fills carry a `PCBLayer` (`Bottom`, `Top`, `Inner 1..6`). - It feeds `PCBLayer.toGerberCopperLayer()`, `PCBLayerFilter`, the trace-mask print and - continuity (`ILayeredComponent.getLayerId()`). +- Traces, pads, PCB text and ground fills carry a `PCBLayer`: `Bottom` and `Top`, plus the + deprecated `Inner 1..6`, kept only so that old files load and hidden in the editor. It feeds + `PCBLayer.toGerberCopperLayer()`, `PCBLayerFilter`, the trace-mask print and continuity + (`ILayeredComponent.getLayerId()`). +- `IDrawingObserver.startDrawingSide(PCBLayer)` / `stopDrawingSide()` already exist + (`copper-side-view.md`): paint on the side facing away from the viewer is drawn faded, in both + the main view and the Underside Display mirror. - Every other Gerber-aware component implements `IGerberComponentSimple`, whose `getGerberLayer()` defaults to `SilkscreenTop`. About 35 classes in `diylc-library` rely on that default (resistors, film and radial capacitors, diodes, LEDs, transistor packages, DIL/SIL ICs, @@ -25,45 +29,44 @@ Status: not started. | Decision | Rationale | | --- | --- | -| A new two-value `BoardSide` (`Top`, `Bottom`), not a reuse of `PCBLayer` | Parts mount on the outer faces only. Inner layers would be invalid choices in the editor. | +| The side is a `PCBLayer` (`_2` Top, `_1` Bottom). No separate side enum | DIYLC only models two-sided boards. The inner layers are deprecated and hidden in the editor (`copper-side-view.md`), so only the two valid choices are offered. | +| A part's side goes through its own `IBoardSideComponent`, never through `ILayeredComponent` | `getLayerId()` drives continuity, and a through-hole part's leads must connect to copper on both sides. Reporting a resistor as layer `_1` there would cut it off from top copper. | | The side is the physical fact. Z-order stays paint order and is not tied to it | Boards are drawn opaque, so painting bottom parts beneath the board would hide them. Bottom-layer traces already follow this rule. | | The canvas shows the side visually and does not rely on z-order for it | That is what removes the ambiguity of a "bottom" part drawn above the board. | | Flipping a part is an action that sets the side, mirrors the part and sends it back | A part seen from above through the board is a mirror image. Changing the side without mirroring gives a footprint that is wrong for fabrication. | | The Gerber layer is swapped centrally, not inside each component | Components keep emitting their `*Top` layer. One place maps it to the other side. | -| Default `Top`, with `null` meaning `Top` | Fields added to existing classes deserialize as `null` in old files. | +| Default `_2` (Top), with `null` meaning Top | Fields added to existing classes deserialize as `null` in old files. | ## 3. Design ### diylc-core -- **`org.diylc.common.BoardSide`**: enum `Top`, `Bottom`, with a `toString()` label like `PCBLayer`. -- **`org.diylc.core.IBoardSideComponent`**: `BoardSide getBoardSide();` with Javadoc and - `@author`, in the same shape as `ILayeredComponent`. Core knows the side only through this - interface. +- **`org.diylc.core.IBoardSideComponent`**: `PCBLayer getBoardSide();` with Javadoc and + `@author`, in the same shape as `ILayeredComponent` but deliberately separate from it (see + Decisions). It returns `_1` or `_2` only. Core knows the side only through this interface. - **`GerberLayer.flip()`**: maps `CopperTop`/`CopperBot`, `SilkscreenTop`/`SilkscreenBot` and `SolderMaskTop`/`SolderMaskBot` to each other. `Outline` and the drill layers map to themselves. - **Gerber dispatch**: in the simple-component branch, use - `getGerberLayer().flip()` when the component is an `IBoardSideComponent` on `Bottom`. Merge + `getGerberLayer().flip()` when the component is an `IBoardSideComponent` on `_1`. Merge `GerberExporter.drawComponent` and `GerberPathRenderer.drawComponent` into one method first, so the rule is written once. -- **Canvas rendering**: `DrawingManager.drawProject` draws bottom-side components with a - composite that sets them apart. Locked components already use a 0.5 alpha composite - (`lockedComposite`), so the bottom-side treatment has to look different from that, for example - a lower alpha or a tint. Pick one and check it against a locked bottom-side part. +- **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. ### diylc-library - The ~35 classes that use the `SilkscreenTop` default implement `IBoardSideComponent`. Each adds a - nullable `BoardSide boardSide` field and a getter/setter pair, with - `@EditableProperty(name = "Board Side")` on the getter. The getter returns `Top` when the field - is `null`. This duplicates the field across classes in the same way the `PCBLayer` field is + nullable `PCBLayer boardSide` field and a getter/setter pair, with + `@EditableProperty(name = "Board Side")` on the getter. The getter returns `_2` (Top) when the + field is `null`. This duplicates the field across classes in the same way the `PCBLayer` field is duplicated today. Where several of these classes share an abstract base (`AbstractRadialComponent`, `AbstractFilmCapacitor`, `AbstractTransistorPackage`, ...), put it on the base. - `CopperTrace`, `CurvedTrace`, `GroundFill`, `SolderPad`, `PCBText` and `DrillHole` do **not** implement it. Their `PCBLayer` already says where they are. -- Before allowing `Bottom` on a class, check that its transformer can mirror it - (`canMirror`). A class that cannot be mirrored must not offer `Bottom`, or the footprint would be +- Before allowing Bottom on a class, check that its transformer can mirror it + (`canMirror`). A class that cannot be mirrored must not offer Bottom, or the footprint would be wrong. ### diylc-swing @@ -81,6 +84,35 @@ Status: not started. silkscreen outline. Split it into top and bottom pages, filtered by side, the same way `TraceMaskDrawingProvider` gives one page per `PCBLayer`. +### Editing through the Underside Display mirror + +The mirror is a second drawing of the same components, not a separate space: every component +lives in main-view coordinates. Editing in the mirror therefore means mapping the mouse back +through the mirror's reflection before the existing logic runs. A reflection is its own inverse, +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 + 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. + Control-point drags (stretching a trace) go through the same mapping. +- **Stage 3: placing in the mirror.** A component instantiated with its first point in a mirror + region is created at the reflected point. If it is an `IBoardSideComponent`, it is created with + side `_1` (Bottom) and mirrored, as Flip to Other Side would do. A `PCBLayer` component is created on + `_1`. Anything else is placed at the reflected point unchanged. + ## 4. Open questions - **Bottom silkscreen text.** Component labels drawn through `IGerberComponentSimple` have to read @@ -94,24 +126,40 @@ Status: not started. - **Plain property or action only.** A plain editable property lets a user change the side without mirroring. Either keep both (the property for already-mirrored parts) or show the side as read-only and change it only through the action. +- **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. +- **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. ## 5. Verification - Unit tests in `diylc-core` for `GerberLayer.flip()` and for the merged dispatch method, using a stub `IBoardSideComponent` (top, bottom and `null`). -- Load an old `.diy` with PCB parts and confirm every component reports `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. - 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. +- 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. - Run the regression suite, since rendering and serialization both change. ## 6. Next steps 1. Merge the two `drawComponent` copies in `diylc-core` and add `GerberLayer.flip()` with tests. -2. Add `BoardSide` and `IBoardSideComponent`, and swap the Gerber layer in the dispatch. +2. Add `IBoardSideComponent` and swap the Gerber layer in the dispatch. 3. Add the field to the library components, putting it on shared bases where they exist. Settle the silkscreen-text question while doing so. -4. Add the bottom-side composite in `DrawingManager`. +4. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. 5. Add the Flip to Other Side action. 6. Split the silkscreen print-out by side. -7. Run the regression suite. +7. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`. +8. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-6 and can ship + early. +9. Mirror stage 2: dragging through the mirror. +10. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. +11. Run the regression suite. diff --git a/diylc/docs/plans/copper-side-view.md b/diylc/docs/plans/copper-side-view.md new file mode 100644 index 000000000..361c60030 --- /dev/null +++ b/diylc/docs/plans/copper-side-view.md @@ -0,0 +1,101 @@ +# Copper-Side View + +Make it obvious which side of a board the canvas shows. The main view is the component side, and +copper on the bottom of the board is drawn faded, as if seen through it. The Underside Display +mirror is a true view from below: copper solid, components faded. + +Status: implemented in the working tree; the open questions below and the regression run remain. + +## 1. Background + +- Z-order is paint order (`BOARD` < `TRACE` < `COMPONENT`), so boards, strips and traces were all + painted with copper on top and users mistook the main view for the copper side. +- Strips and pads are not separate components: the boards paint them inside their own `draw()`. +- `CopperTrace`, `CurvedTrace`, `SolderPad`, `GroundFill` and `PCBText` carry a `PCBLayer` that + defaults to `_1` (Bottom), so the model already placed them on the bottom. + +## 2. Decisions + +| Decision | Rationale | +| --- | --- | +| One rule for every view: near side solid, far side at reduced opacity, as if seen through the board | The same rule covers the main view, the mirror and, later, bottom-side components (`bottom-side-components.md`). | +| Paint order and z-order stay unchanged. Opacity alone shows the side | Boards are opaque, so painting copper beneath the board would hide it. Editing, hit-testing and selection are untouched. | +| Rendering-only change. No serialized field is added, renamed or retyped | Old files open with the new look and nothing to migrate. | +| `PCBLayer` is the side; no separate side enum. Inner layers are deprecated, not removed | DIYLC models two-sided boards only. XStream resolves enums by name, so `_3`-`_8` must stay for old files to load. | +| Far-side paint uses plain reduced opacity (`DrawingManager.FAR_SIDE_ALPHA`, 55%), not raised when selected | Simple, and reads clearly as "behind". 45% was too faint on the default phenolic strips. | +| The fade takes the stronger of `FAR_SIDE_ALPHA` and the component's own alpha; it does not multiply them | Multiplying with the 50% locked alpha left a locked board's copper at about 22%. The cost is that a bottom part already more translucent than `FAR_SIDE_ALPHA` does not fade further. | +| A global Config toggle, **Fade Copper Under Board**, on by default and not saved in projects | Users who prefer the old look can turn it off without a per-board property. | +| The canvas and the layout export, PDF and print follow the toggle. Trace mask, silkscreen and Gerber are unaffected | The layout outputs show what the canvas shows; the fabrication outputs build their own `DrawOption` sets. | +| The canvas draws no side labels. Users label the sides themselves | A caption drawn outside the mirror can sit on top of wiring, and only the user knows where a label fits. | +| `undersideTransparency` no longer fades the whole mirror: `true` (default) fades the far side, `false` draws the mirror fully solid | The near side of a true underside view has to be solid. The field keeps its name, so old files load. | +| Paint a component does not declare is component side, except a board's own paint in the mirror, which is drawn as near side | Components need no changes. A board's substrate is seen from both sides, so it must not fade in the mirror. | +| Holes and trace cuts are visible from both sides (`startDrawingBothSides()`) and never fade; a board's own holes are already solid as board paint | A faded hole reads as copper, and a faded cut hides the break the user is looking for. | +| `Jumper`, `Eyelet`, `Turret`, `Top`-layer copper and `EyeletBoard` eyelets are component side | Jumpers run over the board; eyelets and turrets are visible from the top. | +| `ProtoBoard780` declares its traces and pads bottom side, like `PerfBoard`. `ProtoBoard` declares only the traces and pads its code paints in the underside colours. `EurorackStripboard` declares nothing | The 780-hole boards carry copper on the bottom only. `ProtoBoard` is plated through with some traces on top. Which side `EurorackStripboard`'s traces are on is not sourced. | +| `Breadboard`, `BlankBoard`, `MarshallPerfBoard`, `TagStrip`, `TerminalStrip` and the maker boards declare nothing | They have no bottom copper to show. | + +## 3. Design as built + +### diylc-core + +- `PCBLayer`: `_3`-`_8` are `@Deprecated`, in their original order so that `getId()` is stable. + They are not remapped and keep their own continuity layer. For fading they count as Bottom. +- `IDrawingObserver.startDrawingSide(PCBLayer)`, `startDrawingBothSides()` and + `stopDrawingSide()` are default no-op methods, so `GerberG2DWrapper` and test observers need no + change. +- `G2DWrapper` keeps the viewed side, the side assumed for undeclared paint, and the side being + painted. While the painted side differs from the viewed side and fading is on, it draws at the + smaller of `FAR_SIDE_ALPHA` and the component's own alpha. Both-sides paint is treated as the + viewed side. `getComposite()` returns the + component's own composite and `setComposite()` re-applies the fade, so components that save and + restore their composite keep working. Outline mode never fades. Only the composite changes, so + outline and continuity areas are identical with and without fading. +- `DrawOption.FADE_FAR_SIDE` turns fading on in the main view. `IPlugInPort.FADE_FAR_SIDE_KEY` + (`fadeFarSide`) is the config key. +- `DrawingManager.drawMirroredComponent` views the mirror from `_1`, with fading controlled by + `undersideTransparency`. The reflection is built by `createMirrorTransform`. + +### diylc-library + +- `VeroBoard` and `TriPadBoard` declare each strip `_1`; `PerfBoard` declares each pad `_1`. The + holes stay undeclared. +- `ProtoBoard780` declares its traces and pads `_1`; holes, mounting holes and labels stay + undeclared. +- `ProtoBoard` declares its underside traces and underside mini pads `_1`. +- `CopperTrace` and `PCBText` declare their whole canvas `draw` on `getLayer()`. `CurvedTrace` and + `GroundFill` declare their copper fill. `SolderPad` declares the pad on its layer and its + drilled hole as both sides. +- `TraceCut` draws its cut and hole as both sides. + +### diylc-swing + +- The **Fade Copper Under Board** checkbox is in the Config menu after Extra Working Area. + `CanvasPanel.getDrawOptions` and `ProjectDrawingProvider.draw` add `FADE_FAR_SIDE` from it. +- `EnumEditor` hides `@Deprecated` constants unless one is the current value. +- The language key **Fade Copper Under Board** is in every file in `lang/`. + +## 4. Open questions + +- **`Dot` on stripboard.** Users draw solder joints with `Dot`, which is generic and undeclared, + so dots are solid in the main view and faded in the mirror. +- **`EurorackStripboard`.** Unchanged until the side of its traces is sourced. +- **AI catalog.** `ComponentCatalogGenerator` still lists the deprecated inner layers as possible + values. + +## 5. Verification + +- `DrawingManagerTest` checks rendered pixels: undeclared paint is solid; bottom and inner-layer + paint is faded only with `FADE_FAR_SIDE`; both-sides paint is solid in both views; a locked + bottom part is faded once, not twice. In the mirror, copper is solid and the component side + is faded, and `undersideTransparency=false` draws everything solid. The composite is restored, + setting a composite mid-side re-fades it, outline mode never fades, and continuity areas do not + change. +- `InnerLayerCompatibilityTest` loads a trace saved on `_5` and checks that its layer and layer id + are kept. +- Still to do: run the regression suite and regenerate reference PNGs for the expected + faded-copper changes. Netlists must not change. + +## 6. Next steps + +1. Settle the open questions above. +2. Run the regression suite.