File indexing completed on 2024-06-23 04:27:02

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2007 Jan Hambrecht <jaham@gmx.net>
0003  * SPDX-FileCopyrightText: 2010 Thomas Zander <zander@kde.org>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 
0008 #include "EnhancedPathCommand.h"
0009 #include "EnhancedPathParameter.h"
0010 #include "EnhancedPathShape.h"
0011 #include <KoPathPoint.h>
0012 #include <math.h>
0013 #include <QDebug>
0014 
0015 // radian to degree factor
0016 const qreal rad2deg = 180.0 / M_PI;
0017 
0018 EnhancedPathCommand::EnhancedPathCommand(const QChar &command, EnhancedPathShape *parent)
0019     : m_command(command)
0020     , m_parent(parent)
0021 {
0022     Q_ASSERT(m_parent);
0023 }
0024 
0025 EnhancedPathCommand::~EnhancedPathCommand()
0026 {
0027 }
0028 
0029 bool EnhancedPathCommand::execute()
0030 {
0031     /*
0032      * The parameters of the commands are in viewbox coordinates, which have
0033      * to be converted to the shapes coordinate system by calling viewboxToShape
0034      * on the enhanced path the command works on.
0035      * Parameters which resemble angles are angles corresponding to the viewbox
0036      * coordinate system. Those have to be transformed into angles corresponding
0037      * to the normal mathematically coordinate system to be used for the arcTo
0038      * drawing routine. This is done by computing (2*M_PI - angle).
0039      */
0040     QList<QPointF> points = pointsFromParameters();
0041     const int pointsCount = points.size();
0042 
0043     switch (m_command.unicode()) {
0044     // starts new subpath at given position (x y) +
0045     case 'M':
0046         if (!pointsCount) {
0047             return false;
0048         }
0049         m_parent->moveTo(points[0]);
0050         if (pointsCount > 1)
0051             for (int i = 1; i < pointsCount; i++) {
0052                 m_parent->lineTo(points[i]);
0053             }
0054         break;
0055     // line from current point (x y) +
0056     case 'L':
0057         Q_FOREACH (const QPointF &point, points) {
0058             m_parent->lineTo(point);
0059         }
0060         break;
0061     // cubic bezier curve from current point (x1 y1 x2 y2 x y) +
0062     case 'C':
0063         for (int i = 0; i < pointsCount; i += 3) {
0064             m_parent->curveTo(points[i], points[i + 1], points[i + 2]);
0065         }
0066         break;
0067     // closes the current subpath
0068     case 'Z':
0069         m_parent->close();
0070         break;
0071     // ends the current set of subpaths
0072     case 'N':
0073         // N just ends the complete path
0074         break;
0075     // no fill for current set of subpaths
0076     case 'F':
0077         // TODO implement me
0078         break;
0079     // no stroke for current set of subpaths
0080     case 'S':
0081         // TODO implement me
0082         break;
0083     // segment of an ellipse (x y w h t0 t1) +
0084     case 'T':
0085     // same like T but with implied movement to starting point (x y w h t0 t1) +
0086     case 'U': {
0087         bool lineTo = m_command.unicode() == 'T';
0088 
0089         for (int i = 0; i < pointsCount; i += 3) {
0090             const QPointF &radii = points[i + 1];
0091             const QPointF &angles = points[i + 2] / rad2deg;
0092             // compute the ellipses starting point
0093             QPointF start(radii.x() * cos(angles.x()), -1 * radii.y() * sin(angles.x()));
0094             qreal sweepAngle = degSweepAngle(points[i + 2].x(), points[i + 2].y(), false);
0095 
0096             if (lineTo) {
0097                 m_parent->lineTo(points[i] + start);
0098             } else {
0099                 m_parent->moveTo(points[i] + start);
0100             }
0101 
0102             m_parent->arcTo(radii.x(), radii.y(), points[i + 2].x(), sweepAngle);
0103         }
0104         break;
0105     }
0106     // counter-clockwise arc (x1 y1 x2 y2 x3 y3 x y) +
0107     case 'A':
0108     // the same as A, with implied moveto to the starting point (x1 y1 x2 y2 x3 y3 x y) +
0109     case 'B':
0110     // clockwise arc (x1 y1 x2 y2 x3 y3 x y) +
0111     case 'W':
0112     // the same as W, but implied moveto (x1 y1 x2 y2 x3 y3 x y) +
0113     case 'V': {
0114         bool lineTo = ((m_command.unicode() == 'A') || (m_command.unicode() == 'W'));
0115         bool clockwise = ((m_command.unicode() == 'W') || (m_command.unicode() == 'V'));
0116         for (int i = 0; i < pointsCount; i += 4) {
0117             QRectF bbox = rectFromPoints(points[i], points[i + 1]);
0118             QPointF center = bbox.center();
0119             qreal rx = 0.5 * bbox.width();
0120             qreal ry = 0.5 * bbox.height();
0121 
0122             if (rx == 0) {
0123                 rx = 1;
0124             }
0125 
0126             if (ry == 0) {
0127                 ry = 1;
0128             }
0129 
0130             QPointF startRadialVector = points[i + 2] - center;
0131             QPointF endRadialVector = points[i + 3] - center;
0132 
0133             // convert from ellipse space to unit-circle space
0134             qreal x0 = startRadialVector.x() / rx;
0135             qreal y0 = startRadialVector.y() / ry;
0136 
0137             qreal x1 = endRadialVector.x() / rx;
0138             qreal y1 = endRadialVector.y() / ry;
0139 
0140             qreal startAngle = angleFromPoint(QPointF(x0, y0));
0141             qreal stopAngle = angleFromPoint(QPointF(x1, y1));
0142 
0143             // we are moving counter-clockwise to the end angle
0144             qreal sweepAngle = radSweepAngle(startAngle, stopAngle, clockwise);
0145             // compute the starting point to draw the line to
0146             // as the point x3 y3 is not on the ellipse, spec says the point define radial vector
0147             QPointF startPoint(rx * cos(startAngle), ry * sin(2 * M_PI - startAngle));
0148 
0149             // if A or W is first command in enhanced path
0150             // move to the starting point
0151             bool isFirstCommandInPath = (m_parent->subpathCount() == 0);
0152             bool isFirstCommandInSubpath = m_parent->isClosedSubpath(m_parent->subpathCount() - 1);
0153 
0154             if (lineTo && !isFirstCommandInPath && !isFirstCommandInSubpath) {
0155                 m_parent->lineTo(center + startPoint);
0156             } else {
0157                 m_parent->moveTo(center + startPoint);
0158             }
0159 
0160             m_parent->arcTo(rx, ry, startAngle * rad2deg, sweepAngle * rad2deg);
0161         }
0162         break;
0163     }
0164     // elliptical quadrant (initial segment tangential to x-axis) (x y) +
0165     case 'X': {
0166         KoPathPoint *lastPoint = lastPathPoint();
0167         bool xDir = true;
0168         foreach (const QPointF &point, points) {
0169             qreal rx = point.x() - lastPoint->point().x();
0170             qreal ry = point.y() - lastPoint->point().y();
0171             qreal startAngle = xDir ? (ry > 0.0 ? 90.0 : 270.0) : (rx < 0.0 ? 0.0 : 180.0);
0172             qreal sweepAngle = xDir ? (rx * ry < 0.0 ? 90.0 : -90.0) : (rx * ry > 0.0 ? 90.0 : -90.0);
0173             lastPoint = m_parent->arcTo(fabs(rx), fabs(ry), startAngle, sweepAngle);
0174             xDir = !xDir;
0175         }
0176         break;
0177     }
0178     // elliptical quadrant (initial segment tangential to y-axis) (x y) +
0179     case 'Y': {
0180         KoPathPoint *lastPoint = lastPathPoint();
0181         bool xDir = false;
0182         foreach (const QPointF &point, points) {
0183             qreal rx = point.x() - lastPoint->point().x();
0184             qreal ry = point.y() - lastPoint->point().y();
0185             qreal startAngle = xDir ? (ry > 0.0 ? 90.0 : 270.0) : (rx < 0.0 ? 0.0 : 180.0);
0186             qreal sweepAngle = xDir ? (rx * ry < 0.0 ? 90.0 : -90.0) : (rx * ry > 0.0 ? 90.0 : -90.0);
0187             lastPoint = m_parent->arcTo(fabs(rx), fabs(ry), startAngle, sweepAngle);
0188             xDir = !xDir;
0189         }
0190         break;
0191     }
0192     // quadratic bezier curve (x1 y1 x y)+
0193     case 'Q':
0194         for (int i = 0; i < pointsCount; i += 2) {
0195             m_parent->curveTo(points[i], points[i + 1]);
0196         }
0197         break;
0198     default:
0199         break;
0200     }
0201     return true;
0202 }
0203 
0204 QList<QPointF> EnhancedPathCommand::pointsFromParameters()
0205 {
0206     QList<QPointF> points;
0207     QPointF p;
0208 
0209     int paramCount = m_parameters.count();
0210     for (int i = 0; i < paramCount - 1; i += 2) {
0211         p.setX(m_parameters[i]->evaluate());
0212         p.setY(m_parameters[i + 1]->evaluate());
0213         points.append(p);
0214     }
0215 
0216     int mod = 1;
0217     if (m_command.unicode() == 'C' || m_command.unicode() == 'U'
0218             || m_command.unicode() == 'T') {
0219         mod = 3;
0220     } else if (m_command.unicode() == 'A' || m_command.unicode() == 'B'
0221                || m_command.unicode() == 'W' || m_command.unicode() == 'V') {
0222         mod = 4;
0223     } else if (m_command.unicode() == 'Q') {
0224         mod = 2;
0225     }
0226     if ((points.count() % mod) != 0) { // invalid command
0227         qWarning() << "Invalid point count for command" << m_command << "ignoring" << "count:" << points.count() << "mod:" << mod;
0228         return QList<QPointF>();
0229     }
0230 
0231     return points;
0232 }
0233 
0234 void EnhancedPathCommand::addParameter(EnhancedPathParameter *parameter)
0235 {
0236     if (parameter) {
0237         m_parameters.append(parameter);
0238     }
0239 }
0240 
0241 qreal EnhancedPathCommand::angleFromPoint(const QPointF &point) const
0242 {
0243     qreal angle = atan2(point.y(), point.x());
0244     if (angle < 0.0) {
0245         angle += 2 * M_PI;
0246     }
0247 
0248     return 2 * M_PI - angle;
0249 }
0250 
0251 qreal EnhancedPathCommand::radSweepAngle(qreal start, qreal stop, bool clockwise) const
0252 {
0253     qreal sweepAngle = stop - start;
0254     if (fabs(sweepAngle) < 0.1) {
0255         return 2 * M_PI;
0256     }
0257     if (clockwise) {
0258         // we are moving clockwise to the end angle
0259         if (stop > start) {
0260             sweepAngle = (stop - start) - 2 * M_PI;
0261         }
0262     } else {
0263         // we are moving counter-clockwise to the stop angle
0264         if (start > stop) {
0265             sweepAngle = 2 * M_PI - (start - stop);
0266         }
0267     }
0268 
0269     return sweepAngle;
0270 }
0271 
0272 qreal EnhancedPathCommand::degSweepAngle(qreal start, qreal stop, bool clockwise) const
0273 {
0274     qreal sweepAngle = stop - start;
0275     if (fabs(sweepAngle) < 0.1) {
0276         return 360.0;
0277     }
0278     if (clockwise) {
0279         // we are moving clockwise to the end angle
0280         if (stop > start) {
0281             sweepAngle = (stop - start) - 360.0;
0282         }
0283     } else {
0284         // we are moving counter-clockwise to the stop angle
0285         if (start > stop) {
0286             sweepAngle = 360.0 - (start - stop);
0287         }
0288     }
0289 
0290     return sweepAngle;
0291 }
0292 
0293 KoPathPoint *EnhancedPathCommand::lastPathPoint() const
0294 {
0295     KoPathPoint *lastPoint = 0;
0296     int subpathCount = m_parent->subpathCount();
0297     if (subpathCount) {
0298         int subpathPointCount = m_parent->subpathPointCount(subpathCount - 1);
0299         lastPoint = m_parent->pointByIndex(KoPathPointIndex(subpathCount - 1, subpathPointCount - 1));
0300     }
0301     return lastPoint;
0302 }
0303 
0304 QRectF EnhancedPathCommand::rectFromPoints(const QPointF &tl, const QPointF &br) const
0305 {
0306     return QRectF(tl, QSizeF(br.x() - tl.x(), br.y() - tl.y())).normalized();
0307 }
0308 
0309 QString EnhancedPathCommand::toString() const
0310 {
0311     QString cmd = m_command;
0312 
0313     Q_FOREACH (EnhancedPathParameter *p, m_parameters) {
0314         cmd += p->toString() + ' ';
0315     }
0316 
0317     return cmd.trimmed();
0318 }