diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/IBoardSideComponent.java b/diylc/diylc-core/src/main/java/org/diylc/core/IBoardSideComponent.java new file mode 100644 index 000000000..305a0a5ee --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/core/IBoardSideComponent.java @@ -0,0 +1,42 @@ +/* + + 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.core; + +import org.diylc.common.PCBLayer; + +/** + * Interface for components mounted on one side of a board, such as a resistor or an IC. The side + * says where the part sits physically; it is separate from {@link ILayeredComponent}, which drives + * continuity, because the leads of a through-hole part connect to copper on both sides whichever + * side the part is mounted on. Components whose copper already has a layer (traces, pads, ground + * fills) do not implement it. + * + * @author Branislav Stojkovic + */ +public interface IBoardSideComponent { + + /** + * @return {@link PCBLayer#_2} for the top of the board or {@link PCBLayer#_1} for the bottom, + * never an inner layer or null + */ + PCBLayer getBoardSide(); +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java index a19ddffa0..decba7804 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/gerber/GerberExporter.java @@ -16,8 +16,10 @@ import org.apache.log4j.Logger; import com.bancika.gerberwriter.DataLayer; +import org.diylc.common.PCBLayer; import org.diylc.core.ComponentState; import org.diylc.core.IBoard; +import org.diylc.core.IBoardSideComponent; import org.diylc.core.IDIYComponent; import org.diylc.core.IView; import org.diylc.core.Project; @@ -158,8 +160,14 @@ public class GerberExporter { gerberComponentCustom.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d, g2d); } else if (gerberComponent instanceof IGerberComponentSimple) { IGerberComponentSimple gerberComponentSimple = (IGerberComponentSimple) gerberComponent; - g2d.startGerberOutput(gerberComponentSimple.getGerberLayer(), - gerberComponentSimple.getGerberFunction(), gerberComponentSimple.isGerberNegative()); + GerberLayer layer = gerberComponentSimple.getGerberLayer(); + // components emit their top-side layer; a part mounted underneath goes to the bottom one + if (c instanceof IBoardSideComponent + && ((IBoardSideComponent) c).getBoardSide() == PCBLayer._1) { + layer = layer.flip(); + } + g2d.startGerberOutput(layer, gerberComponentSimple.getGerberFunction(), + gerberComponentSimple.isGerberNegative()); c.draw(g2d, ComponentState.NORMAL, outlineMode, currentProject, g2d); g2d.stopGerberOutput(); } diff --git a/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java b/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java index e76abe62f..71afb77d6 100644 --- a/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java +++ b/diylc/diylc-core/src/test/java/org/diylc/core/gerber/GerberExporterTest.java @@ -29,7 +29,9 @@ import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.EnumSet; +import org.diylc.common.PCBLayer; import org.diylc.core.ComponentState; +import org.diylc.core.IBoardSideComponent; import org.diylc.core.IDrawingObserver; import org.diylc.core.Project; import org.diylc.testcomponents.MockDIYComponent; @@ -51,6 +53,27 @@ public class GerberExporterTest { assertEquals(EnumSet.of(GerberLayer.CopperBot), EnumSet.copyOf(g2d.getLayerMap().keySet())); } + @Test + public void testTopSideComponentKeepsItsLayer() { + GerberG2DWrapper g2d = draw(new SidedGerberComponent(PCBLayer._2)); + + assertEquals(EnumSet.of(GerberLayer.SilkscreenTop), EnumSet.copyOf(g2d.getLayerMap().keySet())); + } + + @Test + public void testBottomSideComponentGoesToTheOtherSide() { + GerberG2DWrapper g2d = draw(new SidedGerberComponent(PCBLayer._1)); + + assertEquals(EnumSet.of(GerberLayer.SilkscreenBot), EnumSet.copyOf(g2d.getLayerMap().keySet())); + } + + @Test + public void testComponentWithoutSideStaysOnTop() { + GerberG2DWrapper g2d = draw(new SidedGerberComponent(null)); + + assertEquals(EnumSet.of(GerberLayer.SilkscreenTop), EnumSet.copyOf(g2d.getLayerMap().keySet())); + } + private static GerberG2DWrapper draw(SimpleGerberComponent component) { BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = img.createGraphics(); @@ -85,4 +108,22 @@ public class GerberExporterTest { g2d.fill(new Rectangle2D.Double(10, 10, 20, 20)); } } + + public static class SidedGerberComponent extends SimpleGerberComponent + implements IBoardSideComponent { + + private static final long serialVersionUID = 1L; + + private PCBLayer side; + + public SidedGerberComponent(PCBLayer side) { + super(null); + this.side = side; + } + + @Override + public PCBLayer getBoardSide() { + return side; + } + } } diff --git a/diylc/docs/plans/bottom-side-components.md b/diylc/docs/plans/bottom-side-components.md index 907a6c7db..0b51e5fec 100644 --- a/diylc/docs/plans/bottom-side-components.md +++ b/diylc/docs/plans/bottom-side-components.md @@ -3,8 +3,9 @@ Let a component be mounted on the bottom of a PCB, so that its silkscreen goes to the bottom Gerber layers and the canvas shows it as a part on the far side of the board. -Status: the Gerber dispatch is a single method and `GerberLayer.flip()` exists, with tests; the -next steps below remain. +Status: the core Gerber side is done, with tests: `IBoardSideComponent` exists and the export +sends a bottom-side part to the bottom layers. No component implements it yet; the next steps below +remain. ## 1. Current state @@ -20,10 +21,12 @@ next steps below remain. default (resistors, film and radial capacitors, diodes, LEDs, transistor packages, DIL/SIL ICs, SMD passives, switches, jacks, relays, transformers, tube sockets, ...). None of them emit copper. Their pads are separate `SolderPad` components, which already have a layer. -- One method decides the layer for a simple component: `GerberExporter.drawComponent`, which - calls `g2d.startGerberOutput(getGerberLayer(), ...)`. It is package-private so that - `GerberExporterTest` can call it. `GerberLayer.flip()` maps a layer to the other side of the - board. +- One method decides the layer for a simple component: `GerberExporter.drawComponent`. It is + package-private so that `GerberExporterTest` can call it. It uses `getGerberLayer()`, flipped + with `GerberLayer.flip()` when the component is an `IBoardSideComponent` on `_1` (Bottom). +- **`org.diylc.core.IBoardSideComponent`**: `PCBLayer getBoardSide()`, returning `_1` or `_2` + only. It is deliberately separate from `ILayeredComponent` (see Decisions). Core knows a part's + side only through this interface. - Z-order is paint order only. A trace on the bottom layer already paints above the board (`zOrder = TRACE`), so the codebase already treats the physical side as separate from z-order. @@ -43,11 +46,6 @@ next steps below remain. ### diylc-core -- **`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. -- **Gerber dispatch**: in the simple-component branch of `GerberExporter.drawComponent`, use - `getGerberLayer().flip()` when the component is an `IBoardSideComponent` on `_1`. - **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. @@ -133,8 +131,8 @@ order. ## 5. Verification -- `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components; - extend it with a stub `IBoardSideComponent` (top, bottom and `null`). +- `GerberLayerTest` covers `flip()`. `GerberExporterTest` covers the dispatch for simple components, + including a stub `IBoardSideComponent` on top, bottom and `null`. - 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 @@ -148,15 +146,14 @@ order. ## 6. Next steps -1. Add `IBoardSideComponent` and swap the Gerber layer in the dispatch. -2. Add the field to the library components, putting it on shared bases where they exist. Settle +1. Add the field to the library components, putting it on shared bases where they exist. Settle the silkscreen-text question while doing so. -3. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. -4. Add the Flip to Other Side action. -5. Split the silkscreen print-out by side. -6. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`. -7. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-5 and can ship +2. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`. +3. Add the Flip to Other Side action. +4. Split the silkscreen print-out by side. +5. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`. +6. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-4 and can ship early. -8. Mirror stage 2: dragging through the mirror. -9. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. -10. Run the regression suite. +7. Mirror stage 2: dragging through the mirror. +8. Mirror stage 3: placing in the mirror, after Flip to Other Side exists. +9. Run the regression suite.