Images can randomly disappear when zooming in and scrolling

This commit is contained in:
Bane Stojkovic
2026-08-20 14:53:36 +02:00
parent b766c55263
commit 8d506b21c4
5 changed files with 374 additions and 46 deletions
@@ -417,6 +417,10 @@
<changeType>IMPROVEMENT</changeType>
<description>Renamed Mac OS build files to start with a 'mac' prefix instead of 'osx'</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Images can randomly disappear when zooming in and scrolling</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
@@ -21,6 +21,7 @@ import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
@@ -105,56 +106,56 @@ public class Image extends AbstractTransparentComponent<Void> {
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
ImageIcon imageIcon = getImage();
if (imageIcon == null || imageIcon.getImage() == null || imageIcon.getIconWidth() <= 0
|| imageIcon.getIconHeight() <= 0) {
return;
}
if (!getImageRectangle().intersects(g2d.getClipBounds())) {
Shape clip = g2d.getClip();
if (clip != null && !clip.intersects(getImageRectangle())) {
return;
}
double scaleX;
double scaleY;
ImageIcon imageIcon = getImage();
if (getSizingMode() == ImageSizingMode.Scale) {
scaleX = scaleY = 1d * getScale().getValue() / 100d;
} else {
Point2D secondPoint = getControlPoint(1);
scaleX = 1d * Math.abs(point.getX() - secondPoint.getX()) / imageIcon.getIconWidth();
scaleY = 1d * Math.abs(point.getY() - secondPoint.getY()) / imageIcon.getIconHeight();
}
Shape clip = g2d.getClip().getBounds();
if (!clip.intersects(new Rectangle2D.Double(point.getX(), point.getY(), imageIcon.getIconWidth() * scaleX, imageIcon
.getIconHeight() * scaleY))) {
return;
}
Composite oldComposite = applyAlpha(g2d, componentState);
double x;
double y;
if (getSizingMode() == ImageSizingMode.Scale)
{
if (getSizingMode() == ImageSizingMode.Scale) {
scaleX = scaleY = 1d * getScale().getValue() / 100d;
x = point.getX();
y = point.getY();
} else {
Point2D secondPoint = getControlPoint(1);
scaleX = 1d * Math.abs(point.getX() - secondPoint.getX()) / imageIcon.getIconWidth();
scaleY = 1d * Math.abs(point.getY() - secondPoint.getY()) / imageIcon.getIconHeight();
x = Math.min(point.getX(), secondPoint.getX());
y = Math.min(point.getY(), secondPoint.getY());
}
Composite oldComposite = applyAlpha(g2d, componentState);
AffineTransform oldTx = g2d.getTransform();
if (getOrientation() != Orientation.DEFAULT) {
double theta = getOrientation().toRadians();
g2d.rotate(theta, x, y);
}
g2d.scale(scaleX, scaleY);
g2d.drawImage(imageIcon.getImage(), (int) (x / scaleX), (int) (y / scaleY), null);
g2d.drawImage(imageIcon.getImage(), (int) Math.round(x / scaleX), (int) Math.round(y / scaleY), null);
if (componentState == ComponentState.SELECTED) {
g2d.setComposite(oldComposite);
g2d.scale(1 / scaleX, 1 / scaleY);
g2d.setColor(SELECTION_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.drawRect((int)x, (int)y, (int) (imageIcon.getIconWidth() * scaleX), (int) (imageIcon.getIconHeight() * scaleY));
g2d.drawRect((int) Math.round(x), (int) Math.round(y),
(int) Math.round(imageIcon.getIconWidth() * scaleX),
(int) Math.round(imageIcon.getIconHeight() * scaleY));
}
g2d.setTransform(oldTx);
g2d.setComposite(oldComposite);
}
@Override
@@ -296,22 +297,34 @@ public class Image extends AbstractTransparentComponent<Void> {
protected Point2D getFinalSecondPoint() {
Point2D finalSecondPoint;
if (getSizingMode() == ImageSizingMode.TwoPoints) {
finalSecondPoint = secondPoint;
finalSecondPoint = getControlPoint(1);
} else {
double scale = 1d * getScale().getValue() / 100d;
ImageIcon iconImage = getImage();
finalSecondPoint = new Point2D.Double(point.getX() + iconImage.getIconWidth() * scale,
point.getY() + iconImage.getIconHeight() * scale);
double w = (iconImage != null && iconImage.getIconWidth() > 0) ? iconImage.getIconWidth() * scale : 0;
double h = (iconImage != null && iconImage.getIconHeight() > 0) ? iconImage.getIconHeight() * scale : 0;
finalSecondPoint = new Point2D.Double(point.getX() + w, point.getY() + h);
}
return finalSecondPoint;
}
protected Rectangle2D getImageRectangle() {
Point2D finalSecondPoint = getFinalSecondPoint();
return new Rectangle2D.Double(Math.min(point.getX(), finalSecondPoint.getX()),
Math.min(point.getY(), finalSecondPoint.getY()),
Math.abs(point.getX() - finalSecondPoint.getX()),
Math.abs(point.getY() - finalSecondPoint.getY()));
double x = Math.min(point.getX(), finalSecondPoint.getX());
double y = Math.min(point.getY(), finalSecondPoint.getY());
double w = Math.abs(point.getX() - finalSecondPoint.getX());
double h = Math.abs(point.getY() - finalSecondPoint.getY());
Rectangle2D rect = new Rectangle2D.Double(x, y, w, h);
if (getOrientation() != null && getOrientation() != Orientation.DEFAULT) {
AffineTransform tx = AffineTransform.getRotateInstance(getOrientation().toRadians(), x, y);
return tx.createTransformedShape(rect).getBounds2D();
}
return rect;
}
@Override
public Rectangle2D getCachingBounds() {
return getImageRectangle();
}
public static enum ImageSizingMode {
@@ -22,6 +22,7 @@ import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
@@ -89,35 +90,35 @@ public class SVGImage extends AbstractTransparentComponent<Void> {
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
double scaleX;
double scaleY;
SVGDiagram svgDiagram = getSvgDiagram();
if (getSizingMode() == ImageSizingMode.Scale) {
scaleX = scaleY = 1d * getScale().getValue() / 100d;
} else {
Point2D secondPoint = getControlPoint(1);
scaleX = 1d * Math.abs(point.getX() - secondPoint.getX()) / svgDiagram.getWidth();
scaleY = 1d * Math.abs(point.getY() - secondPoint.getY()) / svgDiagram.getHeight();
}
Shape clip = g2d.getClip().getBounds();
if (!clip.intersects(new Rectangle2D.Double(point.getX(), point.getY(),
svgDiagram.getWidth() * scaleX, svgDiagram.getHeight() * scaleY))) {
if (svgDiagram == null || svgDiagram.getWidth() <= 0 || svgDiagram.getHeight() <= 0) {
return;
}
Composite oldComposite = applyAlpha(g2d, componentState);
Shape clip = g2d.getClip();
if (clip != null && !clip.intersects(getImageRectangle())) {
return;
}
double scaleX;
double scaleY;
double x;
double y;
if (getSizingMode() == ImageSizingMode.Scale) {
scaleX = scaleY = 1d * getScale().getValue() / 100d;
x = point.getX();
y = point.getY();
} else {
Point2D secondPoint = getControlPoint(1);
scaleX = 1d * Math.abs(point.getX() - secondPoint.getX()) / svgDiagram.getWidth();
scaleY = 1d * Math.abs(point.getY() - secondPoint.getY()) / svgDiagram.getHeight();
x = Math.min(point.getX(), secondPoint.getX());
y = Math.min(point.getY(), secondPoint.getY());
}
Composite oldComposite = applyAlpha(g2d, componentState);
AffineTransform oldTx = g2d.getTransform();
if (getOrientation() != Orientation.DEFAULT) {
double theta = getOrientation().toRadians();
g2d.rotate(theta, x, y);
@@ -139,9 +140,13 @@ public class SVGImage extends AbstractTransparentComponent<Void> {
g2d.scale(1 / scaleX, 1 / scaleY);
g2d.setColor(SELECTION_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
g2d.drawRect((int) x, (int) y, (int) (svgDiagram.getWidth() * scaleX),
(int) (svgDiagram.getHeight() * scaleY));
g2d.drawRect((int) Math.round(x), (int) Math.round(y),
(int) Math.round(svgDiagram.getWidth() * scaleX),
(int) Math.round(svgDiagram.getHeight() * scaleY));
}
g2d.setTransform(oldTx);
g2d.setComposite(oldComposite);
}
@Override
@@ -280,6 +285,39 @@ public class SVGImage extends AbstractTransparentComponent<Void> {
this.orientation = orientation;
}
protected Point2D getFinalSecondPoint() {
Point2D finalSecondPoint;
if (getSizingMode() == ImageSizingMode.TwoPoints) {
finalSecondPoint = getControlPoint(1);
} else {
double scale = 1d * getScale().getValue() / 100d;
SVGDiagram diagram = getSvgDiagram();
double w = (diagram != null && diagram.getWidth() > 0) ? diagram.getWidth() * scale : 0;
double h = (diagram != null && diagram.getHeight() > 0) ? diagram.getHeight() * scale : 0;
finalSecondPoint = new Point2D.Double(point.getX() + w, point.getY() + h);
}
return finalSecondPoint;
}
protected Rectangle2D getImageRectangle() {
Point2D finalSecondPoint = getFinalSecondPoint();
double x = Math.min(point.getX(), finalSecondPoint.getX());
double y = Math.min(point.getY(), finalSecondPoint.getY());
double w = Math.abs(point.getX() - finalSecondPoint.getX());
double h = Math.abs(point.getY() - finalSecondPoint.getY());
Rectangle2D rect = new Rectangle2D.Double(x, y, w, h);
if (getOrientation() != null && getOrientation() != Orientation.DEFAULT) {
AffineTransform tx = AffineTransform.getRotateInstance(getOrientation().toRadians(), x, y);
return tx.createTransformedShape(rect).getBounds2D();
}
return rect;
}
@Override
public Rectangle2D getCachingBounds() {
return getImageRectangle();
}
public static enum ImageSizingMode {
TwoPoints("Opposing Points"), Scale("Scale");
@@ -0,0 +1,201 @@
package org.diylc.components.misc;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import org.diylc.common.Orientation;
import org.diylc.common.Percentage;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.junit.Assert;
import org.junit.Test;
public class ImageTest {
private static final IDrawingObserver NOOP_OBSERVER = new IDrawingObserver() {
@Override public void startTracking() {}
@Override public void stopTracking() {}
@Override public void startTrackingContinuityArea(boolean positive) {}
@Override public void stopTrackingContinuityArea() {}
@Override public boolean isTrackingContinuityArea() { return false; }
@Override public void setContinuityMarker(String marker) {}
};
@Test
public void testImageRectangleScaleModeDefaultOrientation() {
Image image = new Image();
image.setControlPoint(new Point2D.Double(100, 200), 0);
image.setScale(new Percentage(100));
ImageIcon icon = image.getImage();
int w = icon.getIconWidth();
int h = icon.getIconHeight();
Rectangle2D bounds = image.getImageRectangle();
Assert.assertEquals(100.0, bounds.getX(), 0.01);
Assert.assertEquals(200.0, bounds.getY(), 0.01);
Assert.assertEquals((double) w, bounds.getWidth(), 0.01);
Assert.assertEquals((double) h, bounds.getHeight(), 0.01);
}
@Test
public void testImageRectangleScaleMode50Percent() {
Image image = new Image();
image.setControlPoint(new Point2D.Double(100, 200), 0);
image.setScale(new Percentage(50));
ImageIcon icon = image.getImage();
int w = icon.getIconWidth();
int h = icon.getIconHeight();
Rectangle2D bounds = image.getImageRectangle();
Assert.assertEquals(100.0, bounds.getX(), 0.01);
Assert.assertEquals(200.0, bounds.getY(), 0.01);
Assert.assertEquals(w * 0.5, bounds.getWidth(), 0.01);
Assert.assertEquals(h * 0.5, bounds.getHeight(), 0.01);
}
@Test
public void testImageRectangleRotated180() {
Image image = new Image();
image.setControlPoint(new Point2D.Double(500, 500), 0);
image.setScale(new Percentage(100));
image.setOrientation(Orientation._180);
ImageIcon icon = image.getImage();
int w = icon.getIconWidth();
int h = icon.getIconHeight();
Rectangle2D bounds = image.getImageRectangle();
Assert.assertEquals(500.0 - w, bounds.getX(), 0.01);
Assert.assertEquals(500.0 - h, bounds.getY(), 0.01);
Assert.assertEquals((double) w, bounds.getWidth(), 0.01);
Assert.assertEquals((double) h, bounds.getHeight(), 0.01);
}
@Test
public void testImageRectangleRotated90() {
Image image = new Image();
image.setControlPoint(new Point2D.Double(500, 500), 0);
image.setScale(new Percentage(100));
image.setOrientation(Orientation._90);
ImageIcon icon = image.getImage();
int w = icon.getIconWidth();
int h = icon.getIconHeight();
Rectangle2D bounds = image.getImageRectangle();
Assert.assertEquals(500.0 - h, bounds.getX(), 0.01);
Assert.assertEquals(500.0, bounds.getY(), 0.01);
Assert.assertEquals((double) h, bounds.getWidth(), 0.01);
Assert.assertEquals((double) w, bounds.getHeight(), 0.01);
}
@Test
public void testImageRectangleRotated270() {
Image image = new Image();
image.setControlPoint(new Point2D.Double(500, 500), 0);
image.setScale(new Percentage(100));
image.setOrientation(Orientation._270);
ImageIcon icon = image.getImage();
int w = icon.getIconWidth();
int h = icon.getIconHeight();
Rectangle2D bounds = image.getImageRectangle();
Assert.assertEquals(500.0, bounds.getX(), 0.01);
Assert.assertEquals(500.0 - w, bounds.getY(), 0.01);
Assert.assertEquals((double) h, bounds.getWidth(), 0.01);
Assert.assertEquals((double) w, bounds.getHeight(), 0.01);
}
@Test
public void testImageRectangleTwoPointsMode() {
Image image = new Image();
image.setSizingMode(Image.ImageSizingMode.TwoPoints);
image.setControlPoint(new Point2D.Double(400, 500), 0);
image.setControlPoint(new Point2D.Double(100, 200), 1);
Rectangle2D bounds = image.getImageRectangle();
Assert.assertEquals(100.0, bounds.getX(), 0.01);
Assert.assertEquals(200.0, bounds.getY(), 0.01);
Assert.assertEquals(300.0, bounds.getWidth(), 0.01);
Assert.assertEquals(300.0, bounds.getHeight(), 0.01);
}
@Test
public void testDrawWithNullClipDoesNotThrow() {
Image image = new Image();
image.setControlPoint(new Point2D.Double(50, 50), 0);
BufferedImage img = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setClip(null);
// Should not throw NPE
image.draw(g2d, ComponentState.NORMAL, false, new Project(), NOOP_OBSERVER);
g2d.dispose();
}
@Test
public void testDrawRotatedImageRendersWhenRotatedIntoClip() {
Image image = new Image();
image.setControlPoint(new Point2D.Double(100, 100), 0);
image.setScale(new Percentage(100));
image.setOrientation(Orientation._180);
ImageIcon icon = image.getImage();
int w = icon.getIconWidth();
int h = icon.getIconHeight();
// The image at (100, 100) rotated 180 extends to (100 - w, 100 - h).
// The unrotated rectangle would be (100, 100, w, h).
// Clip at (0, 0, 100, 100) overlaps the rotated image, but NOT the unrotated rect.
BufferedImage target = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = target.createGraphics();
g2d.setClip(new Rectangle(0, 0, 100, 100));
// Must not skip rendering
image.draw(g2d, ComponentState.NORMAL, false, new Project(), NOOP_OBSERVER);
g2d.dispose();
// Verify some pixels were drawn in the (0, 0, 100, 100) region
boolean hasNonZeroPixel = false;
for (int x = 0; x < 100; x++) {
for (int y = 0; y < 100; y++) {
if ((target.getRGB(x, y) & 0xFF000000) != 0) {
hasNonZeroPixel = true;
break;
}
}
if (hasNonZeroPixel) break;
}
Assert.assertTrue("Rotated image should have rendered pixels in the clip region", hasNonZeroPixel);
}
@Test
public void testDrawSkipsWhenCompletelyOutsideClip() {
Image image = new Image();
image.setControlPoint(new Point2D.Double(500, 500), 0);
image.setScale(new Percentage(100));
BufferedImage target = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = target.createGraphics();
g2d.setClip(new Rectangle(0, 0, 100, 100));
image.draw(g2d, ComponentState.NORMAL, false, new Project(), NOOP_OBSERVER);
g2d.dispose();
// Target image should remain completely blank
for (int x = 0; x < 100; x++) {
for (int y = 0; y < 100; y++) {
Assert.assertEquals(0, target.getRGB(x, y));
}
}
}
}
@@ -0,0 +1,72 @@
package org.diylc.components.misc;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import org.diylc.common.Orientation;
import org.diylc.common.Percentage;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.junit.Assert;
import org.junit.Test;
public class SVGImageTest {
private static final IDrawingObserver NOOP_OBSERVER = new IDrawingObserver() {
@Override public void startTracking() {}
@Override public void stopTracking() {}
@Override public void startTrackingContinuityArea(boolean positive) {}
@Override public void stopTrackingContinuityArea() {}
@Override public boolean isTrackingContinuityArea() { return false; }
@Override public void setContinuityMarker(String marker) {}
};
@Test
public void testSVGImageRectangleScaleMode() {
SVGImage svg = new SVGImage();
svg.setControlPoint(new Point2D.Double(100, 200), 0);
svg.setScale(new Percentage(100));
Rectangle2D bounds = svg.getImageRectangle();
Assert.assertEquals(100.0, bounds.getX(), 0.01);
Assert.assertEquals(200.0, bounds.getY(), 0.01);
Assert.assertTrue(bounds.getWidth() > 0);
Assert.assertTrue(bounds.getHeight() > 0);
}
@Test
public void testSVGImageDrawWithNullClipDoesNotThrow() {
SVGImage svg = new SVGImage();
svg.setControlPoint(new Point2D.Double(50, 50), 0);
BufferedImage img = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setClip(null);
svg.draw(g2d, ComponentState.NORMAL, false, new Project(), NOOP_OBSERVER);
g2d.dispose();
}
@Test
public void testSVGImageRotated180Clipping() {
SVGImage svg = new SVGImage();
svg.setControlPoint(new Point2D.Double(100, 100), 0);
svg.setScale(new Percentage(100));
svg.setOrientation(Orientation._180);
Rectangle2D bounds = svg.getImageRectangle();
Assert.assertTrue(bounds.getX() < 100.0);
Assert.assertTrue(bounds.getY() < 100.0);
BufferedImage target = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = target.createGraphics();
g2d.setClip(new Rectangle(0, 0, 100, 100));
svg.draw(g2d, ComponentState.NORMAL, false, new Project(), NOOP_OBSERVER);
g2d.dispose();
}
}