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
diff --git a/.rive_head b/.rive_head
index 441fa05..aa93349 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-d5a92b315d2ba25377a7bb9d6824d4381c16b80a
+98647c98c4f4c241b9b9cd4bedefcf1044207582
diff --git a/tess/build/premake5_tess.lua b/tess/build/premake5_tess.lua
index 4177f5a..4d785a1 100644
--- a/tess/build/premake5_tess.lua
+++ b/tess/build/premake5_tess.lua
@@ -24,7 +24,6 @@
         '../src/**.cpp'
     }
     buildoptions {'-Wall', '-fno-exceptions', '-fno-rtti', '-Werror=format'}
-    defines {'CONTOUR_RECURSIVE'}
 
     filter 'configurations:debug'
     do
diff --git a/tess/include/rive/tess/contour_render_path.hpp b/tess/include/rive/tess/contour_render_path.hpp
deleted file mode 100644
index f6f6c1f..0000000
--- a/tess/include/rive/tess/contour_render_path.hpp
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/tess/include/rive/tess/contour_stroke.hpp b/tess/include/rive/tess/contour_stroke.hpp
index ba82659..5558c54 100644
--- a/tess/include/rive/tess/contour_stroke.hpp
+++ b/tess/include/rive/tess/contour_stroke.hpp
@@ -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 @@
         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,
diff --git a/tess/include/rive/tess/segmented_contour.hpp b/tess/include/rive/tess/segmented_contour.hpp
new file mode 100644
index 0000000..f586453
--- /dev/null
+++ b/tess/include/rive/tess/segmented_contour.hpp
@@ -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
\ No newline at end of file
diff --git a/tess/src/contour_render_path.cpp b/tess/src/contour_render_path.cpp
deleted file mode 100644
index 575c675..0000000
--- a/tess/src/contour_render_path.cpp
+++ /dev/null
@@ -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);
-}
\ No newline at end of file
diff --git a/tess/src/contour_render_path_recursive.cpp b/tess/src/contour_render_path_recursive.cpp
deleted file mode 100644
index a5c795c..0000000
--- a/tess/src/contour_render_path_recursive.cpp
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/tess/src/contour_stroke.cpp b/tess/src/contour_stroke.cpp
index 54a7934..653cd19 100644
--- a/tess/src/contour_stroke.cpp
+++ b/tess/src/contour_stroke.cpp
@@ -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 @@
     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 @@
     // 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;
diff --git a/tess/src/segmented_contour.cpp b/tess/src/segmented_contour.cpp
new file mode 100644
index 0000000..ac7df7b
--- /dev/null
+++ b/tess/src/segmented_contour.cpp
@@ -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;
+}