Last active
May 9, 2023 19:51
-
-
Save 0smr/2e595c63bdd5b17a1c75efe9df4ef8fe to your computer and use it in GitHub Desktop.
Revisions
-
0smr revised this gist
May 9, 2023 . No changes.There are no files selected for viewing
-
0smr created this gist
May 9, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,12 @@ A _C++/Qt_ implementation of the SVG arc to cubic curve converter, based on the [svgpath](https://github.com/fontello/svgpath/blob/master/lib/a2c.js) repository. ### Usage ```c++ // A 10 10 0 0 0 14 0 auto curves = arcToCubic({0,0}, {10, 0}, {10, 10}, 0, 0, 0); for(auto curve: curves) { qDebug() << curve.c1 << ' ' << curve.c2 << ' ' << curve.to; } ``` This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,134 @@ // Copyright (C) 2022 smr. // SPDX-License-Identifier: MIT // https://smr.best // A C++/Qt implementation of the SVG arc to cubic curve converter, based on the svgpath repository. // svgpath: https://github.com/fontello/svgpath/blob/master/lib/a2c.js. #include <QPointF> #include <QVector> #include <cmath> struct cubicCurve { QPointF to, c1, c2; }; int sign(qreal v) { return std::signbit(v) ? -1 : 1; } QPointF mapToEllipse(const QPointF& point, const QPointF &radius, qreal cosphi, qreal sinphi, const QPointF ¢er) { qreal x = point.x() * radius.x(), y = point.y() * radius.y(); return QPointF{cosphi * x - sinphi * y + center.x(), sinphi * x + cosphi * y + center.y()}; } QVector<QPointF> approxUnitArc(qreal ang1, qreal ang2) { // If 90 degree circular arc, use a constant // as derived from http://spencermortensen.com/articles/bezier-circle qreal a = qFuzzyCompare(std::abs(ang2), 1.5707963267948966) ? sign(ang2) * 0.551915024494 : 4 / 3 * tan(ang2 / 4); qreal x1 = cos(ang1), y1 = sin(ang1); qreal x2 = cos(ang1 + ang2), y2 = sin(ang1 + ang2); return { {x1 - y1 * a, y1 + x1 * a}, {x2 + y2 * a, y2 - x2 * a}, {x2, y2 } }; } qreal vectorAngle(QPointF u, QPointF v) { qreal sign = (u.x() * v.y() - u.y() * v.x() < 0) ? -1.0 : 1.0; qreal dot = std::clamp(QPointF::dotProduct(u, v), -1.0, 1.0); return sign * acos(dot); } QVector<double> getArcCenter(QPointF from, QPointF to, QPointF radius, bool largeArcFlag, bool sweepFlag, double sinphi, double cosphi, QPointF pp) { const double TAU = M_PI * 2; const double rxsq = radius.x() * radius.x(); const double rysq = radius.y() * radius.y(); const double pxpsq = pp.x() * pp.x(); const double pypsq = pp.y() * pp.y(); double radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq); if(radicant < 0) { radicant = 0; } radicant /= (rxsq * pypsq) + (rysq * pxpsq); radicant = std::sqrt(radicant) * (largeArcFlag == sweepFlag ? -1 : 1); const QPointF ctrp{radicant * radius.x() / radius.y() * pp.y(), radicant * -radius.y() / radius.x() * pp.x()}; const QPointF center { cosphi * ctrp.x() - sinphi * ctrp.y() + (from.x() + to.x()) / 2, sinphi * ctrp.x() + cosphi * ctrp.y() + (from.y() + to.y()) / 2 }; QPointF v1{(pp.x() - ctrp.x()) / radius.x(), (pp.y() - ctrp.y()) / radius.y()}, v2{(-pp.x() - ctrp.x()) / radius.x(), (-pp.y() - ctrp.y()) / radius.y()}; double ang1 = vectorAngle({1, 0}, v1); double ang2 = vectorAngle(v1, v2); if(sweepFlag == 0 && ang2 > 0) { ang2 -= TAU; } if(sweepFlag == 1 && ang2 < 0) { ang2 += TAU; } return {center.x(), center.y(), ang1, ang2}; } QVector<cubicCurve> arcToCubic(QPointF from, QPointF to, QPointF radius, double rotation, bool largeArcFlag, bool sweepFlag) { // 2 * PI or 2π is colloquially referred to tau or τ constexpr double tau = M_PI * 2; double phi = rotation * tau / 360; if(radius.x() == 0 || radius.y() == 0) { return {}; } const double sinphi = sin(phi); const double cosphi = cos(phi); const double pxp = cosphi * (from.x() - to.x()) / 2 + sinphi * (from.y() - to.y()) / 2; const double pyp = -sinphi * (from.x() - to.x()) / 2 + cosphi * (from.y() - to.y()) / 2; if(pxp == 0 && pyp == 0) { return {}; } radius = QPointF{abs(radius.x()), abs(radius.y())}; double lambda = (pxp * pxp) / (radius.x() * radius.x()) + (pyp * pyp) / (radius.y() * radius.y()); if(lambda > 1) radius *= std::sqrt(lambda); QVector<double> centerAngles = getArcCenter(from, to, radius, largeArcFlag, sweepFlag, sinphi, cosphi, {pxp, pyp}); QPointF center {centerAngles[0], centerAngles[1]}; double ang1 = centerAngles[2]; double ang2 = centerAngles[3]; // If 'ang2' == 90.0000000001, then `ratio` will evaluate to 1.0000000001. // This causes `segments` to be greater than one, which is an unecessary split, // and adds extra points to the bezier curve. To alleviate this issue, // we round to 1.0 when the ratio is close to 1.0. double ratio = abs(ang2) / (tau / 4); if(std::abs(1.0 - ratio) < 0.0000001) { ratio = 1.0; } int segments = std::max(int(ceil(ratio)), 1); ang2 /= segments; QVector<QVector<QPointF>> curves; QVector<cubicCurve> results; for(int i = 0; i < segments; i++) { curves.push_back(approxUnitArc(ang1, ang2)); ang1 += ang2; } for(auto curve: curves) { results.push_back({ mapToEllipse(curve[0], radius, cosphi, sinphi, center), // c1 mapToEllipse(curve[1], radius, cosphi, sinphi, center), // c2 mapToEllipse(curve[2], radius, cosphi, sinphi, center) // to }); } return results; }