diff --git a/core/Vector2.cpp b/core/Vector2.cpp index 896963f..900f718 100644 --- a/core/Vector2.cpp +++ b/core/Vector2.cpp @@ -49,6 +49,23 @@ Vector2 Vector2::project(const Vector2 &vector, bool positive) const { return t*n; } +Vector2 Vector2::rotateAround(const Vector2 ¢er, double angleDegree) const { + Vector2 result = *this - center; + double s = std::sin(angleDegree * M_PI / 180); + double c = std::cos(angleDegree * M_PI / 180); + result.x = result.x * c - result.y * s; + result.y = result.x * s + result.y * c; + return result + center; +} + +Vector2 Vector2::rotate(double angleDegree) const { + double s = std::sin(angleDegree * M_PI / 180); + double c = std::cos(angleDegree * M_PI / 180); + double newX = x * c - y * s; + double newY = x * s + y * c; + return Vector2(newX, newY); +} + Vector2::operator const void*() const { return x || y ? this : NULL; } diff --git a/core/Vector2.h b/core/Vector2.h index 47ca637..67e7e16 100644 --- a/core/Vector2.h +++ b/core/Vector2.h @@ -33,6 +33,10 @@ struct Vector2 { Vector2 getOrthonormal(bool polarity = true, bool allowZero = false) const; /// Returns a vector projected along this one. Vector2 project(const Vector2 &vector, bool positive = false) const; + /// Returns a vector that is rotated by the angle around a center + Vector2 rotateAround(const Vector2 ¢er, double angleDegree) const; + /// Returns a vector that is rotated by the angle around the origin + Vector2 rotate(double angleDegree) const; operator const void *() const; bool operator!() const; bool operator==(const Vector2 &other) const; diff --git a/ext/import-svg.cpp b/ext/import-svg.cpp index 6ae3134..8738825 100644 --- a/ext/import-svg.cpp +++ b/ext/import-svg.cpp @@ -52,6 +52,17 @@ static bool readDouble(double &output, const char *&pathDef) { 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(v); + return true; + } + return false; +} + static void consumeOptionalComma(const char *&pathDef) { while (*pathDef == ' ') ++pathDef; @@ -59,6 +70,148 @@ static void consumeOptionalComma(const char *&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(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); @@ -138,7 +291,9 @@ static bool buildFromPath(Shape &shape, const char *pathDef) { } contour.addEdge(new CubicSegment(prevNode, controlPoint[0], controlPoint[1], node)); break; - // TODO A, a + case 'A': case 'a': + node = handleArc(pathDef, nodeType, prevNode, contour); + break; default: REQUIRE(false); }