/************************************************************************* ** EllipticalArc.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2024 Martin Gieseking ** ** ** ** This program is free software; you can redistribute it and/or ** ** modify it under the terms of the GNU General Public License as ** ** published by the Free Software Foundation; either version 3 of ** ** the License, or (at your option) any later version. ** ** ** ** This program is distributed in the hope that it will be useful, but ** ** WITHOUT ANY WARRANTY; without even the implied warranty of ** ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** ** GNU General Public License for more details. ** ** ** ** You should have received a copy of the GNU General Public License ** ** along with this program; if not, see . ** *************************************************************************/ #include #include "EllipticalArc.hpp" #include "utility.hpp" using namespace std; /** Constructs an elliptical arc from end point parameterization. * @param[in] start start point of arc * @param[in] rx length of semi-major axis * @param[in] ry length of semi-minor axis * @param[in] angle rotation of ellipse around its center (in radians) * @param[in] laf if true, choose the larger arc between start and end point * @param[in] sweep if true, arc is drawn in the direction of increasing angles * @param[in] end end point of the arc */ EllipticalArc::EllipticalArc (const DPair &start, double rx, double ry, double angle, bool laf, bool sweep, const DPair &end) : _rx(abs(rx)), _ry(abs(ry)), _rotationAngle(math::normalize_angle(angle, math::PI)), _largeArc(laf), _sweepPositive(sweep), _startPoint(start), _endPoint(end) { if (!isStraightLine()) { // fix out-of-range radii according to section F.6.6.3 in // https://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii double c = cos(_rotationAngle); double s = sin(_rotationAngle); DPair p = (_startPoint-_endPoint)/2.0; p = DPair(c*p.x()+s*p.y(), c*p.y()-s*p.x()); double lambda = (p.x()*p.x())/(_rx*_rx) + (p.y()*p.y())/(_ry*_ry); if (lambda > 1) { lambda = sqrt(lambda); _rx *= lambda; _ry *= lambda; } } } /** Constructs an elliptical arc from center parameterization * @param[in] center absolute coordinates of the center of the ellipse * @param[in] rx length of semi-major axis * @param[in] ry length of semi-minor axis * @param[in] rot rotation of ellipse around its center (in radians) * @param[in] startAngle angle between major axis and vector from center to start point * @param[in] deltaAngle angle between the vectors from center to start and end point, respectively */ EllipticalArc::EllipticalArc (const DPair ¢er, double rx, double ry, double rot, double startAngle, double deltaAngle) : _rx(rx), _ry(ry), _rotationAngle(math::normalize_angle(rot, math::TWO_PI)), _largeArc(abs(deltaAngle) > math::PI), _sweepPositive(deltaAngle > 0) { // https://www.w3.org/TR/SVG/implnote.html#ArcConversionCenterToEndpoint double c = cos(_rotationAngle); double s = sin(_rotationAngle); double c1 = cos(startAngle); double s1 = sin(startAngle); double c2 = cos(startAngle+deltaAngle); double s2 = sin(startAngle+deltaAngle); _startPoint = DPair(c*rx*c1*c - s*ry*s1, s*rx*c1 + c*ry*s1) + center; _endPoint = DPair(c*rx*c2*c - s*ry*s2, s*rx*c2 + c*ry*s2) + center; } /** Returns the angle between (1, 0) and a given vector. * The angle is normalized to the range [0, 2pi). */ static inline double angle (const DPair &p) { return math::normalize_0_2pi(atan2(p.y(), p.x())); } /** Computes the center parameterization of the arc. */ EllipticalArc::CenterParams EllipticalArc::getCenterParams () const { EllipticalArc::CenterParams params; if (isStraightLine()) { params.center = (_endPoint-_startPoint)/2.0; params.startAngle = params.deltaAngle = 0; } else { // https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter double c = cos(_rotationAngle); double s = sin(_rotationAngle); DPair p = (_startPoint-_endPoint)/2.0; p = DPair(c*p.x()+s*p.y(), c*p.y()-s*p.x()); double rx2 = _rx*_rx, ry2 = _ry*_ry; double px2 = p.x()*p.x(), py2 = p.y()*p.y(); double radicand = rx2*ry2 - rx2*py2 - ry2*px2; if (radicand < 0) // should not happen if out-of-range radii were fixed correctly radicand = 0; else radicand /= rx2*py2 + ry2*px2; double root = sqrt(radicand) * (_largeArc == _sweepPositive ? -1 : 1); DPair cp(root*p.y()*_rx/_ry, -root*p.x()*_ry/_rx); DPair mid = (_startPoint+_endPoint)/2.0; params.center = DPair(c*cp.x() - s*cp.y() + mid.x(), s*cp.x() + c*cp.y() + mid.y()); DPair q1((p.x() - cp.x())/_rx, (p.y() - cp.y())/_ry); DPair q2(-(p.x() + cp.x())/_rx, -(p.y() + cp.y())/_ry); params.startAngle = angle(q1); params.deltaAngle = angle(q2) - params.startAngle; if (_sweepPositive && params.deltaAngle < 0) params.deltaAngle += math::TWO_PI; else if (!_sweepPositive && params.deltaAngle > 0) params.deltaAngle -= math::TWO_PI; } return params; } /** Applies the affine transformation described by a given matrix to the arc. */ void EllipticalArc::transform (const Matrix &matrix) { double c = cos(_rotationAngle); double s = sin(_rotationAngle); Matrix ellipse({_rx*c, -_ry*s, 0, _rx*s, _ry*c}); // E := rotate(xrot)*scale(rx, ry) ellipse.lmultiply(matrix); // E':= M*E // Compute the singular value decomposition of the transformed ellipse shape: // E' = rotate(phi)*scale(sx, sy)*rotate(theta) // The initial, right-hand rotation can be ignored because it rotates the unit circle // around the origin, i.e. rotate(theta) maps the circle to itself. // The signs of sx and sy don't matter either. They just flip the yet unrotated // ellipse on the x- and/or y-axis. Thus, |sx| and |sy| are the new radii, // and phi the new rotation angle. auto vec = math::svd({{ellipse.get(0,0), ellipse.get(0,1)}, {ellipse.get(1,0), ellipse.get(1,1)}}); if (std::abs(vec[1]-vec[2]) < 1e-7) { // circle? _rx = _ry = vec[1]; // always >= 0 _rotationAngle = 0; } else { _rx = vec[1]; // always >= 0 _ry = abs(vec[2]); // ensure >= 0 _rotationAngle = math::normalize_angle(vec[0], math::HALF_PI); } // change drawing direction (clockwise vs. counter-clockwise) if 'matrix' // flipped the ellipse horizontally or vertically but not both if ((matrix.get(0, 0) < 0) != (matrix.get(1, 1) < 0)) _sweepPositive = !_sweepPositive; _startPoint = matrix * _startPoint; _endPoint = matrix * _endPoint; } /** Approximates an arc of the unit circle by a single cubic Bézier curve. * @param[in] phi start angle of the arc in radians * @param[in] delta length of the arc */ static CubicBezier approx_unit_arc (double phi, double delta) { double c = 0.551915024494; // see http://spencermortensen.com/articles/bezier-circle if (abs(delta + math::HALF_PI) < 1e-7) c = -c; else c = 4.0/3*tan(delta/4); DPair p1(cos(phi), sin(phi)); DPair p4(cos(phi+delta), sin(phi+delta)); DPair p2(p1.x()-c*p1.y(), p1.y()+c*p1.x()); DPair p3(p4.x()+c*p4.y(), p4.y()-c*p4.x()); return CubicBezier(p1, p2, p3, p4); } /** Approximates the arc by a sequence of cubic Bézier curves. */ vector EllipticalArc::approximate () const { vector beziers; if (_startPoint != _endPoint) { if (isStraightLine()) { DPair dir = (_endPoint - _startPoint); dir /= dir.length()/3.0; beziers.emplace_back(_startPoint, _startPoint+dir, _endPoint-dir, _endPoint); } else { CenterParams cparams = getCenterParams(); int numCurves = ceil(cparams.deltaAngle/math::HALF_PI); double remainder = abs(fmod(cparams.deltaAngle, math::HALF_PI)); if (remainder < 1e-7) numCurves--; else if (math::HALF_PI-remainder < 1e-7) numCurves++; if (numCurves > 0) { double c = cos(_rotationAngle); double s = sin(_rotationAngle); Matrix ellipse{_rx*c, -_ry*s, cparams.center.x(), _rx*s, _ry*c, cparams.center.y()}; double angle = cparams.startAngle; double diff = cparams.deltaAngle/numCurves; while (numCurves-- > 0) { beziers.push_back(approx_unit_arc(angle, diff).transform(ellipse)); angle += diff; } } } } return beziers; } static inline bool is_angle_between (double t, double angle1, double angle2) { if (angle1 < angle2) return angle1 < t && t < angle2; return angle2 > t || t > angle1; } /** Returns the tight bounding box of the arc. */ BoundingBox EllipticalArc::getBBox () const { BoundingBox bbox; bbox.embed(_startPoint); bbox.embed(_endPoint); if (!isStraightLine()) { // compute extremes of ellipse centered at the origin double c = cos(_rotationAngle); double s = sin(_rotationAngle); double tx1 = math::normalize_0_2pi(-atan2(_ry*s, _rx*c)); // position of vertical tangent, d/dt E(tx1)=(0, y) double tx2 = math::normalize_0_2pi(math::PI+tx1); // position of second vertical tangent double ct = cos(tx1); double st = sin(tx1); DPair pv1(_rx*c*ct - _ry*s*st, _rx*s*ct + _ry*c*st); // E(tx1), 1st point on ellipse with vertical tangent DPair pv2 = -pv1; // E(tx2), 2nd point on ellipse with vertical tangent double ty1 = math::normalize_0_2pi(atan2(_ry*c, _rx*s)); // position of horizontal tangent, d/dt E(ty1)=(x, 0) double ty2 = math::normalize_0_2pi(math::PI+ty1); // position of second horizontal tangent ct = cos(ty1); st = sin(ty1); DPair ph1(_rx*c*ct - _ry*s*st, _rx*s*ct + _ry*c*st); // E(ty1), 1st point on ellipse with horizontal tangent DPair ph2 = -ph1; // E(ty2), 2nd point on ellipse with horizontal tangent // translate extreme points to actual coordinates CenterParams cparams = getCenterParams(); pv1 += cparams.center; pv2 += cparams.center; ph1 += cparams.center; ph2 += cparams.center; double angle1 = cparams.startAngle; double angle2 = math::normalize_0_2pi(angle1+cparams.deltaAngle); if (!_sweepPositive) swap(angle1, angle2); // only consider extreme points located on the arc if (is_angle_between(tx1, angle1, angle2)) bbox.embed(pv1); if (is_angle_between(tx2, angle1, angle2)) bbox.embed(pv2); if (is_angle_between(ty1, angle1, angle2)) bbox.embed(ph1); if (is_angle_between(ty2, angle1, angle2)) bbox.embed(ph2); } return bbox; }