Initial working Kanimaji implementation
This commit is contained in:
parent
4e87a67296
commit
99901b878a
8 changed files with 588 additions and 83 deletions
|
@ -1,14 +1,29 @@
|
|||
#include "SVG.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
|
||||
namespace Kanimaji::SVG
|
||||
{
|
||||
ParseError::ParseError(std::size_t current, std::string_view message)
|
||||
: Error(std::format("[At: {}] {}", current, message))
|
||||
, mPosition(current)
|
||||
{}
|
||||
|
||||
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 {
|
||||
|
@ -16,43 +31,187 @@ namespace Kanimaji::SVG
|
|||
.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
|
||||
{
|
||||
class ParseError : public Error
|
||||
void SkipWhitespace(std::string_view source, std::size_t& current)
|
||||
{
|
||||
public:
|
||||
ParseError(std::size_t current, std::string_view message)
|
||||
: Error(std::format("[At: {}] {}", current, message))
|
||||
{}
|
||||
};
|
||||
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)
|
||||
: mRelative(relative), mMove(move)
|
||||
: Element(Type::Move), mRelative(relative), mMove(move)
|
||||
{}
|
||||
|
||||
double Length(Vec2 origin) const override
|
||||
{
|
||||
return 0;
|
||||
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;
|
||||
|
@ -62,17 +221,107 @@ namespace Kanimaji::SVG
|
|||
{
|
||||
public:
|
||||
CubicBezier(bool relative, Vec2 control1, Vec2 control2, Vec2 end)
|
||||
: mRelative(relative), mControl1(control1), mControl2(control2), mEnd(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
|
||||
{
|
||||
throw Error("not implemented");
|
||||
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 mEnd;
|
||||
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
|
||||
|
@ -81,24 +330,47 @@ namespace Kanimaji::SVG
|
|||
Vec2 mEnd;
|
||||
};
|
||||
|
||||
using ElementPtr = std::unique_ptr<Path::Element>;
|
||||
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':
|
||||
// ParseMove
|
||||
elements.push_back(Move::Parse(definition, current));
|
||||
break;
|
||||
case 'C': [[fallthrough]];
|
||||
case 'c':
|
||||
// ParseCubicBezier
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +384,19 @@ namespace Kanimaji::SVG
|
|||
|
||||
double Path::Length() const
|
||||
{
|
||||
return 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue