Bevel shape outline corners beyond threshold

This commit is contained in:
kimci86 2024-04-19 00:45:27 +02:00
parent 6886d1686b
commit 0b9c80d50e
3 changed files with 130 additions and 22 deletions

View File

@ -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)

View File

@ -32,20 +32,19 @@
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstddef>
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 normal pointing towards the outside of the shape
const Vector2f n1 = computeNormal(p0, p1, flipNormals);
const Vector2f n2 = computeNormal(p1, p2, flipNormals);
// Compute their direction
const Vector2f d1 = computeDirection(p0, p1);
const Vector2f d2 = computeDirection(p1, p2);
// 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 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[i * 2 + 0].position = p1;
m_outlineVertices[i * 2 + 1].position = p1 + normal * m_outlineThickness;
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();

View File

@ -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})));
}
}
}