diff --git a/include/SFML/Graphics/Shape.hpp b/include/SFML/Graphics/Shape.hpp index 2dcd7c9e3..e697c0564 100644 --- a/include/SFML/Graphics/Shape.hpp +++ b/include/SFML/Graphics/Shape.hpp @@ -134,6 +134,37 @@ public: //////////////////////////////////////////////////////////// void setOutlineThickness(float thickness); + //////////////////////////////////////////////////////////// + /// \brief Set the limit on the ratio between miter length and outline thickness + /// + /// Outline segments around each shape corner are joined either + /// with a miter or a bevel join. + /// - A miter join is formed by extending outline segments until + /// they intersect. The distance between the point of + /// intersection and the shape's corner is the miter length. + /// - A bevel join is formed by connecting outline segments with + /// a straight line perpendicular to the corner's bissector. + /// + /// The miter limit is used to determine whether ouline segments + /// around a corner are joined with a bevel or a miter. + /// When the ratio between the miter length and outline thickness + /// exceeds the miter limit, a bevel is used instead of a miter. + /// + /// The miter limit is linked to the maximum inner angle of a + /// corner below which a bevel is used by the following formula: + /// + /// miterLimit = 1 / sin(angle / 2) + /// + /// The miter limit must be greater than or equal to 1. + /// By default, the miter limit is 10. + /// + /// \param miterLimit New miter limit + /// + /// \see getMiterLimit + /// + //////////////////////////////////////////////////////////// + void setMiterLimit(float miterLimit); + //////////////////////////////////////////////////////////// /// \brief Get the source texture of the shape /// @@ -188,6 +219,16 @@ public: //////////////////////////////////////////////////////////// float getOutlineThickness() const; + //////////////////////////////////////////////////////////// + /// \brief Get the limit on the ratio between miter length and outline thickness + /// + /// \return Limit on the ratio between miter length and outline thickness + /// + /// \see setMiterLimit + /// + //////////////////////////////////////////////////////////// + float getMiterLimit() const; + //////////////////////////////////////////////////////////// /// \brief Get the total number of points of the shape /// @@ -315,6 +356,7 @@ private: Color m_fillColor{Color::White}; //!< Fill color Color m_outlineColor{Color::White}; //!< Outline color float m_outlineThickness{}; //!< Thickness of the shape's outline + float m_miterLimit{10.f}; //!< Limit on the ratio between miter length and outline thickness VertexArray m_vertices{PrimitiveType::TriangleFan}; //!< Vertex array containing the fill geometry VertexArray m_outlineVertices{PrimitiveType::TriangleStrip}; //!< Vertex array containing the outline geometry FloatRect m_insideBounds; //!< Bounding rectangle of the inside (fill) diff --git a/src/SFML/Graphics/Shape.cpp b/src/SFML/Graphics/Shape.cpp index b9f95a26a..d806a655c 100644 --- a/src/SFML/Graphics/Shape.cpp +++ b/src/SFML/Graphics/Shape.cpp @@ -32,20 +32,19 @@ #include #include +#include #include namespace { -// Compute the normal of a segment -sf::Vector2f computeNormal(const sf::Vector2f& p1, const sf::Vector2f& p2, bool flipped) +// Compute the direction of a segment +sf::Vector2f computeDirection(const sf::Vector2f& p1, const sf::Vector2f& p2) { - sf::Vector2f normal = (p2 - p1).perpendicular(); - const float length = normal.length(); + sf::Vector2f direction = p2 - p1; + const float length = direction.length(); if (length != 0.f) - normal /= length; - if (flipped) - normal = -normal; - return normal; + direction /= length; + return direction; } } // namespace @@ -123,7 +122,7 @@ const Color& Shape::getOutlineColor() const void Shape::setOutlineThickness(float thickness) { m_outlineThickness = thickness; - update(); // recompute everything because the whole shape must be offset + updateOutline(); } @@ -134,6 +133,22 @@ float Shape::getOutlineThickness() const } +//////////////////////////////////////////////////////////// +void Shape::setMiterLimit(float miterLimit) +{ + assert(miterLimit >= 1.f && "Shape::setMiterLimit(float) cannot set miter limit to a value lower than 1"); + m_miterLimit = miterLimit; + updateOutline(); +} + + +//////////////////////////////////////////////////////////// +float Shape::getMiterLimit() const +{ + return m_miterLimit; +} + + //////////////////////////////////////////////////////////// Vector2f Shape::getGeometricCenter() const { @@ -284,8 +299,8 @@ void Shape::updateTexCoords() //////////////////////////////////////////////////////////// void Shape::updateOutline() { - // Return if there is no outline - if (m_outlineThickness == 0.f) + // Return if there is no outline or no vertices + if (m_outlineThickness == 0.f || m_vertices.getVertexCount() < 2) { m_outlineVertices.clear(); m_bounds = m_insideBounds; @@ -293,7 +308,8 @@ void Shape::updateOutline() } const std::size_t count = m_vertices.getVertexCount() - 2; - m_outlineVertices.resize((count + 1) * 2); + m_outlineVertices.resize((count + 1) * 2); // We need at least that many vertices. + // We will add two more vertices each time we need a bevel. // Determine if points are defined clockwise or counterclockwise. This will impact normals computation. const bool flipNormals = [this, count]() @@ -304,6 +320,7 @@ void Shape::updateOutline() return twiceArea >= 0.f; }(); + std::size_t outlineIndex = 0; for (std::size_t i = 0; i < count; ++i) { const std::size_t index = i + 1; @@ -313,22 +330,55 @@ void Shape::updateOutline() const Vector2f p1 = m_vertices[index].position; const Vector2f p2 = m_vertices[index + 1].position; + // Compute their direction + const Vector2f d1 = computeDirection(p0, p1); + const Vector2f d2 = computeDirection(p1, p2); + // Compute their normal pointing towards the outside of the shape - const Vector2f n1 = computeNormal(p0, p1, flipNormals); - const Vector2f n2 = computeNormal(p1, p2, flipNormals); + const Vector2f n1 = flipNormals ? -d1.perpendicular() : d1.perpendicular(); + const Vector2f n2 = flipNormals ? -d2.perpendicular() : d2.perpendicular(); - // Combine them to get the extrusion direction - const float factor = 1.f + (n1.x * n2.x + n1.y * n2.y); - const Vector2f normal = (n1 + n2) / factor; + // Decide whether to add a bevel or not + const float twoCos2 = 1.f + n1.dot(n2); + const float squaredLengthRatio = m_miterLimit * m_miterLimit * twoCos2 / 2.f; + const bool isConvexCorner = d1.dot(n2) * m_outlineThickness >= 0.f; + const bool needsBevel = twoCos2 == 0.f || (squaredLengthRatio < 1.f && isConvexCorner); - // Update the outline points - m_outlineVertices[i * 2 + 0].position = p1; - m_outlineVertices[i * 2 + 1].position = p1 + normal * m_outlineThickness; + if (needsBevel) + { + // Make room for two more vertices + m_outlineVertices.resize(m_outlineVertices.getVertexCount() + 2); + + // Combine normals to get bevel edge's direction and normal vector pointing towards the outside of the shape + const float twoSin2 = 1.f - n1.dot(n2); + const Vector2f direction = (n2 - n1) / twoSin2; // Length is 1 / sin + const Vector2f extrusion = (flipNormals != (d1.dot(n2) >= 0.f) ? direction : -direction).perpendicular(); + + // Compute bevel corner position in (direction, extrusion) coordinates + const float sin = std::sqrt(twoSin2 / 2.f); + const float u = m_miterLimit * sin; + const float v = 1.f - std::sqrt(squaredLengthRatio); + + // Update the outline points + m_outlineVertices[outlineIndex++].position = p1; + m_outlineVertices[outlineIndex++].position = p1 + (u * extrusion - v * direction) * m_outlineThickness; + m_outlineVertices[outlineIndex++].position = p1; + m_outlineVertices[outlineIndex++].position = p1 + (u * extrusion + v * direction) * m_outlineThickness; + } + else + { + // Combine normals to get the extrusion direction + const Vector2f extrusion = (n1 + n2) / twoCos2; + + // Update the outline points + m_outlineVertices[outlineIndex++].position = p1; + m_outlineVertices[outlineIndex++].position = p1 + extrusion * m_outlineThickness; + } } // Duplicate the first point at the end, to close the outline - m_outlineVertices[count * 2 + 0].position = m_outlineVertices[0].position; - m_outlineVertices[count * 2 + 1].position = m_outlineVertices[1].position; + m_outlineVertices[outlineIndex++].position = m_outlineVertices[0].position; + m_outlineVertices[outlineIndex++].position = m_outlineVertices[1].position; // Update outline colors updateOutlineColors(); diff --git a/test/Graphics/Shape.test.cpp b/test/Graphics/Shape.test.cpp index bd0abfde3..60f768fff 100644 --- a/test/Graphics/Shape.test.cpp +++ b/test/Graphics/Shape.test.cpp @@ -60,6 +60,7 @@ TEST_CASE("[Graphics] sf::Shape", runDisplayTests()) CHECK(triangleShape.getFillColor() == sf::Color::White); CHECK(triangleShape.getOutlineColor() == sf::Color::White); CHECK(triangleShape.getOutlineThickness() == 0.0f); + CHECK(triangleShape.getMiterLimit() == 10.0f); CHECK(triangleShape.getLocalBounds() == sf::FloatRect()); CHECK(triangleShape.getGlobalBounds() == sf::FloatRect()); } @@ -100,6 +101,13 @@ TEST_CASE("[Graphics] sf::Shape", runDisplayTests()) CHECK(triangleShape.getOutlineThickness() == 3.14f); } + SECTION("Set/get miter limit") + { + TriangleShape triangleShape({}); + triangleShape.setMiterLimit(6.28f); + CHECK(triangleShape.getMiterLimit() == 6.28f); + } + SECTION("Virtual functions: getPoint, getPointCount, getGeometricCenter") { const TriangleShape triangleShape({2, 2}); @@ -130,5 +138,13 @@ TEST_CASE("[Graphics] sf::Shape", runDisplayTests()) CHECK(triangleShape.getLocalBounds() == Approx(sf::FloatRect({-7.2150f, -14.2400f}, {44.4300f, 59.2400f}))); CHECK(triangleShape.getGlobalBounds() == Approx(sf::FloatRect({-7.2150f, -14.2400f}, {44.4300f, 59.2400f}))); } + + SECTION("Add beveled outline") + { + triangleShape.setMiterLimit(2); + triangleShape.setOutlineThickness(5); + CHECK(triangleShape.getLocalBounds() == Approx(sf::FloatRect({-7.2150f, -10.f}, {44.4300f, 55.f}))); + CHECK(triangleShape.getGlobalBounds() == Approx(sf::FloatRect({-7.2150f, -10.f}, {44.4300f, 55.f}))); + } } }