kanjivg-tools/Libraries/Kanimaji/Source/SVG.cpp
2025-06-08 14:50:26 +02:00

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);
}
}
}