Skip to content

Commit

Permalink
Bevel shape outline corners beyond threshold
Browse files Browse the repository at this point in the history
  • Loading branch information
kimci86 committed Nov 4, 2023
1 parent d390ff7 commit 32d119f
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 25 deletions.
29 changes: 29 additions & 0 deletions include/SFML/Graphics/Shape.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,24 @@ class SFML_GRAPHICS_API Shape : public Drawable, public Transformable
////////////////////////////////////////////////////////////
void setOutlineThickness(float thickness);

////////////////////////////////////////////////////////////
/// \brief Set the limit on the ratio between miter length and outline thickness
///
/// The edges of the shape's outline around sharp angles can meet
/// well beyond the outline thickness. When the ratio between the
/// miter length (distance between the outline's tip and the shape's
/// corner) and the outline thickness exceeds the given limit, then
/// the join is converted from a miter to a bevel.
/// The miter limit must be greater than or equal to 1.
/// By default, the miter limit is 4.
///
/// \param miterLimit New miter limit
///
/// \see getMiterLimit
///
////////////////////////////////////////////////////////////
void setMiterLimit(float miterLimit);

////////////////////////////////////////////////////////////
/// \brief Get the source texture of the shape
///
Expand Down Expand Up @@ -181,6 +199,16 @@ class SFML_GRAPHICS_API Shape : public Drawable, public Transformable
////////////////////////////////////////////////////////////
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
///
Expand Down Expand Up @@ -314,6 +342,7 @@ class SFML_GRAPHICS_API Shape : public Drawable, public Transformable
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{4.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)
Expand Down
106 changes: 81 additions & 25 deletions src/SFML/Graphics/Shape.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,22 @@

#include <algorithm>

#include <cassert>
#include <cmath>

namespace
{
// Compute the normal of a segment
sf::Vector2f computeNormal(const sf::Vector2f& p1, const sf::Vector2f& p2, bool flipped)
// Compute the direction and normal unit vectors of a segment
std::pair<sf::Vector2f, sf::Vector2f> computeDirectionAndNormal(const sf::Vector2f& p1, const sf::Vector2f& p2, bool flipped)
{
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;
direction /= length;
sf::Vector2f normal = direction.perpendicular();
if (flipped)
normal = -normal;
return normal;
return {direction, normal};
}
} // namespace

Expand Down Expand Up @@ -120,7 +124,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();
}


Expand All @@ -131,6 +135,22 @@ float Shape::getOutlineThickness() const
}


////////////////////////////////////////////////////////////
void Shape::setMiterLimit(float miterLimit)
{
assert(1.f <= miterLimit && "Shape::setMiterLimit(float) cannot set miter limit to a value lower than 1");
m_miterLimit = std::max(1.f, miterLimit);
updateOutline();
}


////////////////////////////////////////////////////////////
float Shape::getMiterLimit() const
{
return m_miterLimit;
}


////////////////////////////////////////////////////////////
Vector2f Shape::getGeometricCenter() const
{
Expand Down Expand Up @@ -286,16 +306,17 @@ 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;
return;
}

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]()
Expand All @@ -306,31 +327,66 @@ void Shape::updateOutline()
return twiceArea >= 0.f;
}();

// In each iteration, d1 and n1 vectors will be the direction and normal of the segment before the current one.
auto [d1, n1] = computeDirectionAndNormal(m_vertices[count].position, m_vertices[1].position, flipNormals);

std::size_t outlineIndex = 0;
for (std::size_t i = 0; i < count; ++i)
{
const std::size_t index = i + 1;

// Get the two segments shared by the current point
const Vector2f p0 = (i == 0) ? m_vertices[count].position : m_vertices[index - 1].position;
// Get the segment starting at the current point
const Vector2f p1 = m_vertices[index].position;
const Vector2f p2 = m_vertices[index + 1].position;

// Compute their normal pointing towards the outside of the shape
const Vector2f n1 = computeNormal(p0, p1, flipNormals);
const Vector2f n2 = computeNormal(p1, p2, flipNormals);

// 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;

// Update the outline points
m_outlineVertices[i * 2 + 0].position = p1;
m_outlineVertices[i * 2 + 1].position = p1 + normal * m_outlineThickness;
// Compute its direction and normal pointing towards the outside of the shape
const auto [d2, n2] = computeDirectionAndNormal(p1, p2, flipNormals);

// 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 = 0.f <= d1.dot(n2) * m_outlineThickness;
const bool needsBevel = twoCos2 == 0.f || (squaredLengthRatio < 1.f && isConvexCorner);

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 float sin = std::sqrt(twoSin2 / 2.f);
const Vector2f direction = (n2 - n1) / twoSin2; // Length is 1 / sin
const Vector2f extrusion = (flipNormals != (0.f <= d1.dot(n2)) ? direction : -direction).perpendicular();

// Compute bevel corner position in (direction, extrusion) coordinates
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;
}

// Save direction and normal for the potential next iteration
d1 = d2;
n1 = n2;
}

// 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();
Expand Down
8 changes: 8 additions & 0 deletions test/Graphics/Shape.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ TEST_CASE("[Graphics] sf::Shape")
CHECK(triangleShape.getFillColor() == sf::Color::White);
CHECK(triangleShape.getOutlineColor() == sf::Color::White);
CHECK(triangleShape.getOutlineThickness() == 0.0f);
CHECK(triangleShape.getMiterLimit() == 4.0f);
CHECK(triangleShape.getLocalBounds() == sf::FloatRect());
CHECK(triangleShape.getGlobalBounds() == sf::FloatRect());
}
Expand Down Expand Up @@ -88,6 +89,13 @@ TEST_CASE("[Graphics] sf::Shape")
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});
Expand Down

0 comments on commit 32d119f

Please sign in to comment.