398 lines
13 KiB
C++
398 lines
13 KiB
C++
#include "Kanimaji/Error.hpp"
|
|
#include "SVG.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <charconv>
|
|
#include <cmath>
|
|
#include <format>
|
|
#include <memory>
|
|
|
|
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 <typename T>
|
|
T Squared(T val)
|
|
{
|
|
return val * val;
|
|
}
|
|
|
|
template <typename T>
|
|
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<Path::Element>;
|
|
|
|
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<Move>(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<double>(i) / static_cast<double>(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<CubicBezier>(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<CubicBezier>(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<ElementPtr>& 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<CubicBezier *>(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);
|
|
}
|
|
}
|
|
}
|