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