Skip to content

Instantly share code, notes, and snippets.

@0smr
Last active May 9, 2023 19:51
Show Gist options
  • Select an option

  • Save 0smr/2e595c63bdd5b17a1c75efe9df4ef8fe to your computer and use it in GitHub Desktop.

Select an option

Save 0smr/2e595c63bdd5b17a1c75efe9df4ef8fe to your computer and use it in GitHub Desktop.

Revisions

  1. 0smr revised this gist May 9, 2023. No changes.
  2. 0smr created this gist May 9, 2023.
    12 changes: 12 additions & 0 deletions README.md
    Original 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;
    }
    ```
    134 changes: 134 additions & 0 deletions arcToCubic.h
    Original 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 &center) {
    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;
    }