From 040e959d8d5b3b81373975d10549cc90a70604de Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Sat, 15 Aug 2026 19:41:13 +0200 Subject: [PATCH] Improved the accuracy of board underside rendering; some of the components were not rendered in the right place or at all --- .../org/diylc/presenter/DrawingManager.java | 144 +++-- .../java/org/diylc/presenter/G2DWrapper.java | 144 ++++- .../diylc-core/src/main/resources/update.xml | 4 + .../diylc/presenter/DrawingManagerTest.java | 532 ++++++++++++++++++ 4 files changed, 767 insertions(+), 57 deletions(-) create mode 100644 diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java 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 076f9bd1..9297ee4a 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 @@ -50,7 +50,6 @@ import org.diylc.appframework.miscutils.IConfigurationManager; import org.diylc.appframework.simplemq.MessageDispatcher; import org.diylc.common.*; -import org.diylc.components.AbstractTransparentComponent; import org.diylc.core.*; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; @@ -192,19 +191,27 @@ public class DrawingManager { Map>> boardMap = project.getComponents().stream() .filter(IBoard.class::isInstance) .map(IBoard.class::cast) - .filter(b -> b.getUndersideDisplay() != BoardUndersideDisplay.NONE) + .filter(b -> b.getUndersideDisplay() != null && b.getUndersideDisplay() != BoardUndersideDisplay.NONE) .collect(Collectors.toMap(x -> x, x -> new ArrayList>())); Map, Set> componentBoardMap = new HashMap, Set>(); for (IBoard board : boardMap.keySet()) { Rectangle2D boardRect = board.getBoardRectangle(); + if (boardRect == null) { + continue; + } + double minX = Math.min(boardRect.getMinX(), boardRect.getMaxX()); + double maxX = Math.max(boardRect.getMinX(), boardRect.getMaxX()); + double minY = Math.min(boardRect.getMinY(), boardRect.getMaxY()); + double maxY = Math.max(boardRect.getMinY(), boardRect.getMaxY()); + Rectangle2D normalizedBoardRect = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); for (IDIYComponent c : project.getComponents()) { if (c == board) { continue; } boolean include = false; for (int i = 0; i < c.getControlPointCount(); i++) { - if (boardRect.contains(c.getControlPoint(i))) { + if (normalizedBoardRect.contains(c.getControlPoint(i))) { include = true; break; } @@ -337,26 +344,17 @@ public class DrawingManager { try { // Mirror components that belong to boards that need to be mirrored if (componentBoardMap.containsKey(component)) { - @SuppressWarnings("unchecked") - ComponentType componentType = ComponentProcessor.getInstance() - .extractComponentTypeFrom((Class>) component.getClass()); - if (componentType.getTransformer() != null) { - Set boards = componentBoardMap.get(component); - for (IBoard board : boards) { - drawMirroredComponent(project, board, component, componentType, g2dWrapper, state, - outlineMode); - } + Set boards = componentBoardMap.get(component); + for (IBoard board : boards) { + drawMirroredComponent(project, board, component, g2dWrapper, state, outlineMode); } } // if a component itself is a board that needs to be mirrored, do it - if (IBoard.class.isInstance(component) && componentBoardMap.values().stream() - .anyMatch(boards -> boards.contains(component))) { - ComponentType componentType = ComponentProcessor.getInstance() - .extractComponentTypeFrom((Class>) component.getClass()); - if (componentType.getTransformer() != null) { - drawMirroredComponent(project, (IBoard) component, component, componentType, - g2dWrapper, state, outlineMode); + if (component instanceof IBoard) { + IBoard board = (IBoard) component; + if (board.getUndersideDisplay() != null && board.getUndersideDisplay() != BoardUndersideDisplay.NONE) { + drawMirroredComponent(project, board, component, g2dWrapper, state, outlineMode); } } } finally { @@ -526,39 +524,85 @@ public class DrawingManager { } private static void drawMirroredComponent(Project project, IBoard board, IDIYComponent component, - ComponentType componentType, G2DWrapper g2dWrapper, ComponentState state, boolean outlineMode) { - double offset = board.getUndersideOffset().convertToPixels(); + G2DWrapper g2dWrapper, ComponentState state, boolean outlineMode) { + if (board.getUndersideDisplay() == null || board.getUndersideDisplay() == BoardUndersideDisplay.NONE) { + return; + } + double offset = board.getUndersideOffset() == null ? 0 : board.getUndersideOffset().convertToPixels(); Rectangle2D boardRectangle = board.getBoardRectangle(); - try { - IDIYComponent clonedComponent = component.clone(); - int direction = 0; - Point2D pivotPoint = new Point2D.Double(); - switch (board.getUndersideDisplay()) { - case ABOVE: - direction = IComponentTransformer.VERTICAL; - pivotPoint.setLocation(boardRectangle.getMinX(), boardRectangle.getMinY() - offset / 2); - break; - case BELOW: - direction = IComponentTransformer.VERTICAL; - pivotPoint.setLocation(boardRectangle.getMinX(), boardRectangle.getMaxY() + offset / 2); - break; - case LEFT: - direction = IComponentTransformer.HORIZONTAL; - pivotPoint.setLocation(boardRectangle.getMinX() - offset / 2, boardRectangle.getMinY()); - break; - case RIGHT: - direction = IComponentTransformer.HORIZONTAL; - pivotPoint.setLocation(boardRectangle.getMaxX() + offset / 2, boardRectangle.getMinY()); - break; - } - componentType.getTransformer().mirror(clonedComponent, pivotPoint, direction); - if (board.getUndersideTransparency() && AbstractTransparentComponent.class.isInstance(clonedComponent)) { - AbstractTransparentComponent transparentComponent = (AbstractTransparentComponent) clonedComponent; - transparentComponent.setAlpha(MIRROR_ALPHA); + if (boardRectangle == null) { + return; + } + + double minX = Math.min(boardRectangle.getMinX(), boardRectangle.getMaxX()); + double maxX = Math.max(boardRectangle.getMinX(), boardRectangle.getMaxX()); + double minY = Math.min(boardRectangle.getMinY(), boardRectangle.getMaxY()); + double maxY = Math.max(boardRectangle.getMinY(), boardRectangle.getMaxY()); + + AffineTransform mirrorTx = new AffineTransform(); + switch (board.getUndersideDisplay()) { + case ABOVE: { + double pivotY = minY - offset / 2; + mirrorTx.translate(0, pivotY); + mirrorTx.scale(1.0, -1.0); + mirrorTx.translate(0, -pivotY); + break; } - clonedComponent.draw(g2dWrapper, state, outlineMode, project, g2dWrapper); - } catch (CloneNotSupportedException e) { - throw new RuntimeException(e); + case BELOW: { + double pivotY = maxY + offset / 2; + mirrorTx.translate(0, pivotY); + mirrorTx.scale(1.0, -1.0); + mirrorTx.translate(0, -pivotY); + break; + } + case LEFT: { + double pivotX = minX - offset / 2; + mirrorTx.translate(pivotX, 0); + mirrorTx.scale(-1.0, 1.0); + mirrorTx.translate(-pivotX, 0); + break; + } + case RIGHT: { + double pivotX = maxX + offset / 2; + mirrorTx.translate(pivotX, 0); + mirrorTx.scale(-1.0, 1.0); + mirrorTx.translate(-pivotX, 0); + 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, MIRROR_ALPHA.getValue() / 100f)); + } + 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); } } 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 d69813e0..9bb1c3b1 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 @@ -35,6 +35,7 @@ import java.awt.Shape; import java.awt.Stroke; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; +import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Area; @@ -106,6 +107,12 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { private String continuityMarker; private Shape lastShape; + public enum MirrorDirection { + NONE, HORIZONTAL, VERTICAL + } + + private MirrorDirection mirrorDirection = MirrorDirection.NONE; + private double zoom; private int zOrder; @@ -133,6 +140,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { return canvasGraphics; } + public void setMirrorDirection(MirrorDirection mirrorDirection) { + this.mirrorDirection = mirrorDirection != null ? mirrorDirection : MirrorDirection.NONE; + } + + public MirrorDirection getMirrorDirection() { + return mirrorDirection; + } + /** * Clears out the current area and caches canvas settings. * @@ -332,7 +347,35 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { @Override public void drawGlyphVector(GlyphVector g, float x, float y) { - canvasGraphics.drawGlyphVector(g, x, y); + if (g == null) { + return; + } + if (mirrorDirection != MirrorDirection.NONE) { + Rectangle2D bounds = g.getVisualBounds(); + double width = bounds.getWidth(); + double height = bounds.getHeight(); + AffineTransform oldTx = canvasGraphics.getTransform(); + AffineTransform flipTx = new AffineTransform(); + if (mirrorDirection == MirrorDirection.HORIZONTAL) { + double cx = x + bounds.getX() + width / 2.0; + flipTx.translate(cx, 0); + flipTx.scale(-1.0, 1.0); + flipTx.translate(-cx, 0); + } else if (mirrorDirection == MirrorDirection.VERTICAL) { + double cy = y + bounds.getY() + height / 2.0; + flipTx.translate(0, cy); + flipTx.scale(1.0, -1.0); + flipTx.translate(0, -cy); + } + canvasGraphics.transform(flipTx); + try { + canvasGraphics.drawGlyphVector(g, x, y); + } finally { + canvasGraphics.setTransform(oldTx); + } + } else { + canvasGraphics.drawGlyphVector(g, x, y); + } } @Override @@ -361,7 +404,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { @Override public void drawString(String str, int x, int y) { - canvasGraphics.drawString(str, x, y); + if (str == null) { + return; + } + if (mirrorDirection != MirrorDirection.NONE) { + drawMirroredString(str, x, y); + } else { + canvasGraphics.drawString(str, x, y); + } if (drawingComponent && trackingAllowed) { FontMetrics fontMetrics = canvasGraphics.getFontMetrics(); Rectangle2D rect = fontMetrics.getStringBounds(str, canvasGraphics); @@ -376,7 +426,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { @Override public void drawString(String str, float x, float y) { - canvasGraphics.drawString(str, x, y); + if (str == null) { + return; + } + if (mirrorDirection != MirrorDirection.NONE) { + drawMirroredString(str, x, y); + } else { + canvasGraphics.drawString(str, x, y); + } if (drawingComponent && trackingAllowed) { FontMetrics fontMetrics = canvasGraphics.getFontMetrics(); Rectangle2D rect = fontMetrics.getStringBounds(str, canvasGraphics); @@ -386,14 +443,87 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver { @Override public void drawString(AttributedCharacterIterator iterator, int x, int y) { - canvasGraphics.drawString(iterator, x, y); - // FIXME: process map + if (iterator == null) { + return; + } + if (mirrorDirection != MirrorDirection.NONE) { + drawMirroredString(iterator, x, y); + } else { + canvasGraphics.drawString(iterator, x, y); + } } @Override public void drawString(AttributedCharacterIterator iterator, float x, float y) { - canvasGraphics.drawString(iterator, x, y); - // FIXME: process map + if (iterator == null) { + return; + } + if (mirrorDirection != MirrorDirection.NONE) { + drawMirroredString(iterator, x, y); + } else { + canvasGraphics.drawString(iterator, x, y); + } + } + + private void drawMirroredString(String str, float x, float y) { + FontMetrics fontMetrics = canvasGraphics.getFontMetrics(); + Rectangle2D rect = fontMetrics.getStringBounds(str, canvasGraphics); + double width = rect.getWidth(); + double ascent = fontMetrics.getAscent(); + double descent = fontMetrics.getDescent(); + + AffineTransform oldTx = canvasGraphics.getTransform(); + AffineTransform flipTx = new AffineTransform(); + + if (mirrorDirection == MirrorDirection.HORIZONTAL) { + double cx = x + width / 2.0; + flipTx.translate(cx, 0); + flipTx.scale(-1.0, 1.0); + flipTx.translate(-cx, 0); + } else if (mirrorDirection == MirrorDirection.VERTICAL) { + double cy = y - (ascent - descent) / 2.0; + flipTx.translate(0, cy); + flipTx.scale(1.0, -1.0); + flipTx.translate(0, -cy); + } + + canvasGraphics.transform(flipTx); + try { + canvasGraphics.drawString(str, x, y); + } finally { + canvasGraphics.setTransform(oldTx); + } + } + + private void drawMirroredString(AttributedCharacterIterator iterator, float x, float y) { + FontRenderContext frc = canvasGraphics.getFontRenderContext(); + TextLayout layout = new TextLayout(iterator, frc); + Rectangle2D bounds = layout.getBounds(); + double width = bounds.getWidth(); + double ascent = layout.getAscent(); + double descent = layout.getDescent(); + + AffineTransform oldTx = canvasGraphics.getTransform(); + AffineTransform flipTx = new AffineTransform(); + + if (mirrorDirection == MirrorDirection.HORIZONTAL) { + double cx = x + width / 2.0; + flipTx.translate(cx, 0); + flipTx.scale(-1.0, 1.0); + flipTx.translate(-cx, 0); + } else if (mirrorDirection == MirrorDirection.VERTICAL) { + double cy = y - (ascent - descent) / 2.0; + flipTx.translate(0, cy); + flipTx.scale(1.0, -1.0); + flipTx.translate(0, -cy); + } + + canvasGraphics.transform(flipTx); + try { + canvasGraphics.drawString(iterator, x, y); + } finally { + canvasGraphics.setTransform(oldTx); + } } @Override diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index ea2e6264..47c04dfd 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -401,6 +401,10 @@ IMPROVEMENT Significantly improved AI Assistant's knowledge base on DIYLC usage by ingesting most of the material from the eBook + + IMPROVEMENT + Improved the accuracy of board underside rendering; some of the components were not rendered in the right place or at all + IMPROVEMENT Show a progress dialog while waiting for a long-running AI Analysis to complete 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 new file mode 100644 index 00000000..5d072529 --- /dev/null +++ b/diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java @@ -0,0 +1,532 @@ +package org.diylc.presenter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; + +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.core.BoardUndersideDisplay; +import org.diylc.core.ComponentState; +import org.diylc.core.IBoard; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.testcomponents.MockDIYComponent; +import org.junit.Test; + +public class DrawingManagerTest { + + @ComponentDescriptor(name = "MockBoard", author = "test", category = "Boards", instanceNamePrefix = "B", + description = "Mock board for testing", zOrder = IDIYComponent.BOARD) + public static class MockBoard extends MockDIYComponent implements IBoard { + + private static final long serialVersionUID = 1L; + + private Rectangle2D boardRectangle = new Rectangle2D.Double(100, 100, 200, 300); + private BoardUndersideDisplay undersideDisplay = BoardUndersideDisplay.BELOW; + private Size undersideOffset = new Size(20.0, SizeUnit.px); + private Boolean undersideTransparency = Boolean.TRUE; + + public List capturedTransforms = new ArrayList<>(); + public List capturedAlphas = new ArrayList<>(); + + @Override + public Rectangle2D getBoardRectangle() { + return boardRectangle; + } + + public void setBoardRectangle(Rectangle2D boardRectangle) { + this.boardRectangle = boardRectangle; + } + + @Override + public BoardUndersideDisplay getUndersideDisplay() { + return undersideDisplay; + } + + public void setUndersideDisplay(BoardUndersideDisplay undersideDisplay) { + this.undersideDisplay = undersideDisplay; + } + + @Override + public Size getUndersideOffset() { + return undersideOffset; + } + + public void setUndersideOffset(Size undersideOffset) { + this.undersideOffset = undersideOffset; + } + + @Override + public Boolean getUndersideTransparency() { + return undersideTransparency; + } + + public void setUndersideTransparency(Boolean undersideTransparency) { + this.undersideTransparency = undersideTransparency; + } + + @Override + public boolean shouldExportToGerber() { + return false; + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + capturedTransforms.add(new AffineTransform(g2d.getTransform())); + if (g2d.getComposite() instanceof AlphaComposite) { + capturedAlphas.add(((AlphaComposite) g2d.getComposite()).getAlpha()); + } else { + capturedAlphas.add(1.0f); + } + } + } + + public static class TransformTrackingComponent extends MockDIYComponent { + + private static final long serialVersionUID = 1L; + + public List capturedTransforms = new ArrayList<>(); + public List capturedAlphas = new ArrayList<>(); + private Point2D controlPoint = new Point2D.Double(150, 150); + + @Override + public int getControlPointCount() { + return 1; + } + + @Override + public Point2D getControlPoint(int index) { + return controlPoint; + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + capturedTransforms.add(new AffineTransform(g2d.getTransform())); + if (g2d.getComposite() instanceof AlphaComposite) { + capturedAlphas.add(((AlphaComposite) g2d.getComposite()).getAlpha()); + } else { + capturedAlphas.add(1.0f); + } + } + } + + private DrawingManager createDrawingManager() { + return new DrawingManager(new MessageDispatcher(false), InMemoryConfigurationManager.getInstance()); + } + + @Test + public void testMirrorBelow() { + 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); + + TransformTrackingComponent component = new TransformTrackingComponent(); + project.getComponents().add(board); + project.getComponents().add(component); + + 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), null, null, + Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null); + + g2d.dispose(); + + // The component should be drawn twice: once normally and once mirrored + assertEquals(2, component.capturedTransforms.size()); + + // Original drawing: identity / no reflection + AffineTransform normalTx = component.capturedTransforms.get(0); + Point2D p = new Point2D.Double(150, 150); + Point2D normalP = normalTx.transform(p, null); + assertEquals(150.0, normalP.getX(), 0.001); + assertEquals(150.0, normalP.getY(), 0.001); + + // Mirrored drawing: BELOW reflection across Y = maxY + offset/2 = 400 + 10 = 410 + // Expected y' = 2 * 410 - 150 = 820 - 150 = 670 + AffineTransform mirrorTx = component.capturedTransforms.get(1); + Point2D mirroredP = mirrorTx.transform(p, null); + assertEquals(150.0, mirroredP.getX(), 0.001); + assertEquals(670.0, mirroredP.getY(), 0.001); + + // Check transparency + assertEquals(2, component.capturedAlphas.size()); + assertEquals(1.0f, component.capturedAlphas.get(0), 0.01f); + assertEquals(0.5f, component.capturedAlphas.get(1), 0.01f); + } + + @Test + public void testMirrorAbove() { + Project project = new Project(); + MockBoard board = new MockBoard(); + board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300)); + board.setUndersideDisplay(BoardUndersideDisplay.ABOVE); + board.setUndersideOffset(new Size(40.0, SizeUnit.px)); + board.setUndersideTransparency(false); + + TransformTrackingComponent component = new TransformTrackingComponent(); + project.getComponents().add(board); + project.getComponents().add(component); + + 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), null, null, + Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null); + + g2d.dispose(); + + assertEquals(2, component.capturedTransforms.size()); + + // Mirrored drawing: ABOVE reflection across Y = minY - offset/2 = 100 - 20 = 80 + // Expected y' = 2 * 80 - 150 = 160 - 150 = 10 + AffineTransform mirrorTx = component.capturedTransforms.get(1); + Point2D mirroredP = mirrorTx.transform(new Point2D.Double(150, 150), null); + 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); + } + + @Test + public void testMirrorLeft() { + Project project = new Project(); + MockBoard board = new MockBoard(); + board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300)); + board.setUndersideDisplay(BoardUndersideDisplay.LEFT); + board.setUndersideOffset(new Size(20.0, SizeUnit.px)); + + TransformTrackingComponent component = new TransformTrackingComponent(); + project.getComponents().add(board); + project.getComponents().add(component); + + 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), null, null, + Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null); + + g2d.dispose(); + + assertEquals(2, component.capturedTransforms.size()); + + // Mirrored drawing: LEFT reflection across X = minX - offset/2 = 100 - 10 = 90 + // Expected x' = 2 * 90 - 150 = 180 - 150 = 30 + AffineTransform mirrorTx = component.capturedTransforms.get(1); + Point2D mirroredP = mirrorTx.transform(new Point2D.Double(150, 150), null); + assertEquals(30.0, mirroredP.getX(), 0.001); + assertEquals(150.0, mirroredP.getY(), 0.001); + } + + @Test + public void testMirrorRight() { + Project project = new Project(); + MockBoard board = new MockBoard(); + board.setBoardRectangle(new Rectangle2D.Double(100, 100, 200, 300)); + board.setUndersideDisplay(BoardUndersideDisplay.RIGHT); + board.setUndersideOffset(new Size(20.0, SizeUnit.px)); + + TransformTrackingComponent component = new TransformTrackingComponent(); + project.getComponents().add(board); + project.getComponents().add(component); + + 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), null, null, + Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null); + + g2d.dispose(); + + assertEquals(2, component.capturedTransforms.size()); + + // Mirrored drawing: RIGHT reflection across X = maxX + offset/2 = 300 + 10 = 310 + // Expected x' = 2 * 310 - 150 = 620 - 150 = 470 + AffineTransform mirrorTx = component.capturedTransforms.get(1); + Point2D mirroredP = mirrorTx.transform(new Point2D.Double(150, 150), null); + assertEquals(470.0, mirroredP.getX(), 0.001); + assertEquals(150.0, mirroredP.getY(), 0.001); + } + + @Test + public void testUndersideDisplayNone() { + Project project = new Project(); + MockBoard board = new MockBoard(); + board.setUndersideDisplay(BoardUndersideDisplay.NONE); + + TransformTrackingComponent component = new TransformTrackingComponent(); + project.getComponents().add(board); + project.getComponents().add(component); + + 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), null, null, + Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, null, false, 1.0, 1.0, null); + + g2d.dispose(); + + // No mirrored drawing + assertEquals(1, component.capturedTransforms.size()); + assertEquals(1, board.capturedTransforms.size()); + } + + @Test + public void testTextUnmirroringHorizontal() { + BufferedImage img = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB); + final List capturedDuringDrawString = new ArrayList<>(); + Graphics2D canvasG2d = new Graphics2DProxy(img.createGraphics()) { + @Override + public void drawString(String str, int x, int y) { + capturedDuringDrawString.add(new AffineTransform(getTransform())); + super.drawString(str, x, y); + } + + @Override + public void drawString(String str, float x, float y) { + capturedDuringDrawString.add(new AffineTransform(getTransform())); + super.drawString(str, x, y); + } + }; + + G2DWrapper wrapper = new G2DWrapper(canvasG2d, 1.0, EnumSet.noneOf(DrawOption.class)); + + // Simulate horizontal mirror mode + wrapper.setMirrorDirection(G2DWrapper.MirrorDirection.HORIZONTAL); + AffineTransform mirrorTx = new AffineTransform(); + mirrorTx.translate(300, 0); + mirrorTx.scale(-1.0, 1.0); + mirrorTx.translate(-300, 0); + wrapper.transform(mirrorTx); + + wrapper.drawString("10K Log", 200, 200); + + assertEquals(1, capturedDuringDrawString.size()); + AffineTransform textTx = capturedDuringDrawString.get(0); + + // The determinant should be positive (un-mirrored / right-reading text) + assertTrue("Text transform determinant should be positive (not mirrored)", textTx.getDeterminant() > 0); + + // After drawing, wrapper transform should still be the mirrorTx + assertEquals(-1.0, wrapper.getTransform().getScaleX(), 0.001); + } + + @Test + public void testTextUnmirroringVertical() { + BufferedImage img = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB); + final List capturedDuringDrawString = new ArrayList<>(); + Graphics2D canvasG2d = new Graphics2DProxy(img.createGraphics()) { + @Override + public void drawString(String str, int x, int y) { + capturedDuringDrawString.add(new AffineTransform(getTransform())); + super.drawString(str, x, y); + } + + @Override + public void drawString(String str, float x, float y) { + capturedDuringDrawString.add(new AffineTransform(getTransform())); + super.drawString(str, x, y); + } + }; + + G2DWrapper wrapper = new G2DWrapper(canvasG2d, 1.0, EnumSet.noneOf(DrawOption.class)); + + // Simulate vertical mirror mode + wrapper.setMirrorDirection(G2DWrapper.MirrorDirection.VERTICAL); + AffineTransform mirrorTx = new AffineTransform(); + mirrorTx.translate(0, 300); + mirrorTx.scale(1.0, -1.0); + mirrorTx.translate(0, -300); + wrapper.transform(mirrorTx); + + wrapper.drawString("47uF", 200, 200); + + assertEquals(1, capturedDuringDrawString.size()); + AffineTransform textTx = capturedDuringDrawString.get(0); + + // The determinant should be positive (un-mirrored / right-reading text) + assertTrue("Text transform determinant should be positive (not mirrored)", textTx.getDeterminant() > 0); + + // After drawing, wrapper transform should still be the mirrorTx + assertEquals(-1.0, wrapper.getTransform().getScaleY(), 0.001); + } + + // Simple delegating Graphics2D proxy for testing + private static class Graphics2DProxy extends Graphics2D { + private final Graphics2D delegate; + + public Graphics2DProxy(Graphics2D delegate) { + this.delegate = delegate; + } + + @Override + public void draw(java.awt.Shape s) { delegate.draw(s); } + @Override + public boolean drawImage(java.awt.Image img, AffineTransform xform, java.awt.image.ImageObserver obs) { return delegate.drawImage(img, xform, obs); } + @Override + public void drawImage(BufferedImage img, java.awt.image.BufferedImageOp op, int x, int y) { delegate.drawImage(img, op, x, y); } + @Override + public void drawRenderedImage(java.awt.image.RenderedImage img, AffineTransform xform) { delegate.drawRenderedImage(img, xform); } + @Override + public void drawRenderableImage(java.awt.image.renderable.RenderableImage img, AffineTransform xform) { delegate.drawRenderableImage(img, xform); } + @Override + public void drawString(String str, int x, int y) { delegate.drawString(str, x, y); } + @Override + public void drawString(String str, float x, float y) { delegate.drawString(str, x, y); } + @Override + public void drawString(java.text.AttributedCharacterIterator iterator, int x, int y) { delegate.drawString(iterator, x, y); } + @Override + public void drawString(java.text.AttributedCharacterIterator iterator, float x, float y) { delegate.drawString(iterator, x, y); } + @Override + public void drawGlyphVector(java.awt.font.GlyphVector g, float x, float y) { delegate.drawGlyphVector(g, x, y); } + @Override + public void fill(java.awt.Shape s) { delegate.fill(s); } + @Override + public boolean hit(java.awt.Rectangle rect, java.awt.Shape s, boolean onStroke) { return delegate.hit(rect, s, onStroke); } + @Override + public java.awt.GraphicsConfiguration getDeviceConfiguration() { return delegate.getDeviceConfiguration(); } + @Override + public void setComposite(java.awt.Composite comp) { delegate.setComposite(comp); } + @Override + public void setPaint(java.awt.Paint paint) { delegate.setPaint(paint); } + @Override + public void setStroke(java.awt.Stroke s) { delegate.setStroke(s); } + @Override + public void setRenderingHint(java.awt.RenderingHints.Key hintKey, Object hintValue) { delegate.setRenderingHint(hintKey, hintValue); } + @Override + public Object getRenderingHint(java.awt.RenderingHints.Key hintKey) { return delegate.getRenderingHint(hintKey); } + @Override + public void setRenderingHints(java.util.Map hints) { delegate.setRenderingHints(hints); } + @Override + public void addRenderingHints(java.util.Map hints) { delegate.addRenderingHints(hints); } + @Override + public java.awt.RenderingHints getRenderingHints() { return delegate.getRenderingHints(); } + @Override + public void translate(int x, int y) { delegate.translate(x, y); } + @Override + public void translate(double tx, double ty) { delegate.translate(tx, ty); } + @Override + public void rotate(double theta) { delegate.rotate(theta); } + @Override + public void rotate(double theta, double x, double y) { delegate.rotate(theta, x, y); } + @Override + public void scale(double sx, double sy) { delegate.scale(sx, sy); } + @Override + public void shear(double shx, double shy) { delegate.shear(shx, shy); } + @Override + public void transform(AffineTransform Tx) { delegate.transform(Tx); } + @Override + public void setTransform(AffineTransform Tx) { delegate.setTransform(Tx); } + @Override + public AffineTransform getTransform() { return delegate.getTransform(); } + @Override + public java.awt.Paint getPaint() { return delegate.getPaint(); } + @Override + public java.awt.Composite getComposite() { return delegate.getComposite(); } + @Override + public void setBackground(java.awt.Color color) { delegate.setBackground(color); } + @Override + public java.awt.Color getBackground() { return delegate.getBackground(); } + @Override + public java.awt.Stroke getStroke() { return delegate.getStroke(); } + @Override + public void clip(java.awt.Shape s) { delegate.clip(s); } + @Override + public java.awt.font.FontRenderContext getFontRenderContext() { return delegate.getFontRenderContext(); } + @Override + public java.awt.Graphics create() { return delegate.create(); } + @Override + public java.awt.Color getColor() { return delegate.getColor(); } + @Override + public void setColor(java.awt.Color c) { delegate.setColor(c); } + @Override + public void setPaintMode() { delegate.setPaintMode(); } + @Override + public void setXORMode(java.awt.Color c1) { delegate.setXORMode(c1); } + @Override + public java.awt.Font getFont() { return delegate.getFont(); } + @Override + public void setFont(java.awt.Font font) { delegate.setFont(font); } + @Override + public java.awt.FontMetrics getFontMetrics(java.awt.Font f) { return delegate.getFontMetrics(f); } + @Override + public java.awt.Rectangle getClipBounds() { return delegate.getClipBounds(); } + @Override + public void clipRect(int x, int y, int width, int height) { delegate.clipRect(x, y, width, height); } + @Override + public void setClip(int x, int y, int width, int height) { delegate.setClip(x, y, width, height); } + @Override + public java.awt.Shape getClip() { return delegate.getClip(); } + @Override + public void setClip(java.awt.Shape clip) { delegate.setClip(clip); } + @Override + public void copyArea(int x, int y, int width, int height, int dx, int dy) { delegate.copyArea(x, y, width, height, dx, dy); } + @Override + public void drawLine(int x1, int y1, int x2, int y2) { delegate.drawLine(x1, y1, x2, y2); } + @Override + public void fillRect(int x, int y, int width, int height) { delegate.fillRect(x, y, width, height); } + @Override + public void clearRect(int x, int y, int width, int height) { delegate.clearRect(x, y, width, height); } + @Override + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { delegate.drawRoundRect(x, y, width, height, arcWidth, arcHeight); } + @Override + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { delegate.fillRoundRect(x, y, width, height, arcWidth, arcHeight); } + @Override + public void drawOval(int x, int y, int width, int height) { delegate.drawOval(x, y, width, height); } + @Override + public void fillOval(int x, int y, int width, int height) { delegate.fillOval(x, y, width, height); } + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { delegate.drawArc(x, y, width, height, startAngle, arcAngle); } + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { delegate.fillArc(x, y, width, height, startAngle, arcAngle); } + @Override + public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { delegate.drawPolyline(xPoints, yPoints, nPoints); } + @Override + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { delegate.drawPolygon(xPoints, yPoints, nPoints); } + @Override + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { delegate.fillPolygon(xPoints, yPoints, nPoints); } + @Override + public boolean drawImage(java.awt.Image img, int x, int y, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, x, y, observer); } + @Override + public boolean drawImage(java.awt.Image img, int x, int y, int width, int height, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, x, y, width, height, observer); } + @Override + public boolean drawImage(java.awt.Image img, int x, int y, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, x, y, bgcolor, observer); } + @Override + public boolean drawImage(java.awt.Image img, int x, int y, int width, int height, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, x, y, width, height, bgcolor, observer); } + @Override + public boolean drawImage(java.awt.Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); } + @Override + public boolean drawImage(java.awt.Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return delegate.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); } + @Override + public void dispose() { delegate.dispose(); } + } +} +