Merge branch 'master' into copper-improvements

# Conflicts:
#	diylc/diylc-core/src/test/java/org/diylc/presenter/DrawingManagerTest.java
This commit is contained in:
Bane Stojkovic
2026-11-11 10:26:39 +01:00
7 changed files with 182 additions and 35 deletions
@@ -312,7 +312,8 @@ public class ChatbotService {
return null;
}
DrawingManager drawingManager = plugInPort.getDrawingManager();
List<ContinuityArea> continuityAreas = drawingManager.getContinuityAreas();
List<ContinuityArea> continuityAreas =
drawingManager.getContinuityAreas(plugInPort.getCurrentProject());
return AiProjectBuilder.build(plugInPort.getCurrentProject(), continuityAreas, component -> {
ComponentArea area = drawingManager.getComponentArea(component);
return area == null ? null : area.getOutlineBounds();
@@ -133,7 +133,10 @@ public class DrawingCache {
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
CacheValue value = null;
// a component out of view is drawn only to record its areas, which drawing it directly does
// without rendering an image that would push visible ones out of the cache
if (type != null && type.getEnableCache()
&& (visibleRect == null || visibleRect.intersects(component.getCachingBounds()))
&& shouldCache(component, componentState, zoom, scaleFactor)) {
value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom,
scaleFactor, zOrder, drawOptions, false);
@@ -32,6 +32,7 @@ import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
@@ -234,7 +235,9 @@ public class DrawingManager {
g2dWrapper.setColor(theme.getBgColor());
g2dWrapper.fillRect(0, 0, canvasDimension.width, canvasDimension.height);
g2d.clip(new Rectangle(new Point(0, 0), canvasDimension));
Rectangle canvasRect = new Rectangle(new Point(0, 0), canvasDimension);
g2d.clip(canvasRect);
Shape canvasDeviceClip = g2d.getTransform().createTransformedShape(canvasRect);
Rectangle2D extraSpaceRect = new Rectangle2D.Double();
AffineTransform extraSpaceTx = new AffineTransform();
@@ -253,6 +256,11 @@ public class DrawingManager {
visibleRect.getWidth() / zoom, visibleRect.getHeight() / zoom);
}
// components skip drawing themselves when the clip leaves them out, so a component whose
// areas are recorded is drawn against the whole canvas rather than the visible part of it;
// otherwise a trace cut scrolled out of view would not cut anything
Shape recordingClip = toUserSpace(g2d, canvasDeviceClip);
// Composite mainComposite = g2d.getComposite();
// Composite alphaComposite =
// AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
@@ -304,10 +312,15 @@ public class DrawingManager {
synchronized (this) {
g2dWrapper.startedDrawingComponent(i);
Shape visibleClip = null;
if (!trackArea) {
g2dWrapper.stopTracking();
} else {
g2dWrapper.startRecordingDrawnBounds();
if (recordingClip != null) {
visibleClip = g2d.getClip();
g2d.setClip(recordingClip);
}
}
Composite oldComposite = g2d.getComposite();
Font oldFont = g2d.getFont();
@@ -346,9 +359,15 @@ public class DrawingManager {
}
}
ComponentArea area = g2dWrapper.finishedDrawingComponent();
// after finishedDrawingComponent, which reverts the transform the clip was taken in
if (visibleClip != null) {
g2d.setClip(visibleClip);
}
if (trackArea && area != null && area.hasOutline()) {
componentAreaMap.put(component, area);
lastDrawnStateMap.put(component, state);
// the graph was built from the areas this one replaces
continuityGraphCache = null;
}
try {
@@ -655,6 +674,18 @@ public class DrawingManager {
bounds.getHeight() + 2 * OFF_SCREEN_MARGIN);
}
/**
* @return the specified device-space shape in the current user space of the graphics, or null
* when its transform cannot be inverted
*/
private static Shape toUserSpace(Graphics2D g2d, Shape deviceShape) {
try {
return g2d.getTransform().createInverse().createTransformedShape(deviceShape);
} catch (NoninvertibleTransformException e) {
return null;
}
}
private void configureRenderingHints(Graphics2D g2d, Set<DrawOption> drawOptions) {
if (drawOptions.contains(DrawOption.ANTIALIASING)) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
@@ -819,26 +850,35 @@ public class DrawingManager {
messageDispatcher.dispatchMessage(EventType.REPAINT);
}
public void findContinuityAreaAtPoint(Point2D p) {
if (continuityGraphCache == null)
continuityGraphCache = getContinuityGraph();
public void findContinuityAreaAtPoint(Project project, Point2D p) {
if (continuityGraphCache == null) {
continuityGraphCache = getContinuityGraph(project);
}
currentContinuityAreas = continuityGraphCache.findAreasFor(p);
}
public List<ContinuityArea> getContinuityAreas() {
/**
* Returns the continuity areas recorded for the components of the specified project, with
* negative areas subtracted from the positive areas of the components below them.
*
* @param project
* @return
*/
public List<ContinuityArea> getContinuityAreas(Project project) {
// Find all individual continuity areas for all components
List<ContinuityArea> preliminaryAreas = new ArrayList<ContinuityArea>();
List<Boolean> checkBreakout = new ArrayList<Boolean>();
for (Map.Entry<IDIYComponent<?>, ComponentArea> entry : this.componentAreaMap.entrySet()) {
ComponentArea a = entry.getValue();
// in z-order rather than the order the areas were recorded in, which changes every time a
// component is redrawn after an edit; a trace cut only cuts what lies below it
for (IDIYComponent<?> component : project.getComponents()) {
ComponentArea a = componentAreaMap.get(component);
if (a == null)
if (a == null) {
continue;
IDIYComponent<?> component = entry.getKey();
}
int layerId;
if (component instanceof ILayeredComponent) {
@@ -891,10 +931,13 @@ public class DrawingManager {
return areas;
}
public ContinuityGraph getContinuityGraph() {
public ContinuityGraph getContinuityGraph(Project project) {
Set<Connection> connections = new HashSet<Connection>();
int z = 0;
for (IDIYComponent<?> c : this.componentAreaMap.keySet()) {
for (IDIYComponent<?> c : project.getComponents()) {
if (!componentAreaMap.containsKey(c)) {
continue;
}
if (c instanceof IContinuity) {
for (int i = 0; i < c.getControlPointCount() - 1; i++)
for (int j = i + 1; j < c.getControlPointCount(); j++)
@@ -904,13 +947,13 @@ public class DrawingManager {
z++;
}
List<ContinuityArea> areas = getContinuityAreas();
List<ContinuityArea> areas = getContinuityAreas(project);
return NetlistBuilder.buildContinuityGraph(areas, connections);
}
public List<Area> getContinuityAreaProximity(float threshold) {
List<ContinuityArea> continuityAreas = getContinuityAreas();
public List<Area> getContinuityAreaProximity(Project project, float threshold) {
List<ContinuityArea> continuityAreas = getContinuityAreas(project);
Stroke s = ObjectCache.getInstance().fetchBasicStroke(threshold - 1); // value eyeballed for
// approx good results
@@ -685,7 +685,7 @@ public class Presenter implements IPlugInPort {
LOG.error("Unknown creation method: " + componentTypeSlot.getCreationMethod());
}
} else if (operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS || altDown) {
drawingManager.findContinuityAreaAtPoint(scaledPoint);
drawingManager.findContinuityAreaAtPoint(currentProject, scaledPoint);
messageDispatcher.dispatchMessage(EventType.REPAINT);
} else {
List<IDIYComponent<?>> newSelection = new ArrayList<IDIYComponent<?>>(selectedComponents);
@@ -3188,7 +3188,7 @@ public class Presenter implements IPlugInPort {
@Override
public List<Netlist> extractNetlists(boolean includeSwitches) throws NetlistException {
List<ContinuityArea> continuityAreas = drawingManager.getContinuityAreas();
List<ContinuityArea> continuityAreas = drawingManager.getContinuityAreas(currentProject);
return NetlistBuilder.extractNetlists(includeSwitches, currentProject, continuityAreas);
}
@@ -3246,12 +3246,13 @@ public class Presenter implements IPlugInPort {
areas.put(c, componentArea);
}
return new Snapshot(currentProject.clone(), new HashSet<IDIYComponent<?>>(selectedComponents), areas,
new HashSet<ContinuityArea>(drawingManager.getContinuityAreas()));
new HashSet<ContinuityArea>(drawingManager.getContinuityAreas(currentProject)));
}
@Override
public List<Area> checkContinuityAreaProximity(Size threshold) {
return drawingManager.getContinuityAreaProximity((float)threshold.convertToPixels());
return drawingManager.getContinuityAreaProximity(currentProject,
(float) threshold.convertToPixels());
}
@Override
@@ -641,6 +641,10 @@
<changeType>BUG_FIX</changeType>
<description>Projects with a resistor, capacitor or other leaded component whose Width or Length had been cleared in the property editor could not be opened; such values now fall back to the default size, and Width and Length can no longer be left empty</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Highlight Connected Areas', netlists and AI analysis could ignore trace cuts after the board under them was moved or edited, or when the cuts were scrolled out of view, and showed the whole strip as connected</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
@@ -448,7 +448,7 @@ public class DrawingManagerTest {
drawCachedAndClipped(drawingManager, project, 100);
// the netlist and continuity checks are built from the areas recorded while painting
assertEquals(1, drawingManager.getContinuityAreas().size());
assertEquals(1, drawingManager.getContinuityAreas(project).size());
assertEquals(1, component.drawCount);
drawCachedAndClipped(drawingManager, project, 100);
@@ -642,7 +642,7 @@ public class DrawingManagerTest {
drawVisibleRect(drawingManager, project, null);
// the cut splits the strip in two for the netlist...
assertEquals(2, drawingManager.getContinuityAreas().size());
assertEquals(2, drawingManager.getContinuityAreas(project).size());
// ...but the strip's own recorded area must stay whole, so that moving or deleting the cut
// does not leave a hole behind
Area recorded =
@@ -799,6 +799,66 @@ public class DrawingManagerTest {
return img;
}
@Test
public void testCutAppliesAfterStripIsRedrawn() {
Project project = new Project();
ConductorComponent strip = new ConductorComponent(new Rectangle2D.Double(0, 0, 100, 10), true);
ConductorComponent cut = new ConductorComponent(new Rectangle2D.Double(40, 0, 20, 10), false);
project.getComponents().add(strip);
project.getComponents().add(cut);
DrawingManager drawingManager = createDrawingManager();
drawVisibleRect(drawingManager, project, null);
// an edit to the board records its areas again, after those of the cuts already on it
drawingManager.invalidateComponent(strip);
drawVisibleRect(drawingManager, project, null);
assertEquals(2, drawingManager.getContinuityAreas(project).size());
}
@Test
public void testCutOutOfViewAppliesWhenAreasAreRecorded() {
Project project = new Project();
project.getComponents()
.add(new ConductorComponent(new Rectangle2D.Double(0, 0, 300, 10), true));
project.getComponents()
.add(new SelfCullingConductorComponent(new Rectangle2D.Double(240, 0, 20, 10), false));
DrawingManager drawingManager = createDrawingManager();
drawCachedAndClipped(drawingManager, project, 100);
assertEquals(2, drawingManager.getContinuityAreas(project).size());
}
@Test
public void testHighlightFollowsAreasRecordedAfterIt() {
Project project = new Project();
project.getComponents()
.add(new ConductorComponent(new Rectangle2D.Double(0, 0, 100, 10), true));
DrawingManager drawingManager = createDrawingManager();
drawVisibleRect(drawingManager, project, null);
drawingManager.findContinuityAreaAtPoint(project, new Point2D.Double(10, 5));
// nothing was edited, the cut was only drawn for the first time, e.g. scrolled into view
project.getComponents()
.add(new ConductorComponent(new Rectangle2D.Double(40, 0, 20, 10), false));
drawVisibleRect(drawingManager, project, null);
drawingManager.findContinuityAreaAtPoint(project, new Point2D.Double(10, 5));
BufferedImage img = render(drawingManager, project);
assertTrue(img.getRGB(10, 5) != img.getRGB(80, 5));
}
private BufferedImage render(DrawingManager drawingManager, Project project) {
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,
Collections.emptySet(), 1.0, 1.0, null);
g2d.dispose();
return img;
}
private static float farSideAlpha() {
return (float) (DrawingManager.FAR_SIDE_ALPHA.getValue().doubleValue() / 100d);
}
@@ -950,6 +1010,38 @@ public class DrawingManagerTest {
}
}
// skips drawing when clipped out, as TraceCut and the boards do
public static class SelfCullingConductorComponent extends ConductorComponent {
private static final long serialVersionUID = 1L;
private Point2D center;
public SelfCullingConductorComponent(Rectangle2D shape, boolean positive) {
super(shape, positive);
this.center = new Point2D.Double(shape.getCenterX(), shape.getCenterY());
}
@Override
public int getControlPointCount() {
return 1;
}
@Override
public Point2D getControlPoint(int index) {
return center;
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
super.draw(g2d, componentState, outlineMode, project, drawingObserver);
}
}
// Simple delegating Graphics2D proxy for testing
private static class Graphics2DProxy extends Graphics2D {
private final Graphics2D delegate;