This commit is contained in:
Bane Stojkovic
2026-11-10 22:50:16 +01:00
parent 9f6124f719
commit f73574641c
35 changed files with 729 additions and 67 deletions
@@ -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;
}
@@ -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";
@@ -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;
@@ -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() {}
}
@@ -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<DrawOption> drawOptions, Double scaleFactor,
@@ -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.
@@ -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<DrawOption> 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<DrawOption> 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;
@@ -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());
@@ -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();
}
}
@@ -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);
@@ -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;
}
@@ -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);
@@ -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<Void> 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));
@@ -87,9 +87,11 @@ public class CurvedTrace extends AbstractCurvedComponent<Void> 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")
@@ -83,11 +83,13 @@ public class GroundFill extends AbstractTransparentComponent<Void>
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();
@@ -97,6 +97,7 @@ public class SolderPad extends AbstractTransparentComponent<Void>
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<Void>
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<Void>
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)
@@ -83,6 +83,8 @@ public class TraceCut extends AbstractTransparentComponent<Void> 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<Void> 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);
}
@@ -156,7 +156,9 @@ public class PCBText extends AbstractComponent<Void> 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
@@ -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 <http://www.gnu.org/licenses/>.
*/
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("<layer>_2</layer>"));
CopperTrace loaded = (CopperTrace) createXStream().fromXML(xml.replace("<layer>_2</layer>", "<layer>_5</layer>"));
Assert.assertEquals(PCBLayer._5, loaded.getLayer());
Assert.assertEquals(5, loaded.getLayerId());
Assert.assertTrue(createXStream().toXML(loaded).contains("<layer>_5</layer>"));
}
}
@@ -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<Object> {
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<Object>(values));
@@ -76,4 +78,24 @@ public class EnumEditor extends JComboBox<Object> {
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<Object> values = new ArrayList<Object>();
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;
}
}
}
@@ -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;
}
@@ -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);
@@ -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);
}
@@ -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
@@ -725,3 +725,4 @@ Remove default model
Value Notation
Decimal (4.7K, 0.22uF, 6.3V)
RKM (4K7, 0u22, 6V3)
Fade Copper Under Board
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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|बोर्ड के नीचे तांबा धुंधला करें
@@ -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|淡化板下铜箔
+71 -23
View File
@@ -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.
+101
View File
@@ -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.