From afdc84bd1836331469fb96b97124ea6a343b6612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 5 Jul 2023 22:50:27 +0200 Subject: [PATCH] Fixed several issues with drawing ellipses I've replaced the ellipse drawing algorithms with implementations by Zingl Alois, originally written in 2012 and published under MIT license. This addresses several issues with the previously used implementations of pointsOnEllipse and ellipseRegion: * Ellipses no longer degrade when very large (near 2000 tiles in size). * No more gaps in the circle for some sizes. * Shape Fill tool can now actually draw circles of equal width and height and can draw both even and odd sized ellipses. The Shape Fill tool was adjusted such that drawing ellipses work in bounding-box mode by default. The Alt modifier now toggles to center + radius mode, which also applies when drawing rectangles. Closes #3775 --- NEWS.md | 1 + docs/manual/editing-tile-layers.rst | 11 +- src/tiled/geometry.cpp | 195 +++++++++++----------------- src/tiled/geometry.h | 11 +- src/tiled/shapefilltool.cpp | 37 +++--- src/tiled/stampbrush.cpp | 4 +- 6 files changed, 115 insertions(+), 144 deletions(-) diff --git a/NEWS.md b/NEWS.md index 18f82673f64..f9201fd6afd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,7 @@ * Fixed hover highlight rendering with active parallax factor (#3669) * Fixed updating of object selection outlines when changing parallax factor (#3669) * Fixed "Offset Map" action to offset all objects when choosing "Whole Map" as bounds +* Fixed several issues with drawing ellipses (#3776) * Godot 4 plugin: Export custom tile properties as Custom Data Layers (with Kevin Harrison, #3653) * AppImage: Updated to Sentry 0.6.4 * Qt 6: Increased the image allocation limit from 1 GB to 4 GB (#3616) diff --git a/docs/manual/editing-tile-layers.rst b/docs/manual/editing-tile-layers.rst index 46f29356d42..812d54e0c75 100644 --- a/docs/manual/editing-tile-layers.rst +++ b/docs/manual/editing-tile-layers.rst @@ -110,7 +110,16 @@ Shape Fill Tool Shortcut: ``P`` |rectangle-fill| This tool provides a quick way to fill rectangles or ellipses with a certain -tile or pattern. Hold ``Shift`` to fill an exact square or circle. +tile or pattern. + +- Holding ``Shift`` fills an exact square or circle. + +.. raw:: html + +
Since Tiled 1.10.2
+ +- Holding ``Alt`` draws the rectangle or ellipse centered around the starting + location. You can also flip and rotate the current stamp as described for the :ref:`stamp-tool`. diff --git a/src/tiled/geometry.cpp b/src/tiled/geometry.cpp index 7581f829ef8..eed67997754 100644 --- a/src/tiled/geometry.cpp +++ b/src/tiled/geometry.cpp @@ -1,6 +1,9 @@ /* * geometry.cpp * Copyright 2010-2011, Stefan Beller + * Copyright 2017, Benjamin Trotter + * Copyright 2020, Zingl Alois + * Copyright 2017-2023, Thorbjørn Lindeijer * * This file is part of Tiled. * @@ -26,142 +29,94 @@ namespace Tiled { /** * Returns a lists of points on an ellipse. - * (x0,y0) is the midpoint - * (x1,y1) determines the radius. + * (xm,ym) is the midpoint + * (a,b) determines the radii. * - * It is adapted from http://en.wikipedia.org/wiki/Midpoint_circle_algorithm - * here is the original: http://homepage.smc.edu/kennedy_john/belipse.pdf + * From "Bresenham Curve Rasterizing Algorithms". + * + * @version V20.15 april 2020 + * @copyright MIT open-source license software + * @url https://github.com/zingl/Bresenham + * @author Zingl Alois */ -QVector pointsOnEllipse(int x0, int y0, int x1, int y1) +QVector pointsOnEllipse(int xm, int ym, int a, int b) { QVector ret; - int x, y; - int xChange, yChange; - int ellipseError; - int twoXSquare, twoYSquare; - int stoppingX, stoppingY; - int radiusX = x0 > x1 ? x0 - x1 : x1 - x0; - int radiusY = y0 > y1 ? y0 - y1 : y1 - y0; - - if (radiusX == 0 && radiusY == 0) - return ret; - - twoXSquare = 2 * radiusX * radiusX; - twoYSquare = 2 * radiusY * radiusY; - x = radiusX; - y = 0; - xChange = radiusY * radiusY * (1 - 2 * radiusX); - yChange = radiusX * radiusX; - ellipseError = 0; - stoppingX = twoYSquare*radiusX; - stoppingY = 0; - while (stoppingX >= stoppingY) { - ret += QPoint(x0 + x, y0 + y); - ret += QPoint(x0 - x, y0 + y); - ret += QPoint(x0 + x, y0 - y); - ret += QPoint(x0 - x, y0 - y); - y++; - stoppingY += twoXSquare; - ellipseError += yChange; - yChange += twoXSquare; - if ((2 * ellipseError + xChange) > 0) { - x--; - stoppingX -= twoYSquare; - ellipseError += xChange; - xChange += twoYSquare; - } - } - x = 0; - y = radiusY; - xChange = radiusY * radiusY; - yChange = radiusX * radiusX * (1 - 2 * radiusY); - ellipseError = 0; - stoppingX = 0; - stoppingY = twoXSquare * radiusY; - while (stoppingX <= stoppingY) { - ret += QPoint(x0 + x, y0 + y); - ret += QPoint(x0 - x, y0 + y); - ret += QPoint(x0 + x, y0 - y); - ret += QPoint(x0 - x, y0 - y); - x++; - stoppingX += twoYSquare; - ellipseError += xChange; - xChange += twoYSquare; - if ((2 * ellipseError + yChange) > 0) { - y--; - stoppingY -= twoXSquare; - ellipseError += yChange; - yChange += twoXSquare; - } + + long x = -a, y = 0; /* II. quadrant from bottom left to top right */ + long e2 = b, dx = (1+2*x)*e2*e2; /* error increment */ + long dy = x*x, err = dx+dy; /* error of 1.step */ + + do { + ret += QPoint(xm-x, ym+y); /* I. Quadrant */ + ret += QPoint(xm+x, ym+y); /* II. Quadrant */ + ret += QPoint(xm+x, ym-y); /* III. Quadrant */ + ret += QPoint(xm-x, ym-y); /* IV. Quadrant */ + e2 = 2*err; + if (e2 >= dx) { x++; err += dx += 2*(long)b*b; } /* x step */ + if (e2 <= dy) { y++; err += dy += 2*(long)a*a; } /* y step */ + } while (x <= 0); + + while (y++ < b) { /* too early stop for flat ellipses with a=1, */ + ret += QPoint(xm, ym+y); /* -> finish tip of ellipse */ + ret += QPoint(xm, ym-y); } return ret; } /** - * returns an elliptical region centered at x0,y0 with radius determined by x1,y1 + * Returns an elliptical region based on a rectangle given by x0,y0 (top-left) + * and x1,y1 (bottom-right), inclusive. + * + * From "Bresenham Curve Rasterizing Algorithms", adjusted to output a filled + * region instead of an outline. + * + * @version V20.15 april 2020 + * @copyright MIT open-source license software + * @url https://github.com/zingl/Bresenham + * @author Zingl Alois */ QRegion ellipseRegion(int x0, int y0, int x1, int y1) { QRegion ret; - int x, y; - int xChange, yChange; - int ellipseError; - int twoXSquare, twoYSquare; - int stoppingX, stoppingY; - int radiusX = x0 > x1 ? x0 - x1 : x1 - x0; - int radiusY = y0 > y1 ? y0 - y1 : y1 - y0; - - if (radiusX == 0 && radiusY == 0) - return ret; - - twoXSquare = 2 * radiusX * radiusX; - twoYSquare = 2 * radiusY * radiusY; - x = radiusX; - y = 0; - xChange = radiusY * radiusY * (1 - 2 * radiusX); - yChange = radiusX * radiusX; - ellipseError = 0; - stoppingX = twoYSquare*radiusX; - stoppingY = 0; - while (stoppingX >= stoppingY) { - ret += QRect(-x, y, x * 2, 1); - ret += QRect(-x, -y, x * 2, 1); - y++; - stoppingY += twoXSquare; - ellipseError += yChange; - yChange += twoXSquare; - if ((2 * ellipseError + xChange) > 0) { - x--; - stoppingX -= twoYSquare; - ellipseError += xChange; - xChange += twoYSquare; - } - } - x = 0; - y = radiusY; - xChange = radiusY * radiusY; - yChange = radiusX * radiusX * (1 - 2 * radiusY); - ellipseError = 0; - stoppingX = 0; - stoppingY = twoXSquare * radiusY; - while (stoppingX <= stoppingY) { - ret += QRect(-x, y, x * 2, 1); - ret += QRect(-x, -y, x * 2, 1); - x++; - stoppingX += twoYSquare; - ellipseError += xChange; - xChange += twoYSquare; - if ((2 * ellipseError + yChange) > 0) { - y--; - stoppingY -= twoXSquare; - ellipseError += yChange; - yChange += twoXSquare; - } + + auto addRect = [&ret](int x0, int y0, int x1, int y1) { + ret += QRect(QPoint(x0, y0), QPoint(x1, y1)); + }; + + long a = abs(x1-x0), b = abs(y1-y0), b1 = b&1; /* diameter */ + double dx = 4*(1.0-a)*b*b, dy = 4*(b1+1)*a*a; /* error increment */ + double err = dx+dy+b1*a*a, e2; /* error of 1.step */ + + if (x0 > x1) { x0 = x1; x1 += a; } /* if called with swapped points */ + if (y0 > y1) y0 = y1; /* .. exchange them */ + y0 += (b+1)/2; y1 = y0-b1; /* starting pixel */ + a = 8*a*a; b1 = 8*b*b; + + do { + // (x1, y0) /* I. Quadrant */ + // (x0, y0) /* II. Quadrant */ + // (x0, y1) /* III. Quadrant */ + // (x1, y1) /* IV. Quadrant */ + + addRect(x0, y0, x1, y0); /* Bottom half */ + addRect(x0, y1, x1, y1); /* Top half */ + + e2 = 2*err; + if (e2 <= dy) { y0++; y1--; err += dy += a; } /* y step */ + if (e2 >= dx || 2*err > dy) { x0++; x1--; err += dx += b1; } /* x step */ + } while (x0 <= x1); + + while (y0-y1 <= b) { /* too early stop of flat ellipses a=1 */ + addRect(x0-1, y0, x1+1, y0); /* -> finish tip of ellipse */ + addRect(x0-1, y1, x1+1, y1); + y0++; + y1--; } - return ret.translated(x0, y0); -} + return ret; +}; /** * Returns the lists of points on a line from (x0,y0) to (x1,y1). diff --git a/src/tiled/geometry.h b/src/tiled/geometry.h index 8dfd059cc2e..726f3b5b551 100644 --- a/src/tiled/geometry.h +++ b/src/tiled/geometry.h @@ -1,6 +1,8 @@ /* * geometry.h * Copyright 2010-2011, Stefan Beller + * Copyright 2017, Benjamin Trotter + * Copyright 2017-2023, Thorbjørn Lindeijer * * This file is part of Tiled. * @@ -27,12 +29,15 @@ namespace Tiled { -QVector pointsOnEllipse(int x0, int y0, int x1, int y1); +QVector pointsOnEllipse(int xm, int ym, int a, int b); QRegion ellipseRegion(int x0, int y0, int x1, int y1); QVector pointsOnLine(int x0, int y0, int x1, int y1, bool manhattan = false); -inline QVector pointsOnEllipse(QPoint a, QPoint b) -{ return pointsOnEllipse(a.x(), a.y(), b.x(), b.y()); } +inline QVector pointsOnEllipse(QPoint center, int radiusX, int radiusY) +{ return pointsOnEllipse(center.x(), center.y(), radiusX, radiusY); } + +inline QRegion ellipseRegion(QRect rect) +{ return ellipseRegion(rect.left(), rect.top(), rect.right(), rect.bottom()); } inline QVector pointsOnLine(QPoint a, QPoint b, bool manhattan = false) { return pointsOnLine(a.x(), a.y(), b.x(), b.y(), manhattan); } diff --git a/src/tiled/shapefilltool.cpp b/src/tiled/shapefilltool.cpp index 6731b9540ea..155d6249ed6 100644 --- a/src/tiled/shapefilltool.cpp +++ b/src/tiled/shapefilltool.cpp @@ -21,16 +21,15 @@ #include "shapefilltool.h" #include "actionmanager.h" -#include "addremovetileset.h" #include "brushitem.h" #include "geometry.h" #include "mapdocument.h" -#include "painttilelayer.h" #include "stampactions.h" -#include #include +#include #include +#include #include @@ -62,9 +61,9 @@ ShapeFillTool::ShapeFillTool(QObject *parent) ActionManager::registerAction(mRectFill, "ShapeFillTool.RectangleFill"); ActionManager::registerAction(mCircleFill, "ShapeFillTool.CircleFill"); - connect(mRectFill, &QAction::triggered, + connect(mRectFill, &QAction::triggered, this, [this] { setCurrentShape(Rect); }); - connect(mCircleFill, &QAction::triggered, + connect(mCircleFill, &QAction::triggered, this, [this] { setCurrentShape(Circle); }); setActionsEnabled(false); @@ -207,27 +206,27 @@ void ShapeFillTool::updateFillOverlay() dy = ((dy > 0) - (dy < 0)) * min; } - const QRect boundingRect(mStartCorner, mStartCorner + QPoint(dx, dy)); + const bool alt = mModifiers & Qt::AltModifier; + const QPoint p1 = alt ? mStartCorner - QPoint(dx, dy) + : mStartCorner; + const QPoint p2 = mStartCorner + QPoint(dx, dy); - switch (mCurrentShape) { - case Rect: { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - QRect area = boundingRect.normalized(); - if (area.width() == 0) - area.adjust(-1, 0, 1, 0); - if (area.height() == 0) - area.adjust(0, -1, 0, 1); + QRect area = QRect(p1, p2).normalized(); + if (area.width() == 0) + area.adjust(-1, 0, 1, 0); + if (area.height() == 0) + area.adjust(0, -1, 0, 1); #else - QRect area = QRect::span(mStartCorner, mStartCorner + QPoint(dx, dy)); + QRect area = QRect::span(p1, p2); #endif + + switch (mCurrentShape) { + case Rect: updatePreview(area); break; - } case Circle: - updatePreview(ellipseRegion(boundingRect.left(), - boundingRect.top(), - boundingRect.right(), - boundingRect.bottom())); + updatePreview(ellipseRegion(area)); break; } } diff --git a/src/tiled/stampbrush.cpp b/src/tiled/stampbrush.cpp index 70660d51542..d07bd79bed3 100644 --- a/src/tiled/stampbrush.cpp +++ b/src/tiled/stampbrush.cpp @@ -617,7 +617,9 @@ void StampBrush::updatePreview(QPoint tilePos) drawPreviewLayer(pointsOnLine(mStampReference, tilePos)); break; case CircleMidSet: - drawPreviewLayer(pointsOnEllipse(mStampReference, tilePos)); + drawPreviewLayer(pointsOnEllipse(mStampReference, + qAbs(mStampReference.x() - tilePos.x()), + qAbs(mStampReference.y() - tilePos.y()))); break; case Capture: // already handled above