text on path experiment

Diffs=
946d84479 text on path experiment
diff --git a/.rive_head b/.rive_head
index c56723d..f3589ac 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-db49cdfaa1ec3a56ad7c5d26dd574e67498a96ae
+946d84479e404dd8405dd7d91789b95c52cd40bc
diff --git a/include/rive/math/path_types.hpp b/include/rive/math/path_types.hpp
index d6d1711..b3591d7 100644
--- a/include/rive/math/path_types.hpp
+++ b/include/rive/math/path_types.hpp
@@ -23,13 +23,16 @@
     };
 
     enum class PathVerb : uint8_t {
-        move,
-        line,
-        quad,
-        conic_unused, // so we match skia's order
-        cubic,
-        close,
+        // These deliberately match Skia's values
+        move    = 0,
+        line    = 1,
+        quad    = 2,
+        // conic
+        cubic   = 4,
+        close   = 5,
     };
 
+    int path_verb_to_point_count(PathVerb);
+
 } // namespace rive
 #endif
diff --git a/include/rive/math/raw_path.hpp b/include/rive/math/raw_path.hpp
index 046f065..56e141d 100644
--- a/include/rive/math/raw_path.hpp
+++ b/include/rive/math/raw_path.hpp
@@ -37,6 +37,9 @@
 
         RawPath transform(const Mat2D&) const;
         void transformInPlace(const Mat2D&);
+        RawPath operator*(const Mat2D& mat) const {
+            return this->transform(mat);
+        }
 
         Span<const Vec2D> points() const { return toSpan(m_Points); }
         Span<Vec2D> points() { return toSpan(m_Points); }
@@ -63,6 +66,27 @@
         void addRect(const AABB&, PathDirection = PathDirection::cw);
         void addOval(const AABB&, PathDirection = PathDirection::cw);
         void addPoly(Span<const Vec2D>, bool isClosed);
+
+        class Iter {
+            const Vec2D* m_currPts;
+            const PathVerb* m_currVerb;
+            const PathVerb* m_stopVerb; // 1 past last verb
+        public:
+            Iter(const RawPath& path) {
+                m_currPts  = path.m_Points.data();
+                m_currVerb = path.m_Verbs.data();
+                m_stopVerb = path.m_Verbs.data() + path.m_Verbs.size();
+            }
+            
+            struct Rec {
+                const Vec2D* pts;
+                int count;
+                PathVerb verb;
+                
+                operator bool() const { return pts != nullptr; }
+            };
+            Rec next();
+        };
     };
 
 } // namespace rive
diff --git a/skia/renderer/include/skia_factory.hpp b/skia/renderer/include/skia_factory.hpp
index 22c432e..b9e0699 100644
--- a/skia/renderer/include/skia_factory.hpp
+++ b/skia/renderer/include/skia_factory.hpp
@@ -11,6 +11,7 @@
 namespace rive {
 
 class SkiaFactory : public Factory {
+public:
     rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override;
     rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override;
     rcp<RenderBuffer> makeBufferF32(Span<const float>) override;
@@ -44,7 +45,6 @@
     // New virtual for access the platform's codecs
     //
 
-public:
     enum class ColorType {
         rgba,
         bgra,
diff --git a/skia/renderer/include/to_skia.hpp b/skia/renderer/include/to_skia.hpp
index e92649c..0ba14f8 100644
--- a/skia/renderer/include/to_skia.hpp
+++ b/skia/renderer/include/to_skia.hpp
@@ -7,10 +7,12 @@
 
 #include "include/core/SkMatrix.h"
 #include "include/core/SkPaint.h"
+#include "include/core/SkPath.h"
 #include "include/core/SkPathTypes.h"
 #include "include/core/SkTileMode.h"
 
 #include "rive/math/mat2d.hpp"
+#include "rive/math/raw_path.hpp"
 #include "rive/math/vec2d.hpp"
 #include "rive/renderer.hpp"
 #include "rive/shapes/paint/stroke_cap.hpp"
@@ -91,6 +93,14 @@
             assert(false);
             return SkBlendMode::kSrcOver;
         }
+
+        static SkPath convert(const RawPath& rp) {
+            const auto pts = rp.points();
+            const auto vbs = rp.verbsU8();
+            return SkPath::Make((const SkPoint*)pts.data(), pts.size(),
+                                vbs.data(), vbs.size(),
+                                nullptr, 0, SkPathFillType::kWinding);
+        }
         // clang-format off
     };
 } // namespace rive
diff --git a/skia/viewer/src/contour_measure.cpp b/skia/viewer/src/contour_measure.cpp
new file mode 100644
index 0000000..dc97423
--- /dev/null
+++ b/skia/viewer/src/contour_measure.cpp
@@ -0,0 +1,51 @@
+
+
+#include "contour_measure.hpp"
+#include "to_skia.hpp"
+
+#include "include/core/SkContourMeasure.h"
+
+namespace rive {
+
+ContourMeasure::ContourMeasure(const RawPath& path) {
+    auto skpath = ToSkia::convert(path);
+    m_meas = SkContourMeasureIter(skpath, false).next().release();
+}
+
+ContourMeasure::~ContourMeasure() {
+    m_meas->unref();
+}
+
+float ContourMeasure::length() const { return m_meas->length(); }
+
+bool ContourMeasure::computePosTan(float distance, Vec2D* pos, Vec2D* tan) const {
+    return m_meas->getPosTan(distance, (SkPoint*)pos, (SkPoint*)tan);
+}
+
+RawPath ContourMeasure::warp(const RawPath& src) const {
+    RawPath dst;
+
+    RawPath::Iter iter(src);
+    while (auto rec = iter.next()) {
+        switch (rec.verb) {
+            case PathVerb::move:
+                dst.move(this->warp(rec.pts[0]));
+                break;
+            case PathVerb::line:
+                dst.line(this->warp(rec.pts[0]));
+                break;
+            case PathVerb::quad:
+                dst.quad(this->warp(rec.pts[0]), this->warp(rec.pts[1]));
+                break;
+            case PathVerb::cubic:
+                dst.cubic(this->warp(rec.pts[0]), this->warp(rec.pts[1]), this->warp(rec.pts[2]));
+                break;
+            case PathVerb::close:
+                dst.close();
+                break;
+        }
+    }
+    return dst;
+}
+
+}
diff --git a/skia/viewer/src/contour_measure.hpp b/skia/viewer/src/contour_measure.hpp
new file mode 100644
index 0000000..564efcc
--- /dev/null
+++ b/skia/viewer/src/contour_measure.hpp
@@ -0,0 +1,43 @@
+
+
+#ifndef _RIVE_CONTOUR_MEASURE_HPP_
+#define _RIVE_CONTOUR_MEASURE_HPP_
+
+#include "rive/math/raw_path.hpp"
+
+class SkContourMeasure;
+
+namespace rive {
+
+    class ContourMeasure {
+        SkContourMeasure* m_meas;
+    public:
+        ContourMeasure(const RawPath& path);
+        ~ContourMeasure();
+
+        float length() const;
+
+        bool computePosTan(float distance, Vec2D* pos, Vec2D* tan) const;
+
+        bool warp(Vec2D src, Vec2D* dst) const {
+            Vec2D pos, tan;
+            if (this->computePosTan(src.x, &pos, &tan)) {
+                *dst = {
+                    pos.x - tan.y * src.y,
+                    pos.y + tan.x * src.y,
+                };
+                return true;
+            }
+            return false;
+        }
+
+        Vec2D warp(Vec2D point) const {
+            Vec2D result;
+            return this->warp(point, &result) ? result : Vec2D{0, 0};
+        }
+
+        RawPath warp(const RawPath&) const;
+    };
+}
+
+#endif
diff --git a/skia/viewer/src/text_content.cpp b/skia/viewer/src/text_content.cpp
index cbae245..8f62b46 100644
--- a/skia/viewer/src/text_content.cpp
+++ b/skia/viewer/src/text_content.cpp
@@ -110,6 +110,8 @@
     std::vector<int> m_breaks;
 
     std::vector<RenderFontGlyphRuns> m_gruns;
+    rive::Mat2D m_xform;
+    float m_width = 300;
 
     RenderFontTextRuns make_truns(RenderFontFactory fact) {
         auto loader = [fact](const char filename[]) -> rive::rcp<rive::RenderFont> {
@@ -158,14 +160,14 @@
             m_gruns.push_back(truns[0].font->shapeText(rive::toSpan(m_unichars),
                                                        rive::toSpan(truns)));
         }
+
+        m_xform = rive::Mat2D::fromTranslate(10, 0)
+                * rive::Mat2D::fromScale(3, 3);
     }
 
     void draw(rive::Renderer* renderer, float width, const RenderFontGlyphRuns& gruns) {
         renderer->save();
-        renderer->translate(10, 0);
-
-        renderer->save();
-        renderer->scale(3, 3);
+        renderer->transform(m_xform);
 
         auto lines = rive::RenderGlyphLine::BreakLines(rive::toSpan(gruns), rive::toSpan(m_breaks), width);
 
@@ -173,30 +175,43 @@
         draw_line(&skiaFactory, renderer, width);
 
         renderer->restore();
-
-        renderer->restore();
     }
 
     void handleDraw(SkCanvas* canvas, double) override {
         rive::SkiaRenderer renderer(canvas);
 
-        static float width = 300;
-        static float dw = 1;
-        if (false) {
-            width += dw; if (width > 600) { dw = -dw; } if (width < 50) { dw = -dw; }
-        }
-
         for (auto& grun : m_gruns) {
-            this->draw(&renderer, width, grun);
+            this->draw(&renderer, m_width, grun);
             renderer.translate(1200, 0);
         }
 
     }
 
     void handleResize(int width, int height) override {}
-    void handleImgui() override {}
+    void handleImgui() override {
+        ImGui::Begin("text", nullptr);
+        ImGui::SliderFloat("Alignment", &m_width, 10, 400); 
+        ImGui::End();
+    }
 };
 
+static bool ends_width(const char str[], const char suffix[]) {
+    size_t ln = strlen(str);
+    size_t lx = strlen(suffix);
+    if (lx > ln) {
+        return false;
+    }
+    for (size_t i = 0; i < lx; ++i) {
+        if (str[ln - lx + i] != suffix[i]) {
+            return false;
+        }
+    }
+    return true;
+}
+
 std::unique_ptr<ViewerContent> ViewerContent::Text(const char filename[]) {
-    return std::make_unique<TextContent>();
+    if (ends_width(filename, ".svg")) {
+        return std::make_unique<TextContent>();
+    }
+    return nullptr;
 }
diff --git a/skia/viewer/src/textpath_content.cpp b/skia/viewer/src/textpath_content.cpp
new file mode 100644
index 0000000..b14b738
--- /dev/null
+++ b/skia/viewer/src/textpath_content.cpp
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer_content.hpp"
+
+#include "rive/refcnt.hpp"
+#include "rive/render_text.hpp"
+
+#include "contour_measure.hpp"
+#include "skia_factory.hpp"
+#include "skia_renderer.hpp"
+#include "line_breaker.hpp"
+#include "to_skia.hpp"
+
+using namespace rive;
+
+using RenderFontTextRuns = std::vector<RenderTextRun>;
+using RenderFontGlyphRuns = std::vector<RenderGlyphRun>;
+using RenderFontFactory = rcp<RenderFont> (*)(const Span<const uint8_t>);
+
+template <typename Handler>
+void visit(const std::vector<RenderGlyphRun>& gruns, Vec2D origin, Handler proc) {
+    for (const auto& gr : gruns) {
+        for (size_t i = 0; i < gr.glyphs.size(); ++i) {
+            auto path = gr.font->getPath(gr.glyphs[i]);
+            auto mx = Mat2D::fromTranslate(origin.x + gr.xpos[i], origin.y)
+                    * Mat2D::fromScale(gr.size, gr.size);
+            path.transformInPlace(mx);
+            proc(path);
+        }
+    }
+}
+
+static Vec2D ave(Vec2D a, Vec2D b) {
+    return (a + b) * 0.5f;
+}
+
+static RawPath make_quad_path(Span<const Vec2D> pts) {
+    const int N = pts.size();
+    RawPath path;
+    if (N >= 2) {
+        path.move(pts[0]);
+        if (N == 2) {
+            path.line(pts[1]);
+        } else if (N == 3) {
+            path.quad(pts[1], pts[2]);
+        } else {
+            for (int i = 1; i < N - 2; ++i) {
+                path.quad(pts[i], ave(pts[i], pts[i+1]));
+            }
+            path.quad(pts[N-2], pts[N-1]);
+        }
+    }
+    return path;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+
+#include "renderfont_skia.hpp"
+#include "renderfont_hb.hpp"
+#include "renderfont_coretext.hpp"
+
+static SkiaFactory skiaFactory;
+
+static std::unique_ptr<RenderPath> make_rpath(const RawPath& path) {
+    return skiaFactory.makeRenderPath(path.points(), path.verbsU8(), FillRule::nonZero);
+}
+
+static void stroke_path(Renderer* renderer, const RawPath& path, float size, ColorInt color) {
+    auto paint = skiaFactory.makeRenderPaint();
+    paint->color(color);
+    paint->thickness(size);
+    paint->style(RenderPaintStyle::stroke);
+    renderer->drawPath(make_rpath(path).get(), paint.get());
+}
+
+static void fill_rect(Renderer* renderer, const AABB& r, RenderPaint* paint) {
+    RawPath rp;
+    rp.addRect(r);
+    renderer->drawPath(make_rpath(rp).get(), paint);
+}
+
+static void fill_point(Renderer* renderer, Vec2D p, float r, RenderPaint* paint) {
+    fill_rect(renderer, {p.x - r, p.y - r, p.x + r, p.y + r}, paint);
+}
+
+typedef rcp<RenderFont> (*RenderFontFactory)(Span<const uint8_t>);
+
+static RenderTextRun append(std::vector<Unichar>* unichars, rcp<RenderFont> font,
+                            float size, const char text[]) {
+    uint32_t n = 0;
+    while (text[n]) {
+        unichars->push_back(text[n]);   // todo: utf8 -> unichar
+        n += 1;
+    }
+    return { std::move(font), size, n };
+}
+
+class TextPathContent : public ViewerContent {
+    std::vector<Unichar> m_unichars;
+    RenderFontGlyphRuns m_gruns;
+    std::unique_ptr<RenderPaint> m_paint;
+
+    std::vector<Vec2D> m_pathpts;
+    Vec2D m_lastPt = {0,0};
+    int m_trackingIndex = -1;
+    Mat2D m_trans;
+
+    float m_alignment = 0,
+          m_scaleY = 1,
+          m_offsetY = 0,
+          m_windowWidth = 1,    // %
+          m_windowOffset = 0;   // %
+
+    RenderFontTextRuns make_truns(RenderFontFactory fact) {
+        auto loader = [fact](const char filename[]) -> rcp<RenderFont> {
+            auto bytes = ViewerContent::LoadFile(filename);
+            if (bytes.size() == 0) {
+                assert(false);
+                return nullptr;
+            }
+            return fact(toSpan(bytes));
+        };
+
+        const char* fontFiles[] = {
+            "../../test/assets/RobotoFlex.ttf",
+            "../../test/assets/LibreBodoni-Italic-VariableFont_wght.ttf",
+        };
+
+        auto font0 = loader(fontFiles[0]);
+        auto font1 = loader(fontFiles[1]);
+        assert(font0);
+        assert(font1);
+
+        RenderFont::Coord c1 = {'wght', 100.f},
+                          c2 = {'wght', 800.f};
+
+        RenderFontTextRuns truns;
+
+        truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U"));
+        truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "neasy"));
+        truns.push_back(append(&m_unichars, font1, 30, " fits the crown"));
+        truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often"));
+        truns.push_back(append(&m_unichars, font0, 30, " lies the head."));
+
+        return truns;
+    }
+
+public:
+    TextPathContent() {
+        auto truns = this->make_truns(CoreTextRenderFont::Decode);
+        m_gruns = truns[0].font->shapeText(toSpan(m_unichars), toSpan(truns));
+
+        m_paint = skiaFactory.makeRenderPaint();
+        m_paint->color(0xFFFFFFFF);
+
+        m_pathpts.push_back({ 20, 300});
+        m_pathpts.push_back({220, 100});
+        m_pathpts.push_back({420, 500});
+        m_pathpts.push_back({620, 100});
+        m_pathpts.push_back({820, 300});
+
+        m_trans = Mat2D::fromTranslate(200, 200) * Mat2D::fromScale(2, 2);
+    }
+
+    void draw_warp(Renderer* renderer, const RawPath& warp) {
+        stroke_path(renderer, warp, 0.5, 0xFF00FF00);
+
+        auto paint = skiaFactory.makeRenderPaint();
+        paint->color(0xFF008800);
+        const float r = 4;
+        for (auto p : m_pathpts) {
+            fill_point(renderer, p, r, paint.get());
+        }
+    }
+
+    static size_t count_glyphs(const RenderFontGlyphRuns& gruns) {
+        size_t n = 0;
+        for (const auto& gr : gruns) {
+            n += gr.glyphs.size();
+        }
+        return n;
+    }
+
+    void modify(float amount) {
+        m_paint->color(0xFFFFFFFF); 
+    }
+
+    void draw(Renderer* renderer, const RenderFontGlyphRuns& gruns) {
+        auto get_path = [this](const RenderGlyphRun& run, int index, float dx) {
+            auto path = run.font->getPath(run.glyphs[index]);
+            path.transformInPlace(Mat2D::fromTranslate(run.xpos[index] + dx, m_offsetY)
+                                * Mat2D::fromScale(run.size, run.size * m_scaleY));
+            return path;
+        };
+
+        renderer->save();
+        renderer->transform(m_trans);
+
+        RawPath warp = make_quad_path(toSpan(m_pathpts));
+        this->draw_warp(renderer, warp);
+
+        ContourMeasure meas(warp);
+        const float warpLength = meas.length();
+        const float textLength = gruns.back().xpos.back();
+        const float offset = (warpLength - textLength) * m_alignment;
+
+        const size_t glyphCount = count_glyphs(gruns);
+        size_t glyphIndex = 0;
+        float windowEnd = m_windowOffset + m_windowWidth;
+
+        for (const auto& gr : gruns) {
+            for (size_t i = 0; i < gr.glyphs.size(); ++i) {
+                float percent = glyphIndex / (float)(glyphCount - 1);
+                float amount = (percent >= m_windowOffset && percent <= windowEnd);
+
+                float scaleY = m_scaleY;
+                m_paint->color(0xFF666666);
+                m_paint->style(RenderPaintStyle::fill);
+                if (amount > 0) {
+                    this->modify(amount);
+                }
+
+                auto path = meas.warp(get_path(gr, i, offset));
+                renderer->drawPath(make_rpath(path).get(), m_paint.get());
+                glyphIndex += 1;
+                m_scaleY = scaleY;
+            }
+        }
+        renderer->restore();
+    }
+
+    void handleDraw(SkCanvas* canvas, double) override {
+        SkiaRenderer renderer(canvas);
+
+       this->draw(&renderer, m_gruns);
+    }
+
+    void handlePointerMove(float x, float y) override {
+        m_lastPt = m_trans.invertOrIdentity() * Vec2D{x, y};
+        if (m_trackingIndex >= 0) {
+            m_pathpts[m_trackingIndex] = m_lastPt;
+        }
+    }
+    void handlePointerDown() override {
+        auto close_to = [](Vec2D a, Vec2D b) {
+            return Vec2D::distance(a, b) <= 10;
+        };
+        for (size_t i = 0; i < m_pathpts.size(); ++i) {
+            if (close_to(m_lastPt, m_pathpts[i])) {
+                m_trackingIndex = i;
+                break;
+            }
+        }
+    }
+
+    void handlePointerUp() override {
+        m_trackingIndex = -1;
+    }
+
+    void handleResize(int width, int height) override {}
+    
+    void handleImgui() override {
+        ImGui::Begin("path", nullptr);
+        ImGui::SliderFloat("Alignment", &m_alignment, -3, 4); 
+        ImGui::SliderFloat("Scale Y", &m_scaleY, 0.25f, 3.0f); 
+        ImGui::SliderFloat("Offset Y", &m_offsetY, -100, 100); 
+        ImGui::SliderFloat("Window Offset", &m_windowOffset, -1.1f, 1.1f); 
+        ImGui::SliderFloat("Window Width", &m_windowWidth, 0, 1.2f); 
+        ImGui::End();
+    }
+};
+
+std::unique_ptr<ViewerContent> ViewerContent::TextPath(const char filename[]) {
+    return std::make_unique<TextPathContent>();
+}
diff --git a/skia/viewer/src/viewer_content.hpp b/skia/viewer/src/viewer_content.hpp
index 3fd2697..a186911 100644
--- a/skia/viewer/src/viewer_content.hpp
+++ b/skia/viewer/src/viewer_content.hpp
@@ -27,7 +27,7 @@
 
     // Searches all handlers and returns a content if it is found.
     static std::unique_ptr<ViewerContent> FindHandler(const char filename[]) {
-        Factory factories[] = { Scene, Image, Text };
+        Factory factories[] = { Scene, Image, Text, TextPath };
         for (auto f : factories) {
             if (auto content = f(filename)) {
                 return content;
@@ -40,6 +40,7 @@
     static std::unique_ptr<ViewerContent> Scene(const char[]);
     static std::unique_ptr<ViewerContent> Image(const char[]);
     static std::unique_ptr<ViewerContent> Text(const char[]);
+    static std::unique_ptr<ViewerContent> TextPath(const char[]);
 
     static std::vector<uint8_t> LoadFile(const char path[]);
 };
diff --git a/src/math/raw_path.cpp b/src/math/raw_path.cpp
index 2461c3d..da05d50 100644
--- a/src/math/raw_path.cpp
+++ b/src/math/raw_path.cpp
@@ -165,3 +165,34 @@
         close();
     }
 }
+
+//////////////////////////////////////////////////////////////////////////
+
+namespace rive {
+int path_verb_to_point_count(PathVerb v) {
+    static uint8_t ptCounts[] = {
+        1,  // move
+        1,  // line
+        2,  // quad
+        2,  // conic (unused)
+        3,  // cubic
+        0,  // close
+    };
+    size_t index = (size_t)v;
+    assert(index < sizeof(ptCounts));
+    return ptCounts[index];
+}
+}
+
+RawPath::Iter::Rec RawPath::Iter::next() {
+    Rec rec = {nullptr, 0, PathVerb::move};
+
+    if (m_currVerb < m_stopVerb) {
+        rec.pts = m_currPts;
+        rec.verb = *m_currVerb++;
+        rec.count = path_verb_to_point_count(rec.verb);
+
+        m_currPts += rec.count;
+    }
+    return rec;
+}
diff --git a/test/raw_path_test.cpp b/test/raw_path_test.cpp
index 182fb12..e152351 100644
--- a/test/raw_path_test.cpp
+++ b/test/raw_path_test.cpp
@@ -75,3 +75,81 @@
         }
     }
 }
+
+//////////////////////////////////////////////////////////////////////////
+
+static bool is_move(const RawPath::Iter::Rec& rec) {
+    if (rec.verb == PathVerb::move) {
+        REQUIRE(rec.count == 1);
+        return true;
+    }
+    return false;
+}
+
+static bool is_line(const RawPath::Iter::Rec& rec) {
+    if (rec.verb == PathVerb::line) {
+        REQUIRE(rec.count == 1);
+        return true;
+    }
+    return false;
+}
+
+static bool is_quad(const RawPath::Iter::Rec& rec) {
+    if (rec.verb == PathVerb::quad) {
+        REQUIRE(rec.count == 2);
+        return true;
+    }
+    return false;
+}
+
+static bool is_cubic(const RawPath::Iter::Rec& rec) {
+    if (rec.verb == PathVerb::cubic) {
+        REQUIRE(rec.count == 3);
+        return true;
+    }
+    return false;
+}
+
+static bool is_close(const RawPath::Iter::Rec& rec) {
+    if (rec.verb == PathVerb::close) {
+        REQUIRE(rec.count == 0);
+        return true;
+    }
+    return false;
+}
+
+TEST_CASE("rawpath-iter", "[rawpath]") {
+    auto eq = [](Vec2D p, float x, float y) { return p.x == x && p.y == y; };
+
+    {
+        RawPath rp;
+        RawPath::Iter iter(rp);
+        REQUIRE(iter.next() == false);
+        REQUIRE(iter.next() == false);  // should be safe to call again
+    }
+    {
+        RawPath rp;
+        rp.moveTo(1, 2);
+        rp.lineTo(3, 4);
+        rp.quadTo(5, 6, 7, 8);
+        rp.cubicTo(9, 10, 11, 12, 13, 14);
+        rp.close();
+        RawPath::Iter iter(rp);
+        auto rec = iter.next();
+        REQUIRE((rec && is_move(rec) && eq(rec.pts[0], 1,2)));
+        rec = iter.next();
+        REQUIRE((rec && is_line(rec) && eq(rec.pts[0], 3,4)));
+        rec = iter.next();
+        REQUIRE((rec && is_quad(rec) && eq(rec.pts[0], 5,6) 
+                                     && eq(rec.pts[1], 7,8)));
+        rec = iter.next();
+        REQUIRE((rec && is_cubic(rec) && eq(rec.pts[0], 9,10) 
+                                      && eq(rec.pts[1], 11,12)
+                                      && eq(rec.pts[2], 13,14)));
+        rec = iter.next();
+        REQUIRE((rec && is_close(rec)));
+        rec = iter.next();
+        REQUIRE(rec == false);
+        REQUIRE(iter.next() == false);  // should be safe to call again
+    }
+}