#include "Kanimaji/Error.hpp" #include "SVG.hpp" #include #include #include #include #include #include namespace Kanimaji::SVG { struct Vec2 { double x; double y; double Length() const { return std::sqrt(x * x + y * y); } friend Vec2 operator+(const Vec2& u, const Vec2& v) { return Vec2 { .x = u.x + v.x, .y = u.y + v.y, }; } friend Vec2 operator-(const Vec2& u, const Vec2& v) { return Vec2 { .x = u.x - v.x, .y = u.y - v.y, }; } friend Vec2 operator-(const Vec2& u) { return Vec2 { .x = -u.x, .y = -u.y, }; } friend Vec2 operator*(const Vec2& u, double s) { return Vec2 { .x = u.x * s, .y = u.y * s, }; } friend Vec2 operator*(double s, const Vec2& u) { return Vec2 { .x = s * u.x, .y = s * u.y, }; } }; template T Squared(T val) { return val * val; } template T Cubed(T val) { return val * val * val; } class Path::Element { public: enum class Type { Move, CubicBezier, }; Element(Type type) : mType(type) {} virtual ~Element() = default; Type GetType() const { return mType; } virtual double Length(Vec2 origin) const = 0; virtual Vec2 Endpoint(Vec2 origin) const = 0; virtual void Serialize(std::string& out) const = 0; protected: Type mType; }; namespace { void SkipWhitespace(std::string_view source, std::size_t& current) { while (current < source.size() && std::isspace(source[current])) { current++; } } void SkipCommaWhitespace(std::string_view source, std::size_t& current) { SkipWhitespace(source, current); if (current < source.size() && source[current] == ',') { current++; } SkipWhitespace(source, current); } double ConvertNumber(std::size_t position, std::string_view raw) { double result = 0; auto [ptr, ec] = std::from_chars(raw.data(), raw.data() + raw.size(), result); if (ec == std::errc::invalid_argument) { throw ParseError(position, "This is not a number"); } else if (ec == std::errc::result_out_of_range) { throw ParseError(position, "This number cannot fit in a double"); } else if (ec != std::errc{}) { throw ParseError(position, "Unknown error"); } else if (ptr != raw.data() + raw.size()) { throw ParseError(position, "The number does not reach the end of the range"); } return result; } double ParseNumber(std::string_view source, std::size_t& current) { constexpr auto isNumeric = [] (char ch) { return std::isdigit(ch) || ch == '.'; }; std::size_t begin = current; if (current < source.size() && source[current] == '-') { current++; } while (current < source.size() && isNumeric(source[current])) { current++; } if (current - begin == 0) { throw ParseError(current, "Numbers cannot be zero width"); } return ConvertNumber(begin, { source.data() + begin, current - begin }); } Vec2 ParseVector(std::string_view source, std::size_t& current) { double x = ParseNumber(source, current); SkipCommaWhitespace(source, current); if (current >= source.size()) { throw ParseError(current, "Unexpected input end, a 2D vector needs two numbers"); } double y = ParseNumber(source, current); SkipCommaWhitespace(source, current); return { .x = x, .y = y, }; } using ElementPtr = std::unique_ptr; class Move : public Path::Element { public: Move(bool relative, Vec2 move) : Element(Type::Move), mRelative(relative), mMove(move) {} double Length(Vec2 origin) const override { return 0.0; } Vec2 Endpoint(Vec2 origin) const override { return mRelative ? origin + mMove : mMove; } static ElementPtr Parse(std::string_view source, std::size_t& current) { constexpr auto verify = [] (char ch) { return ch == 'M' || ch == 'm'; }; if (current >= source.size() || !verify(source[current])) { throw ParseError(current, "Invalid move start"); } bool relative = std::islower(source[current]); current++; SkipWhitespace(source, current); return std::make_unique(relative, ParseVector(source, current)); } void Serialize(std::string& out) const override { out.append(std::format("{}{:.3},{:.3}", mRelative ? 'm' : 'M', mMove.x, mMove.y)); } private: bool mRelative; // 7B padding, oof Vec2 mMove; }; class CubicBezier : public Path::Element { public: CubicBezier(bool relative, Vec2 control1, Vec2 control2, Vec2 end) : Element(Type::CubicBezier) , mRelative(relative) , mControl1(control1) , mControl2(control2) , mEnd(end) {} Vec2 Interpolate(Vec2 origin, double t) const { t = std::clamp(t, 0.0, 1.0); Vec2 control1 = mRelative ? origin + mControl1 : mControl1; Vec2 control2 = mRelative ? origin + mControl2 : mControl2; Vec2 end = mRelative ? origin + mEnd : mEnd; return origin * Cubed(1.0 - t) + 3.0 * control1 * Squared(1.0 - t) * t + 3.0 * control2 * (1.0 - t) * Squared(t) + end * Cubed(t); } double Length(Vec2 origin) const override { constexpr std::int32_t precision = 128; double length = 0.0; Vec2 previous = origin; Vec2 current = origin; for (std::int32_t i = 1; i <= precision; ++i) { double t = static_cast(i) / static_cast(precision); current = Interpolate(origin, t); length += (previous - current).Length(); previous = current; } return length; } Vec2 Endpoint(Vec2 origin) const override { return mRelative ? origin + mEnd : mEnd; } Vec2 ReflectControl2(const Vec2& origin, bool relative) const { Vec2 control2 = mRelative ? origin + mControl2 : mControl2; Vec2 end = mRelative ? origin + mEnd : mEnd; Vec2 reflected = relative ? end - control2 : 2.0 * end - control2; return reflected; } static ElementPtr Parse(std::string_view source, std::size_t& current) { constexpr auto verify = [] (char ch) { return ch == 'C' || ch == 'c'; }; if (current >= source.size() || !verify(source[current])) { throw ParseError(current, "Invalid Cubic Bezier start"); } bool relative = std::islower(source[current]); current++; SkipWhitespace(source, current); Vec2 control1 = ParseVector(source, current); SkipCommaWhitespace(source, current); Vec2 control2 = ParseVector(source, current); SkipCommaWhitespace(source, current); Vec2 end = ParseVector(source, current); SkipWhitespace(source, current); return std::make_unique(relative, control1, control2, end); } static ElementPtr ParseExtension(std::string_view source, std::size_t& current, const Vec2& previousEnd, const CubicBezier& previous) { constexpr auto verify = [] (char ch) { return ch == 'S' || ch == 's'; }; if (current >= source.size() || !verify(source[current])) { throw ParseError(current, "Invalid Cubic Bezier Extension start"); } bool relative = std::islower(source[current]); current++; SkipWhitespace(source, current); Vec2 control1 = previous.ReflectControl2(previousEnd, relative); Vec2 control2 = ParseVector(source, current); SkipCommaWhitespace(source, current); Vec2 end = ParseVector(source, current); SkipWhitespace(source, current); return std::make_unique(relative, control1, control2, end); } void Serialize(std::string& out) const override { out.append(std::format("{}{:.3},{:.3},{:.3},{:.3},{:.3},{:.3}", mRelative ? 'c' : 'C', mControl1.x, mControl1.y, mControl2.x, mControl2.y, mEnd.x, mEnd.y)); } private: bool mRelative; // 7B padding, oof Vec2 mControl1; Vec2 mControl2; Vec2 mEnd; }; void Parse(std::string_view definition, std::vector& elements) { std::size_t current = 0; Vec2 previousEnd = { .x = 0.0, .y = 0.0, }; Vec2 currentEnd = { .x = 0.0, .y = 0.0, }; while (current < definition.size()) { SkipWhitespace(definition, current); switch (definition[current]) { case 'M': [[fallthrough]]; case 'm': elements.push_back(Move::Parse(definition, current)); break; case 'C': [[fallthrough]]; case 'c': elements.push_back(CubicBezier::Parse(definition, current)); break; case 'S': [[fallthrough]]; case 's': { if (elements.empty()) { throw ParseError(current, "Cubic Bezier Extensions without predecessors " "are unsupported"); } if (elements.back()->GetType() != Path::Element::Type::CubicBezier) { throw ParseError(current, "The predecessor of this Cubic Bezier Extension " "is not a Cubic Bezier itself"); } CubicBezier *previous = dynamic_cast(elements.back().get()); if (!previous) { throw ParseError(current, "The previous element could not be cast to a " "Cubic Bezier for some reason"); } elements.push_back(CubicBezier::ParseExtension(definition, current, previousEnd, *previous)); break; } default: throw ParseError(current, "Unrecognized character"); } previousEnd = currentEnd; currentEnd = elements.back()->Endpoint(currentEnd); } } } Path::Path(std::string_view definition) { Parse(definition, mSegments); } Path::~Path() = default; double Path::Length() const { Vec2 origin { .x = 0.0, .y = 0.0, }; double length = 0.0; for (const auto& segment : mSegments) { length += segment->Length(origin); origin = segment->Endpoint(origin); } return length; } void Path::Serialize(std::string& out) const { for (const auto& segment : mSegments) { segment->Serialize(out); } } }