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 Dec 10, 2023
1 parent 04d6136 commit 6383b34
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 24 deletions.
42 changes: 42 additions & 0 deletions include/SFML/Graphics/Shape.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,37 @@ 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
///
/// 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
///
Expand Down Expand Up @@ -181,6 +212,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 @@ -308,6 +349,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{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)
Expand Down
100 changes: 76 additions & 24 deletions src/SFML/Graphics/Shape.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,19 @@

#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 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

Expand Down Expand Up @@ -120,7 +121,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 +132,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
{
Expand Down Expand Up @@ -282,16 +299,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 @@ -302,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;
Expand All @@ -311,22 +330,55 @@ void Shape::updateOutline()
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;
// Compute their direction
const Vector2f d1 = computeDirection(p0, p1);
const Vector2f d2 = computeDirection(p1, p2);

// Update the outline points
m_outlineVertices[i * 2 + 0].position = p1;
m_outlineVertices[i * 2 + 1].position = p1 + normal * m_outlineThickness;
// Compute their normal pointing towards the outside of the shape
const Vector2f n1 = flipNormals ? -d1.perpendicular() : d1.perpendicular();
const Vector2f n2 = flipNormals ? -d2.perpendicular() : d2.perpendicular();

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

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();
Expand Down
16 changes: 16 additions & 0 deletions test/Graphics/Shape.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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());
}
Expand Down Expand Up @@ -99,6 +100,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});
Expand Down Expand Up @@ -129,5 +137,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})));
}
}
}

0 comments on commit 6383b34

Please sign in to comment.