mirror of https://github.com/Chlumsky/msdfgen.git
339 lines
12 KiB
C++
339 lines
12 KiB
C++
|
|
#include "import-svg.h"
|
|
|
|
#include <cstdio>
|
|
#include <tinyxml2.h>
|
|
|
|
#ifdef _WIN32
|
|
#pragma warning(disable:4996)
|
|
#endif
|
|
|
|
namespace msdfgen {
|
|
|
|
#define REQUIRE(cond) { if (!(cond)) return false; }
|
|
|
|
static bool readNodeType(char &output, const char *&pathDef) {
|
|
int shift;
|
|
char nodeType;
|
|
if (sscanf(pathDef, " %c%n", &nodeType, &shift) == 1 && nodeType != '+' && nodeType != '-' && nodeType != '.' && nodeType != ',' && (nodeType < '0' || nodeType > '9')) {
|
|
pathDef += shift;
|
|
output = nodeType;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool readCoord(Point2 &output, const char *&pathDef) {
|
|
int shift;
|
|
double x, y;
|
|
if (sscanf(pathDef, "%lf%lf%n", &x, &y, &shift) == 2) {
|
|
output.x = x;
|
|
output.y = y;
|
|
pathDef += shift;
|
|
return true;
|
|
}
|
|
if (sscanf(pathDef, "%lf,%lf%n", &x, &y, &shift) == 2) {
|
|
output.x = x;
|
|
output.y = y;
|
|
pathDef += shift;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool readDouble(double &output, const char *&pathDef) {
|
|
int shift;
|
|
double v;
|
|
if (sscanf(pathDef, "%lf%n", &v, &shift) == 1) {
|
|
pathDef += shift;
|
|
output = v;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool readBool(bool &output, const char *&pathDef) {
|
|
int shift;
|
|
int v;
|
|
if (sscanf(pathDef, "%d%n", &v, &shift) == 1) {
|
|
pathDef += shift;
|
|
output = static_cast<bool>(v);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void consumeOptionalComma(const char *&pathDef) {
|
|
while (*pathDef == ' ')
|
|
++pathDef;
|
|
if (*pathDef == ',')
|
|
++pathDef;
|
|
}
|
|
|
|
static double toDegrees(double rad) {
|
|
return rad * 180.0 / M_PI;
|
|
}
|
|
|
|
static double toRad(double degrees) {
|
|
return degrees * M_PI / 180.0;
|
|
}
|
|
|
|
static Point2 handleArc(const char *&pathDef, char nodeType, Point2 prevNode, Contour &contour) {
|
|
Point2 arcEnd;
|
|
double radiusX = 0;
|
|
double radiusY = 0;
|
|
double xAxisRotation = 0;
|
|
bool largeArc = false;
|
|
bool sweep = false;
|
|
|
|
// arc syntax
|
|
// A rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
|
// a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy
|
|
REQUIRE(readDouble(radiusX, pathDef));
|
|
radiusX = std::abs(radiusX);
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readDouble(radiusY, pathDef));
|
|
radiusY = std::abs(radiusY);
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readDouble(xAxisRotation, pathDef));
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readBool(largeArc, pathDef));
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readBool(sweep, pathDef));
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readCoord(arcEnd, pathDef));
|
|
if (nodeType == 'a') {
|
|
arcEnd += prevNode;
|
|
}
|
|
|
|
// Zero arc radius results in a straight line
|
|
if (radiusX == 0 || radiusY == 0) {
|
|
contour.addEdge(new LinearSegment(prevNode, arcEnd));
|
|
return arcEnd;
|
|
}
|
|
|
|
if (prevNode.x == arcEnd.x && prevNode.y == arcEnd.y) {
|
|
// End point matches previous node,
|
|
// which means it has a length of zero and can be ignored
|
|
return prevNode;
|
|
}
|
|
|
|
double xAxisRotationRad = toRad(std::fmod(xAxisRotation, 360.0));
|
|
double cosRotation = std::cos(xAxisRotationRad);
|
|
double sinRotation = std::sin(xAxisRotationRad);
|
|
Point2 mid = (prevNode - arcEnd) / 2.0;
|
|
|
|
// Compute transformed start point
|
|
double x1 = (cosRotation * mid.x + sinRotation * mid.y);
|
|
double y1 = (-sinRotation * mid.x + cosRotation * mid.y);
|
|
double radXsq = std::pow(radiusX, 2);
|
|
double radYsq = std::pow(radiusY, 2);
|
|
double x1sq = std::pow(x1, 2);
|
|
double y1sq = std::pow(y1, 2);
|
|
|
|
// Scale the radius if it is not big enough
|
|
double radiiCheck = x1sq / radXsq + y1sq / radYsq;
|
|
if (radiiCheck > 1) {
|
|
radiusX = std::sqrt(radiiCheck) * radiusX;
|
|
radiusY = std::sqrt(radiiCheck) * radiusY;
|
|
radXsq = std::pow(radiusX, 2);
|
|
radYsq = std::pow(radiusY, 2);
|
|
}
|
|
|
|
// Compute centre point
|
|
double sign = (largeArc == sweep) ? -1 : 1;
|
|
double sq = ((radXsq * radYsq) - (radXsq * y1sq) - (radYsq * x1sq)) / ((radXsq * y1sq) + (radYsq * x1sq));
|
|
sq = (sq < 0) ? 0 : sq;
|
|
double coefficient = sign * std::sqrt(sq);
|
|
double cx1 = coefficient * ((radiusX * y1) / radiusY);
|
|
double cy1 = coefficient * -((radiusY * x1) / radiusX);
|
|
Point2 sx = (prevNode + arcEnd) / 2.0;
|
|
double cx = sx.x + (cosRotation * cx1 - sinRotation * cy1);
|
|
double cy = sx.y + (sinRotation * cx1 + cosRotation * cy1);
|
|
Point2 center(cx, cy);
|
|
|
|
// Compute the angle start
|
|
double ux = (x1 - cx1) / radiusX;
|
|
double uy = (y1 - cy1) / radiusY;
|
|
double n = std::sqrt((ux * ux) + (uy * uy));
|
|
double p = ux;
|
|
sign = (uy < 0) ? -1.0 : 1.0;
|
|
double angleStart = toDegrees(sign * std::acos(p / n));
|
|
|
|
// Compute the angle extent
|
|
double vx = (-x1 - cx1) / radiusX;
|
|
double vy = (-y1 - cy1) / radiusY;
|
|
n = std::sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
|
|
p = ux * vx + uy * vy;
|
|
sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
|
|
double angleExtent = toDegrees(sign * std::acos(p / n));
|
|
if (!sweep && angleExtent > 0) {
|
|
angleExtent -= 360;
|
|
} else if (sweep && angleExtent < 0) {
|
|
angleExtent += 360;
|
|
}
|
|
|
|
// generate bezier curves for a unit circle that covers the given arc angles
|
|
int segmentCount = static_cast<int>(std::ceil(std::abs(angleExtent) / 90.0));
|
|
angleStart = toRad(std::fmod(angleStart, 360.0));
|
|
angleExtent = toRad(std::fmod(angleExtent, 360.0));
|
|
double angleIncrement = angleExtent / segmentCount;
|
|
double controlLength = 4.0 / 3.0 * std::sin(angleIncrement / 2.0) / (1.0 + std::cos(angleIncrement / 2.0));
|
|
|
|
Point2 startPoint = prevNode;
|
|
Point2 bezEndpoint;
|
|
Point2 controlPoint[2];
|
|
for (int i = 0; i < segmentCount; i++) {
|
|
double angle = angleStart + i * angleIncrement;
|
|
double dx = std::cos(angle);
|
|
double dy = std::sin(angle);
|
|
controlPoint[0].x = (dx - controlLength * dy) * radiusX;
|
|
controlPoint[0].y = (dy + controlLength * dx) * radiusY;
|
|
controlPoint[0] = controlPoint[0].rotate(xAxisRotation) + center;
|
|
angle += angleIncrement;
|
|
dx = std::cos(angle);
|
|
dy = std::sin(angle);
|
|
controlPoint[1].x = (dx + controlLength * dy) * radiusX;
|
|
controlPoint[1].y = (dy - controlLength * dx) * radiusY;
|
|
controlPoint[1] = controlPoint[1].rotate(xAxisRotation) + center;
|
|
bezEndpoint.x = dx * radiusX;
|
|
bezEndpoint.y = dy * radiusY;
|
|
bezEndpoint = bezEndpoint.rotate(xAxisRotation);
|
|
bezEndpoint = bezEndpoint + center;
|
|
if (i == segmentCount - 1) {
|
|
// to prevent rounding errors
|
|
bezEndpoint = arcEnd;
|
|
}
|
|
|
|
contour.addEdge(new CubicSegment(startPoint, controlPoint[0], controlPoint[1], bezEndpoint));
|
|
startPoint = bezEndpoint;
|
|
}
|
|
|
|
return arcEnd;
|
|
}
|
|
|
|
static bool buildFromPath(Shape &shape, const char *pathDef) {
|
|
char nodeType;
|
|
Point2 prevNode(0, 0);
|
|
while (readNodeType(nodeType, pathDef)) {
|
|
Contour &contour = shape.addContour();
|
|
bool contourStart = true;
|
|
|
|
Point2 startPoint;
|
|
Point2 controlPoint[2];
|
|
Point2 node;
|
|
|
|
while (true) {
|
|
switch (nodeType) {
|
|
case 'M': case 'm':
|
|
REQUIRE(contourStart);
|
|
REQUIRE(readCoord(node, pathDef));
|
|
if (nodeType == 'm')
|
|
node += prevNode;
|
|
startPoint = node;
|
|
--nodeType; // to 'L' or 'l'
|
|
break;
|
|
case 'Z': case 'z':
|
|
if (prevNode != startPoint)
|
|
contour.addEdge(new LinearSegment(prevNode, startPoint));
|
|
prevNode = startPoint;
|
|
goto NEXT_CONTOUR;
|
|
case 'L': case 'l':
|
|
REQUIRE(readCoord(node, pathDef));
|
|
if (nodeType == 'l')
|
|
node += prevNode;
|
|
contour.addEdge(new LinearSegment(prevNode, node));
|
|
break;
|
|
case 'H': case 'h':
|
|
REQUIRE(readDouble(node.x, pathDef));
|
|
if (nodeType == 'h')
|
|
node.x += prevNode.x;
|
|
contour.addEdge(new LinearSegment(prevNode, node));
|
|
break;
|
|
case 'V': case 'v':
|
|
REQUIRE(readDouble(node.y, pathDef));
|
|
if (nodeType == 'v')
|
|
node.y += prevNode.y;
|
|
contour.addEdge(new LinearSegment(prevNode, node));
|
|
break;
|
|
case 'Q': case 'q':
|
|
REQUIRE(readCoord(controlPoint[0], pathDef));
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readCoord(node, pathDef));
|
|
if (nodeType == 'q') {
|
|
controlPoint[0] += prevNode;
|
|
node += prevNode;
|
|
}
|
|
contour.addEdge(new QuadraticSegment(prevNode, controlPoint[0], node));
|
|
break;
|
|
// TODO T, t
|
|
case 'C': case 'c':
|
|
REQUIRE(readCoord(controlPoint[0], pathDef));
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readCoord(controlPoint[1], pathDef));
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readCoord(node, pathDef));
|
|
if (nodeType == 'c') {
|
|
controlPoint[0] += prevNode;
|
|
controlPoint[1] += prevNode;
|
|
node += prevNode;
|
|
}
|
|
contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node));
|
|
break;
|
|
case 'S': case 's':
|
|
controlPoint[0] = node+node-controlPoint[1];
|
|
REQUIRE(readCoord(controlPoint[1], pathDef));
|
|
consumeOptionalComma(pathDef);
|
|
REQUIRE(readCoord(node, pathDef));
|
|
if (nodeType == 's') {
|
|
controlPoint[1] += prevNode;
|
|
node += prevNode;
|
|
}
|
|
contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node));
|
|
break;
|
|
case 'A': case 'a':
|
|
node = handleArc(pathDef, nodeType, prevNode, contour);
|
|
break;
|
|
default:
|
|
REQUIRE(false);
|
|
}
|
|
contourStart &= nodeType == 'M' || nodeType == 'm';
|
|
prevNode = node;
|
|
readNodeType(nodeType, pathDef);
|
|
}
|
|
NEXT_CONTOUR:;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool loadSvgShape(Shape &output, const char *filename, Vector2 *dimensions) {
|
|
tinyxml2::XMLDocument doc;
|
|
if (doc.LoadFile(filename))
|
|
return false;
|
|
tinyxml2::XMLElement *root = doc.FirstChildElement("svg");
|
|
if (!root)
|
|
return false;
|
|
|
|
tinyxml2::XMLElement *path = root->FirstChildElement("path");
|
|
if (!path) {
|
|
tinyxml2::XMLElement *g = root->FirstChildElement("g");
|
|
if (g)
|
|
path = g->FirstChildElement("path");
|
|
}
|
|
if (!path)
|
|
return false;
|
|
const char *pd = path->Attribute("d");
|
|
if (!pd)
|
|
return false;
|
|
|
|
output.contours.clear();
|
|
output.inverseYAxis = true;
|
|
if (dimensions) {
|
|
dimensions->x = root->DoubleAttribute("width");
|
|
dimensions->y = root->DoubleAttribute("height");
|
|
}
|
|
return buildFromPath(output, pd);
|
|
}
|
|
|
|
}
|