mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 21:21:17 +01:00
Adding SegmentedContour
Adding a utility class for TessRenderPath to turn a RawPath into a segmented contour that it can then triangulate. More of the "move towards composition vs inheritance" work. Also removes old ContourRenderPath as this effectively replaces the logic it tried to implement via inheritance. This mostly moves that old code around, in doing so it also adopts the new path iteration which leaves the "quad" case for the segmenter unhandled, which is ok for now as none of our render paths currently have quad commands. Diffs= 98647c98c Change based on feedback 1db72a148 Adding SegmentedContour and removing old contour_render_path
This commit is contained in:
@@ -24,7 +24,6 @@ do
|
||||
'../src/**.cpp'
|
||||
}
|
||||
buildoptions {'-Wall', '-fno-exceptions', '-fno-rtti', '-Werror=format'}
|
||||
defines {'CONTOUR_RECURSIVE'}
|
||||
|
||||
filter 'configurations:debug'
|
||||
do
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
#ifndef _RIVE_CONTOUR_RENDER_PATH_HPP_
|
||||
#define _RIVE_CONTOUR_RENDER_PATH_HPP_
|
||||
|
||||
#include "rive/renderer.hpp"
|
||||
#include "rive/math/aabb.hpp"
|
||||
#include "rive/tess/sub_path.hpp"
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace rive {
|
||||
enum class PathCommandType : uint8_t {
|
||||
/// Corresponds to CommandPath::moveTo
|
||||
move,
|
||||
/// Corresponds to CommandPath::lineTo
|
||||
line,
|
||||
/// Corresponds to CommandPath::cubicTo
|
||||
cubic,
|
||||
/// Corresponds to CommandPath::close
|
||||
close
|
||||
};
|
||||
|
||||
class PathCommand {
|
||||
private:
|
||||
PathCommandType m_Type;
|
||||
/// Only used when m_Type is cubic.
|
||||
Vec2D m_OutPoint;
|
||||
|
||||
/// Only used when m_Type is cubic.
|
||||
Vec2D m_InPoint;
|
||||
|
||||
/// Only used when m_Type is move or close or cubic.
|
||||
Vec2D m_Point;
|
||||
|
||||
public:
|
||||
PathCommand(PathCommandType type);
|
||||
PathCommand(PathCommandType type, float x, float y);
|
||||
PathCommand(
|
||||
PathCommandType type, float outX, float outY, float inX, float inY, float x, float y);
|
||||
|
||||
PathCommandType type() const { return m_Type; }
|
||||
const Vec2D& outPoint() const { return m_OutPoint; }
|
||||
const Vec2D& inPoint() const { return m_InPoint; }
|
||||
const Vec2D& point() const { return m_Point; }
|
||||
};
|
||||
|
||||
class ContourStroke;
|
||||
///
|
||||
/// Segments curves into line segments and computes the bounds of the
|
||||
/// segmented curve.
|
||||
///
|
||||
class ContourRenderPath : public RenderPath {
|
||||
protected:
|
||||
AABB m_ContourBounds;
|
||||
std::vector<Vec2D> m_ContourVertices;
|
||||
std::vector<SubPath> m_SubPaths;
|
||||
std::vector<PathCommand> m_Commands;
|
||||
bool m_IsDirty = true;
|
||||
float m_ContourThreshold = 1.0f;
|
||||
bool m_IsClosed = false;
|
||||
|
||||
public:
|
||||
std::size_t contourLength() const { return m_ContourVertices.size(); }
|
||||
const std::vector<Vec2D>& contourVertices() const { return m_ContourVertices; }
|
||||
bool isClosed() const { return m_IsClosed; }
|
||||
|
||||
bool isContainer() const;
|
||||
void addRenderPath(RenderPath* path, const Mat2D& transform) override;
|
||||
|
||||
void reset() override;
|
||||
void moveTo(float x, float y) override;
|
||||
void lineTo(float x, float y) override;
|
||||
void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override;
|
||||
void close() override;
|
||||
|
||||
void computeContour();
|
||||
bool isDirty() const { return m_IsDirty; }
|
||||
|
||||
void extrudeStroke(ContourStroke* stroke,
|
||||
StrokeJoin join,
|
||||
StrokeCap cap,
|
||||
float strokeWidth,
|
||||
const Mat2D& transform);
|
||||
};
|
||||
} // namespace rive
|
||||
#endif
|
||||
@@ -8,10 +8,10 @@
|
||||
#include <cstdint>
|
||||
|
||||
namespace rive {
|
||||
class ContourRenderPath;
|
||||
class SegmentedContour;
|
||||
|
||||
///
|
||||
/// Builds a triangle strip vertex buffer from a ContourRenderPath.
|
||||
/// Builds a triangle strip vertex buffer from a SegmentedContour.
|
||||
///
|
||||
class ContourStroke {
|
||||
protected:
|
||||
@@ -26,7 +26,7 @@ namespace rive {
|
||||
void resetRenderOffset();
|
||||
void nextRenderOffset(std::size_t& start, std::size_t& end);
|
||||
|
||||
void extrude(const ContourRenderPath* renderPath,
|
||||
void extrude(const SegmentedContour* contour,
|
||||
bool isClosed,
|
||||
StrokeJoin join,
|
||||
StrokeCap cap,
|
||||
|
||||
44
tess/include/rive/tess/segmented_contour.hpp
Normal file
44
tess/include/rive/tess/segmented_contour.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef _RIVE_SEGMENTED_CONTOUR_HPP_
|
||||
#define _RIVE_SEGMENTED_CONTOUR_HPP_
|
||||
|
||||
#include "rive/math/vec2d.hpp"
|
||||
#include "rive/math/aabb.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace rive {
|
||||
class RawPath;
|
||||
|
||||
/// Utilty for converting a RawPath into a contour segments.
|
||||
class SegmentedContour {
|
||||
private:
|
||||
Vec2D m_pen;
|
||||
Vec2D m_penDown;
|
||||
bool m_isPenDown = false;
|
||||
std::vector<Vec2D> m_contourPoints;
|
||||
|
||||
AABB m_bounds;
|
||||
float m_threshold;
|
||||
float m_thresholdSquared;
|
||||
|
||||
void addVertex(Vec2D vertex);
|
||||
void penDown();
|
||||
void close();
|
||||
void segmentCubic(const Vec2D& from,
|
||||
const Vec2D& fromOut,
|
||||
const Vec2D& toIn,
|
||||
const Vec2D& to,
|
||||
float t1,
|
||||
float t2);
|
||||
|
||||
public:
|
||||
const Span<const Vec2D> contourPoints() const;
|
||||
SegmentedContour(float threshold);
|
||||
|
||||
float threshold() const;
|
||||
void threshold(float value);
|
||||
const AABB& bounds() const;
|
||||
|
||||
void contour(const RawPath& rawPath);
|
||||
};
|
||||
} // namespace rive
|
||||
#endif
|
||||
@@ -1,60 +0,0 @@
|
||||
#include "rive/tess/contour_render_path.hpp"
|
||||
#include "rive/tess/contour_stroke.hpp"
|
||||
|
||||
using namespace rive;
|
||||
|
||||
PathCommand::PathCommand(PathCommandType type) : m_Type(type) {}
|
||||
PathCommand::PathCommand(PathCommandType type, float x, float y) : m_Type(type), m_Point(x, y) {}
|
||||
PathCommand::PathCommand(
|
||||
PathCommandType type, float outX, float outY, float inX, float inY, float x, float y) :
|
||||
m_Type(type), m_OutPoint(outX, outY), m_InPoint(inX, inY), m_Point(x, y) {}
|
||||
|
||||
void ContourRenderPath::addRenderPath(RenderPath* path, const Mat2D& transform) {
|
||||
m_SubPaths.emplace_back(SubPath(path, transform));
|
||||
}
|
||||
|
||||
void ContourRenderPath::reset() {
|
||||
m_IsClosed = false;
|
||||
m_SubPaths.clear();
|
||||
m_ContourVertices.clear();
|
||||
m_Commands.clear();
|
||||
m_IsDirty = true;
|
||||
}
|
||||
|
||||
void ContourRenderPath::moveTo(float x, float y) {
|
||||
m_Commands.emplace_back(PathCommand(PathCommandType::move, x, y));
|
||||
}
|
||||
|
||||
void ContourRenderPath::lineTo(float x, float y) {
|
||||
m_Commands.emplace_back(PathCommand(PathCommandType::line, x, y));
|
||||
}
|
||||
|
||||
void ContourRenderPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y) {
|
||||
m_Commands.emplace_back(PathCommand(PathCommandType::cubic, ox, oy, ix, iy, x, y));
|
||||
}
|
||||
void ContourRenderPath::close() {
|
||||
m_Commands.emplace_back(PathCommand(PathCommandType::close));
|
||||
m_IsClosed = true;
|
||||
}
|
||||
|
||||
bool ContourRenderPath::isContainer() const { return !m_SubPaths.empty(); }
|
||||
|
||||
void ContourRenderPath::extrudeStroke(ContourStroke* stroke,
|
||||
StrokeJoin join,
|
||||
StrokeCap cap,
|
||||
float strokeWidth,
|
||||
const Mat2D& transform) {
|
||||
if (isContainer()) {
|
||||
for (auto& subPath : m_SubPaths) {
|
||||
static_cast<ContourRenderPath*>(subPath.path())
|
||||
->extrudeStroke(stroke, join, cap, strokeWidth, subPath.transform());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDirty()) {
|
||||
computeContour();
|
||||
}
|
||||
|
||||
stroke->extrude(this, m_IsClosed, join, cap, strokeWidth, transform);
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
/// This is optional as we intend to try out other types of contouring like
|
||||
/// https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
|
||||
#if defined(CONTOUR_RECURSIVE)
|
||||
|
||||
#include "rive/tess/contour_render_path.hpp"
|
||||
#include "rive/math/cubic_utilities.hpp"
|
||||
#include <cassert>
|
||||
|
||||
using namespace rive;
|
||||
|
||||
// TODO when we add strokes, add ranges in the contour that need to be stroked
|
||||
// as contiguous lines.
|
||||
|
||||
// struct StrokeRange
|
||||
// {
|
||||
// unsigned int start;
|
||||
// unsigned int end;
|
||||
// };
|
||||
|
||||
class RecursiveCubicSegmenter {
|
||||
private:
|
||||
Vec2D m_Pen, m_PenDown;
|
||||
bool m_IsPenDown = false;
|
||||
std::vector<Vec2D>* m_Contour;
|
||||
// std::vector<StrokeRange> m_StrokeRanges;
|
||||
|
||||
AABB m_Bounds;
|
||||
float m_Threshold, m_ThresholdSquared;
|
||||
|
||||
public:
|
||||
RecursiveCubicSegmenter(std::vector<Vec2D>* contour, float threshold) :
|
||||
m_Contour(contour),
|
||||
m_Bounds(AABB::forExpansion()),
|
||||
m_Threshold(threshold),
|
||||
m_ThresholdSquared(threshold * threshold) {}
|
||||
|
||||
const Vec2D& pen() { return m_Pen; }
|
||||
bool isPenDown() { return m_IsPenDown; }
|
||||
|
||||
void addVertex(const Vec2D& vertex) {
|
||||
m_Contour->emplace_back(vertex);
|
||||
AABB::expandTo(m_Bounds, vertex);
|
||||
}
|
||||
|
||||
const AABB& bounds() const { return m_Bounds; }
|
||||
|
||||
inline void penUp() {
|
||||
if (!m_IsPenDown) {
|
||||
return;
|
||||
}
|
||||
m_IsPenDown = false;
|
||||
}
|
||||
|
||||
inline void penDown() {
|
||||
if (m_IsPenDown) {
|
||||
return;
|
||||
}
|
||||
m_IsPenDown = true;
|
||||
m_PenDown = m_Pen;
|
||||
addVertex(m_PenDown);
|
||||
}
|
||||
|
||||
inline void close() {
|
||||
if (!m_IsPenDown) {
|
||||
return;
|
||||
}
|
||||
m_Pen = m_PenDown;
|
||||
m_IsPenDown = false;
|
||||
|
||||
// TODO: Can we optimize and not dupe this point if it's the last point
|
||||
// already in the list? For example: a procedural triangle closes itself
|
||||
// with a lineTo the first point.
|
||||
addVertex(m_PenDown);
|
||||
}
|
||||
|
||||
inline void pen(const Vec2D& position) { m_Pen = position; }
|
||||
|
||||
void segmentCubic(const Vec2D& from,
|
||||
const Vec2D& fromOut,
|
||||
const Vec2D& toIn,
|
||||
const Vec2D& to,
|
||||
float t1,
|
||||
float t2) {
|
||||
if (CubicUtilities::shouldSplitCubic(from, fromOut, toIn, to, m_Threshold)) {
|
||||
float halfT = (t1 + t2) / 2.0f;
|
||||
|
||||
Vec2D hull[6];
|
||||
CubicUtilities::computeHull(from, fromOut, toIn, to, 0.5f, hull);
|
||||
|
||||
segmentCubic(from, hull[0], hull[3], hull[5], t1, halfT);
|
||||
|
||||
segmentCubic(hull[5], hull[4], hull[2], to, halfT, t2);
|
||||
} else {
|
||||
if (Vec2D::distanceSquared(from, to) > m_ThresholdSquared) {
|
||||
addVertex(Vec2D(CubicUtilities::cubicAt(t2, from.x, fromOut.x, toIn.x, to.x),
|
||||
CubicUtilities::cubicAt(t2, from.y, fromOut.y, toIn.y, to.y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void ContourRenderPath::computeContour() {
|
||||
m_IsDirty = false;
|
||||
assert(m_ContourVertices.empty());
|
||||
RecursiveCubicSegmenter segmenter(&m_ContourVertices, m_ContourThreshold);
|
||||
|
||||
// First four vertices are the bounds.
|
||||
m_ContourVertices.emplace_back(Vec2D());
|
||||
m_ContourVertices.emplace_back(Vec2D());
|
||||
m_ContourVertices.emplace_back(Vec2D());
|
||||
m_ContourVertices.emplace_back(Vec2D());
|
||||
|
||||
for (rive::PathCommand& command : m_Commands) {
|
||||
switch (command.type()) {
|
||||
case PathCommandType::move:
|
||||
segmenter.penUp();
|
||||
segmenter.pen(command.point());
|
||||
break;
|
||||
case PathCommandType::line:
|
||||
segmenter.penDown();
|
||||
segmenter.pen(command.point());
|
||||
segmenter.addVertex(command.point());
|
||||
break;
|
||||
case PathCommandType::cubic:
|
||||
segmenter.penDown();
|
||||
segmenter.segmentCubic(segmenter.pen(),
|
||||
command.outPoint(),
|
||||
command.inPoint(),
|
||||
command.point(),
|
||||
0.0f,
|
||||
1.0f);
|
||||
// segmenter.addVertex(command.point());
|
||||
segmenter.pen(command.point());
|
||||
break;
|
||||
case PathCommandType::close:
|
||||
segmenter.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO: when we stroke we may want to differentiate whether or not the path
|
||||
// actually closed.
|
||||
segmenter.close();
|
||||
|
||||
// TODO: consider if there's a case with no points.
|
||||
m_ContourBounds = segmenter.bounds();
|
||||
Vec2D& first = m_ContourVertices[0];
|
||||
first.x = m_ContourBounds.minX;
|
||||
first.y = m_ContourBounds.minY;
|
||||
|
||||
Vec2D& second = m_ContourVertices[1];
|
||||
second.x = m_ContourBounds.maxX;
|
||||
second.y = m_ContourBounds.minY;
|
||||
|
||||
Vec2D& third = m_ContourVertices[2];
|
||||
third.x = m_ContourBounds.maxX;
|
||||
third.y = m_ContourBounds.maxY;
|
||||
|
||||
Vec2D& fourth = m_ContourVertices[3];
|
||||
fourth.x = m_ContourBounds.minX;
|
||||
fourth.y = m_ContourBounds.maxY;
|
||||
}
|
||||
#endif
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "rive/math/math_types.hpp"
|
||||
#include "rive/tess/contour_stroke.hpp"
|
||||
#include "rive/tess/contour_render_path.hpp"
|
||||
#include "rive/tess/segmented_contour.hpp"
|
||||
#include "rive/math/vec2d.hpp"
|
||||
#include <assert.h>
|
||||
#include <algorithm>
|
||||
@@ -22,7 +22,7 @@ void ContourStroke::nextRenderOffset(std::size_t& start, std::size_t& end) {
|
||||
end = m_Offsets[m_RenderOffset++];
|
||||
}
|
||||
|
||||
void ContourStroke::extrude(const ContourRenderPath* renderPath,
|
||||
void ContourStroke::extrude(const SegmentedContour* contour,
|
||||
bool isClosed,
|
||||
StrokeJoin join,
|
||||
StrokeCap cap,
|
||||
@@ -31,8 +31,9 @@ void ContourStroke::extrude(const ContourRenderPath* renderPath,
|
||||
// TODO: if transform is identity, no need to copy and transform
|
||||
// contourPoints->points.
|
||||
|
||||
const std::vector<Vec2D>& contourPoints = renderPath->contourVertices();
|
||||
std::vector<Vec2D> points(contourPoints);
|
||||
auto contourPoints = contour->contourPoints();
|
||||
std::vector<Vec2D> points(contourPoints.begin(), contourPoints.end());
|
||||
|
||||
auto pointCount = points.size();
|
||||
if (pointCount < 6) {
|
||||
return;
|
||||
|
||||
121
tess/src/segmented_contour.cpp
Normal file
121
tess/src/segmented_contour.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "rive/tess/segmented_contour.hpp"
|
||||
#include "rive/math/raw_path.hpp"
|
||||
#include "rive/math/cubic_utilities.hpp"
|
||||
|
||||
using namespace rive;
|
||||
|
||||
SegmentedContour::SegmentedContour(float threshold) :
|
||||
m_threshold(threshold), m_thresholdSquared(threshold * threshold) {}
|
||||
|
||||
float SegmentedContour::threshold() const { return m_threshold; }
|
||||
void SegmentedContour::threshold(float value) {
|
||||
m_threshold = value;
|
||||
m_thresholdSquared = value * value;
|
||||
}
|
||||
const AABB& SegmentedContour::bounds() const { return m_bounds; }
|
||||
void SegmentedContour::addVertex(Vec2D vertex) {}
|
||||
void SegmentedContour::penDown() {
|
||||
if (m_isPenDown) {
|
||||
return;
|
||||
}
|
||||
m_isPenDown = true;
|
||||
m_penDown = m_pen;
|
||||
addVertex(m_penDown);
|
||||
}
|
||||
void SegmentedContour::close() {
|
||||
if (!m_isPenDown) {
|
||||
return;
|
||||
}
|
||||
m_pen = m_penDown;
|
||||
m_isPenDown = false;
|
||||
|
||||
// TODO: Can we optimize and not dupe this point if it's the last point
|
||||
// already in the list? For example: a procedural triangle closes itself
|
||||
// with a lineTo the first point.
|
||||
addVertex(m_penDown);
|
||||
}
|
||||
|
||||
const Span<const Vec2D> SegmentedContour::contourPoints() const {
|
||||
return Span<const Vec2D>(m_contourPoints.data(), m_contourPoints.size());
|
||||
}
|
||||
|
||||
void SegmentedContour::segmentCubic(const Vec2D& from,
|
||||
const Vec2D& fromOut,
|
||||
const Vec2D& toIn,
|
||||
const Vec2D& to,
|
||||
float t1,
|
||||
float t2) {
|
||||
if (CubicUtilities::shouldSplitCubic(from, fromOut, toIn, to, m_threshold)) {
|
||||
float halfT = (t1 + t2) / 2.0f;
|
||||
|
||||
Vec2D hull[6];
|
||||
CubicUtilities::computeHull(from, fromOut, toIn, to, 0.5f, hull);
|
||||
|
||||
segmentCubic(from, hull[0], hull[3], hull[5], t1, halfT);
|
||||
|
||||
segmentCubic(hull[5], hull[4], hull[2], to, halfT, t2);
|
||||
} else {
|
||||
if (Vec2D::distanceSquared(from, to) > m_thresholdSquared) {
|
||||
addVertex(Vec2D(CubicUtilities::cubicAt(t2, from.x, fromOut.x, toIn.x, to.x),
|
||||
CubicUtilities::cubicAt(t2, from.y, fromOut.y, toIn.y, to.y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SegmentedContour::contour(const RawPath& rawPath) {
|
||||
m_contourPoints.clear();
|
||||
|
||||
// First four vertices are the bounds.
|
||||
m_contourPoints.emplace_back(Vec2D());
|
||||
m_contourPoints.emplace_back(Vec2D());
|
||||
m_contourPoints.emplace_back(Vec2D());
|
||||
m_contourPoints.emplace_back(Vec2D());
|
||||
|
||||
RawPath::Iter iter(rawPath);
|
||||
while (auto rec = iter.next()) {
|
||||
switch (rec.verb) {
|
||||
case PathVerb::move:
|
||||
m_isPenDown = false;
|
||||
m_pen = rec.pts[0];
|
||||
break;
|
||||
case PathVerb::line:
|
||||
penDown();
|
||||
m_pen = rec.pts[0];
|
||||
addVertex(rec.pts[0]);
|
||||
break;
|
||||
case PathVerb::cubic:
|
||||
penDown();
|
||||
segmentCubic(m_pen, rec.pts[0], rec.pts[1], rec.pts[2], 0.0f, 1.0f);
|
||||
m_pen = rec.pts[2];
|
||||
break;
|
||||
case PathVerb::close:
|
||||
close();
|
||||
break;
|
||||
case PathVerb::quad:
|
||||
// TODO: not currently used by render paths, however might be
|
||||
// necessary for fonts.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: when we stroke we may want to differentiate whether or not the path
|
||||
// actually closed.
|
||||
close();
|
||||
|
||||
// TODO: consider if there's a case with no points.
|
||||
Vec2D& first = m_contourPoints[0];
|
||||
first.x = m_bounds.minX;
|
||||
first.y = m_bounds.minY;
|
||||
|
||||
Vec2D& second = m_contourPoints[1];
|
||||
second.x = m_bounds.maxX;
|
||||
second.y = m_bounds.minY;
|
||||
|
||||
Vec2D& third = m_contourPoints[2];
|
||||
third.x = m_bounds.maxX;
|
||||
third.y = m_bounds.maxY;
|
||||
|
||||
Vec2D& fourth = m_contourPoints[3];
|
||||
fourth.x = m_bounds.minX;
|
||||
fourth.y = m_bounds.maxY;
|
||||
}
|
||||
Reference in New Issue
Block a user