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

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2007, 2010, 2011 Jan Hambrecht <jaham@gmx.net>
0003  * SPDX-FileCopyrightText: 2009-2010 Thomas Zander <zander@kde.org>
0004  * SPDX-FileCopyrightText: 2010 Carlos Licea <carlos@kdab.com>
0005  * SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies).
0006  *   Contact: Suresh Chande suresh.chande@nokia.com
0007  * SPDX-FileCopyrightText: 2009-2010 Thorsten Zachmann <zachmann@kde.org>
0008  *
0009  * SPDX-License-Identifier: LGPL-2.0-or-later
0010  */
0011 
0012 #include <KoParameterShape_p.h>
0013 
0014 #include "EnhancedPathShape.h"
0015 #include "EnhancedPathCommand.h"
0016 #include "EnhancedPathParameter.h"
0017 #include "EnhancedPathHandle.h"
0018 #include "EnhancedPathFormula.h"
0019 
0020 #include <QPainterPath>
0021 
0022 #include <KoXmlNS.h>
0023 #include <KoXmlWriter.h>
0024 #include <KoShapeSavingContext.h>
0025 #include <KoUnit.h>
0026 #include <KoPathPoint.h>
0027 
0028 EnhancedPathShape::EnhancedPathShape(const QRect &viewBox)
0029     : m_viewBox(viewBox)
0030     , m_viewBoxOffset(0.0, 0.0)
0031     , m_mirrorVertically(false)
0032     , m_mirrorHorizontally(false)
0033     , m_pathStretchPointX(-1)
0034     , m_pathStretchPointY(-1)
0035     , m_cacheResults(false)
0036 {
0037 }
0038 
0039 EnhancedPathShape::EnhancedPathShape(const EnhancedPathShape &rhs)
0040     : KoParameterShape(rhs),
0041       m_viewBox(rhs.m_viewBox),
0042       m_viewBound(rhs.m_viewBound),
0043       m_viewMatrix(rhs.m_viewMatrix),
0044       m_mirrorMatrix(rhs.m_mirrorMatrix),
0045       m_viewBoxOffset(rhs.m_viewBoxOffset),
0046       m_textArea(rhs.m_textArea),
0047       m_commands(rhs.m_commands),
0048       m_enhancedHandles(rhs.m_enhancedHandles),
0049       m_formulae(rhs.m_formulae),
0050       m_modifiers(rhs.m_modifiers),
0051       m_parameters(rhs.m_parameters),
0052       m_mirrorVertically(rhs.m_mirrorVertically),
0053       m_mirrorHorizontally(rhs.m_mirrorHorizontally),
0054       m_pathStretchPointX(rhs.m_pathStretchPointX),
0055       m_pathStretchPointY(rhs.m_pathStretchPointY),
0056       m_resultCache(rhs.m_resultCache),
0057       m_cacheResults(rhs.m_cacheResults)
0058 {
0059 }
0060 
0061 EnhancedPathShape::~EnhancedPathShape()
0062 {
0063     reset();
0064 }
0065 
0066 KoShape *EnhancedPathShape::cloneShape() const
0067 {
0068     return new EnhancedPathShape(*this);
0069 }
0070 
0071 void EnhancedPathShape::reset()
0072 {
0073     qDeleteAll(m_commands);
0074     m_commands.clear();
0075     qDeleteAll(m_enhancedHandles);
0076     m_enhancedHandles.clear();
0077     setHandles(QList<QPointF>());
0078     qDeleteAll(m_formulae);
0079     m_formulae.clear();
0080     qDeleteAll(m_parameters);
0081     m_parameters.clear();
0082     m_modifiers.clear();
0083     m_viewMatrix.reset();
0084     m_viewBoxOffset = QPointF();
0085     clear();
0086     m_textArea.clear();
0087 }
0088 
0089 void EnhancedPathShape::moveHandleAction(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers)
0090 {
0091     Q_UNUSED(modifiers);
0092     EnhancedPathHandle *handle = m_enhancedHandles[ handleId ];
0093     if (handle) {
0094         handle->changePosition(shapeToViewbox(point));
0095     }
0096 }
0097 
0098 void EnhancedPathShape::updatePath(const QSizeF &size)
0099 {
0100     if (isParametricShape()) {
0101         clear();
0102         enableResultCache(true);
0103 
0104         foreach (EnhancedPathCommand *cmd, m_commands) {
0105             cmd->execute();
0106         }
0107 
0108         enableResultCache(false);
0109 
0110         qreal stretchPointsScale = 1;
0111         bool isStretched = useStretchPoints(size, stretchPointsScale);
0112         m_viewBound = outline().boundingRect();
0113         m_mirrorMatrix.reset();
0114         m_mirrorMatrix.translate(m_viewBound.center().x(), m_viewBound.center().y());
0115         m_mirrorMatrix.scale(m_mirrorHorizontally ? -1 : 1, m_mirrorVertically ? -1 : 1);
0116         m_mirrorMatrix.translate(-m_viewBound.center().x(), -m_viewBound.center().y());
0117         QTransform matrix(1.0, 0.0, 0.0, 1.0, m_viewBoxOffset.x(), m_viewBoxOffset.y());
0118 
0119         // if stretch points are set than stretch the path manually
0120         if (isStretched) {
0121             //if the path was stretched manually the stretch matrix is not more valid
0122             //and it has to be recalculated so that stretching in x and y direction is the same
0123             matrix.scale(stretchPointsScale, stretchPointsScale);
0124             matrix = m_mirrorMatrix * matrix;
0125         } else {
0126             matrix = m_mirrorMatrix * m_viewMatrix * matrix;
0127         }
0128         foreach (KoSubpath *subpath, subpaths()) {
0129             foreach (KoPathPoint *point, *subpath) {
0130                 point->map(matrix);
0131             }
0132         }
0133 
0134         const int handleCount = m_enhancedHandles.count();
0135         QList<QPointF> handles;
0136         for (int i = 0; i < handleCount; ++i) {
0137             handles.append(matrix.map(m_enhancedHandles[i]->position()));
0138         }
0139         setHandles(handles);
0140 
0141         normalize();
0142     }
0143 }
0144 
0145 void EnhancedPathShape::setSize(const QSizeF &newSize)
0146 {
0147     // handle offset
0148     KoParameterShape::setSize(newSize);
0149 
0150     // calculate scaling factors from viewbox size to shape size
0151     qreal xScale = m_viewBound.width() == 0 ? 1 : newSize.width() / m_viewBound.width();
0152     qreal yScale = m_viewBound.height() == 0 ? 1 : newSize.height() / m_viewBound.height();
0153 
0154     // create view matrix, take mirroring into account
0155     m_viewMatrix.reset();
0156     m_viewMatrix.scale(xScale, yScale);
0157 
0158     updatePath(newSize);
0159 }
0160 
0161 QPointF EnhancedPathShape::normalize()
0162 {
0163     QPointF offset = KoParameterShape::normalize();
0164 
0165     m_viewBoxOffset -= offset;
0166 
0167     return offset;
0168 }
0169 
0170 QPointF EnhancedPathShape::shapeToViewbox(const QPointF &point) const
0171 {
0172     return (m_mirrorMatrix * m_viewMatrix).inverted().map(point - m_viewBoxOffset);
0173 }
0174 
0175 void EnhancedPathShape::evaluateHandles()
0176 {
0177     const int handleCount = m_enhancedHandles.count();
0178     QList<QPointF> handles;
0179     for (int i = 0; i < handleCount; ++i) {
0180         handles.append(m_enhancedHandles[i]->position());
0181     }
0182     setHandles(handles);
0183 }
0184 
0185 QRect EnhancedPathShape::viewBox() const
0186 {
0187     return m_viewBox;
0188 }
0189 
0190 qreal EnhancedPathShape::evaluateReference(const QString &reference)
0191 {
0192     if (reference.isEmpty()) {
0193         return 0.0;
0194     }
0195 
0196     const char c = reference[0].toLatin1();
0197 
0198     qreal res = 0.0;
0199 
0200     switch (c) {
0201     // referenced modifier
0202     case '$': {
0203         bool success = false;
0204         int modifierIndex = reference.mid(1).toInt(&success);
0205         res = m_modifiers.value(modifierIndex);
0206         break;
0207     }
0208     // referenced formula
0209     case '?': {
0210         QString fname = reference.mid(1);
0211         if (m_cacheResults && m_resultCache.contains(fname)) {
0212             res = m_resultCache.value(fname);
0213         } else {
0214             FormulaStore::const_iterator formulaIt = m_formulae.constFind(fname);
0215             if (formulaIt != m_formulae.constEnd()) {
0216                 EnhancedPathFormula *formula = formulaIt.value();
0217                 if (formula) {
0218                     res = formula->evaluate();
0219                     if (m_cacheResults) {
0220                         m_resultCache.insert(fname, res);
0221                     }
0222                 }
0223             }
0224         }
0225         break;
0226     }
0227     // maybe an identifier ?
0228     default:
0229         EnhancedPathNamedParameter p(reference, this);
0230         res = p.evaluate();
0231         break;
0232     }
0233 
0234     return res;
0235 }
0236 
0237 qreal EnhancedPathShape::evaluateConstantOrReference(const QString &val)
0238 {
0239     bool ok = true;
0240     qreal res = val.toDouble(&ok);
0241     if (ok) {
0242         return res;
0243     }
0244     return evaluateReference(val);
0245 }
0246 
0247 void EnhancedPathShape::modifyReference(const QString &reference, qreal value)
0248 {
0249     if (reference.isEmpty()) {
0250         return;
0251     }
0252 
0253     const char c = reference[0].toLatin1();
0254 
0255     if (c == '$') {
0256         bool success = false;
0257         int modifierIndex = reference.mid(1).toInt(&success);
0258         if (modifierIndex >= 0 && modifierIndex < m_modifiers.count()) {
0259             m_modifiers[modifierIndex] = value;
0260         }
0261     }
0262 }
0263 
0264 EnhancedPathParameter *EnhancedPathShape::parameter(const QString &text)
0265 {
0266     Q_ASSERT(! text.isEmpty());
0267 
0268     ParameterStore::const_iterator parameterIt = m_parameters.constFind(text);
0269     if (parameterIt != m_parameters.constEnd()) {
0270         return parameterIt.value();
0271     } else {
0272         EnhancedPathParameter *parameter = 0;
0273         const char c = text[0].toLatin1();
0274         if (c == '$' || c == '?') {
0275             parameter = new EnhancedPathReferenceParameter(text, this);
0276         } else {
0277             bool success = false;
0278             qreal constant = text.toDouble(&success);
0279             if (success) {
0280                 parameter = new EnhancedPathConstantParameter(constant, this);
0281             } else {
0282                 Identifier identifier = EnhancedPathNamedParameter::identifierFromString(text);
0283                 if (identifier != IdentifierUnknown) {
0284                     parameter = new EnhancedPathNamedParameter(identifier, this);
0285                 }
0286             }
0287         }
0288 
0289         if (parameter) {
0290             m_parameters[text] = parameter;
0291         }
0292 
0293         return parameter;
0294     }
0295 }
0296 
0297 void EnhancedPathShape::addFormula(const QString &name, const QString &formula)
0298 {
0299     if (name.isEmpty() || formula.isEmpty()) {
0300         return;
0301     }
0302 
0303     m_formulae[name] = new EnhancedPathFormula(formula, this);
0304 }
0305 
0306 void EnhancedPathShape::addHandle(const QMap<QString, QVariant> &handle)
0307 {
0308     if (handle.isEmpty()) {
0309         return;
0310     }
0311 
0312     if (!handle.contains("draw:handle-position")) {
0313         return;
0314     }
0315     QVariant position = handle.value("draw:handle-position");
0316 
0317     QStringList tokens = position.toString().simplified().split(' ');
0318     if (tokens.count() < 2) {
0319         return;
0320     }
0321 
0322     EnhancedPathHandle *newHandle = new EnhancedPathHandle(this);
0323     newHandle->setPosition(parameter(tokens[0]), parameter(tokens[1]));
0324 
0325     // check if we have a polar handle
0326     if (handle.contains("draw:handle-polar")) {
0327         QVariant polar = handle.value("draw:handle-polar");
0328         QStringList tokens = polar.toString().simplified().split(' ');
0329         if (tokens.count() == 2) {
0330             newHandle->setPolarCenter(parameter(tokens[0]), parameter(tokens[1]));
0331 
0332             QVariant minRadius = handle.value("draw:handle-radius-range-minimum");
0333             QVariant maxRadius = handle.value("draw:handle-radius-range-maximum");
0334             if (minRadius.isValid() && maxRadius.isValid()) {
0335                 newHandle->setRadiusRange(parameter(minRadius.toString()), parameter(maxRadius.toString()));
0336             }
0337         }
0338     } else {
0339         QVariant minX = handle.value("draw:handle-range-x-minimum");
0340         QVariant maxX = handle.value("draw:handle-range-x-maximum");
0341         if (minX.isValid() && maxX.isValid()) {
0342             newHandle->setRangeX(parameter(minX.toString()), parameter(maxX.toString()));
0343         }
0344 
0345         QVariant minY = handle.value("draw:handle-range-y-minimum");
0346         QVariant maxY = handle.value("draw:handle-range-y-maximum");
0347         if (minY.isValid() && maxY.isValid()) {
0348             newHandle->setRangeY(parameter(minY.toString()), parameter(maxY.toString()));
0349         }
0350     }
0351 
0352     m_enhancedHandles.append(newHandle);
0353 
0354     evaluateHandles();
0355 }
0356 
0357 void EnhancedPathShape::addModifiers(const QString &modifiers)
0358 {
0359     if (modifiers.isEmpty()) {
0360         return;
0361     }
0362 
0363     QStringList tokens = modifiers.simplified().split(' ');
0364     int tokenCount = tokens.count();
0365     for (int i = 0; i < tokenCount; ++i) {
0366         m_modifiers.append(tokens[i].toDouble());
0367     }
0368 }
0369 
0370 void EnhancedPathShape::addCommand(const QString &command)
0371 {
0372     addCommand(command, true);
0373 }
0374 
0375 void EnhancedPathShape::addCommand(const QString &command, bool triggerUpdate)
0376 {
0377     QString commandStr = command.simplified();
0378     if (commandStr.isEmpty()) {
0379         return;
0380     }
0381 
0382     // the first character is the command
0383     EnhancedPathCommand *cmd = new EnhancedPathCommand(commandStr[0], this);
0384 
0385     // strip command char
0386     commandStr = commandStr.mid(1).simplified();
0387 
0388     // now parse the command parameters
0389     if (!commandStr.isEmpty()) {
0390         QStringList tokens = commandStr.split(' ');
0391         for (int i = 0; i < tokens.count(); ++i) {
0392             cmd->addParameter(parameter(tokens[i]));
0393         }
0394     }
0395     m_commands.append(cmd);
0396 
0397     if (triggerUpdate) {
0398         updatePath(size());
0399     }
0400 }
0401 
0402 bool EnhancedPathShape::useStretchPoints(const QSizeF &size, qreal &scale)
0403 {
0404     bool retval = false;
0405     if (m_pathStretchPointX != -1 && m_pathStretchPointY != -1) {
0406         qreal scaleX = size.width();
0407         qreal scaleY = size.height();
0408         if (qreal(m_viewBox.width()) / m_viewBox.height() < qreal(scaleX) / scaleY) {
0409             qreal deltaX = (scaleX * m_viewBox.height()) / scaleY - m_viewBox.width();
0410             foreach (KoSubpath *subpath, subpaths()) {
0411                 foreach (KoPathPoint *currPoint, *subpath) {
0412                     if (currPoint->point().x() >=  m_pathStretchPointX &&
0413                             currPoint->controlPoint1().x() >= m_pathStretchPointX &&
0414                             currPoint->controlPoint2().x() >= m_pathStretchPointX) {
0415                         currPoint->setPoint(QPointF(currPoint->point().x() + deltaX, currPoint->point().y()));
0416                         currPoint->setControlPoint1(QPointF(currPoint->controlPoint1().x() + deltaX,
0417                                                             currPoint->controlPoint1().y()));
0418                         currPoint->setControlPoint2(QPointF(currPoint->controlPoint2().x() + deltaX,
0419                                                             currPoint->controlPoint2().y()));
0420                         retval = true;
0421                     }
0422                 }
0423             }
0424             scale = scaleY / m_viewBox.height();
0425         } else if (qreal(m_viewBox.width()) / m_viewBox.height() > qreal(scaleX) / scaleY) {
0426             qreal deltaY = (m_viewBox.width() * scaleY) / scaleX - m_viewBox.height();
0427             foreach (KoSubpath *subpath, subpaths()) {
0428                 foreach (KoPathPoint *currPoint, *subpath) {
0429                     if (currPoint->point().y() >=  m_pathStretchPointY &&
0430                             currPoint->controlPoint1().y() >= m_pathStretchPointY &&
0431                             currPoint->controlPoint2().y() >= m_pathStretchPointY) {
0432                         currPoint->setPoint(QPointF(currPoint->point().x(), currPoint->point().y() + deltaY));
0433                         currPoint->setControlPoint1(QPointF(currPoint->controlPoint1().x(),
0434                                                             currPoint->controlPoint1().y() + deltaY));
0435                         currPoint->setControlPoint2(QPointF(currPoint->controlPoint2().x(),
0436                                                             currPoint->controlPoint2().y() + deltaY));
0437                         retval = true;
0438                     }
0439                 }
0440             }
0441             scale = scaleX / m_viewBox.width();
0442         }
0443 
0444         notifyPointsChanged();
0445     }
0446     return retval;
0447 }
0448 
0449 void EnhancedPathShape::parsePathData(const QString &data)
0450 {
0451     if (data.isEmpty()) {
0452         return;
0453     }
0454 
0455     int start = -1;
0456     bool separator = true;
0457     for (int i = 0; i < data.length(); ++i) {
0458         QChar ch = data.at(i);
0459         ushort uni_ch = ch.unicode();
0460         if (separator && (uni_ch == 'M' || uni_ch == 'L'
0461                           || uni_ch == 'C' || uni_ch == 'Z'
0462                           || uni_ch == 'N' || uni_ch == 'F'
0463                           || uni_ch == 'S' || uni_ch == 'T'
0464                           || uni_ch == 'U' || uni_ch == 'A'
0465                           || uni_ch == 'B' || uni_ch == 'W'
0466                           || uni_ch == 'V' || uni_ch == 'X'
0467                           || uni_ch == 'Y' || uni_ch == 'Q')) {
0468             if (start != -1) { // process last chars
0469                 addCommand(data.mid(start, i - start), false);
0470             }
0471             start = i;
0472         }
0473         separator = ch.isSpace();
0474     }
0475     if (start < data.length()) {
0476         addCommand(data.mid(start));
0477     }
0478     if (start != -1) {
0479         updatePath(size());
0480     }
0481 }
0482 
0483 void EnhancedPathShape::setMirrorHorizontally(bool mirrorHorizontally)
0484 {
0485     if (m_mirrorHorizontally != mirrorHorizontally) {
0486         m_mirrorHorizontally = mirrorHorizontally;
0487         updatePath(size());
0488     }
0489 
0490 }
0491 
0492 void EnhancedPathShape::setMirrorVertically(bool mirrorVertically)
0493 {
0494     if (m_mirrorVertically != mirrorVertically) {
0495         m_mirrorVertically = mirrorVertically;
0496         updatePath(size());
0497     }
0498 }
0499 
0500 void EnhancedPathShape::shapeChanged(ChangeType type, KoShape *shape)
0501 {
0502     KoParameterShape::shapeChanged(type, shape);
0503 
0504     if (!shape || shape == this) {
0505         if (type == ParentChanged || type == ParameterChanged) {
0506             updateTextArea();
0507         }
0508     }
0509 }
0510 
0511 void EnhancedPathShape::updateTextArea()
0512 {
0513     if (m_textArea.size() >= 4) {
0514         QRectF r = m_viewBox;
0515         r.setLeft(evaluateConstantOrReference(m_textArea[0]));
0516         r.setTop(evaluateConstantOrReference(m_textArea[1]));
0517         r.setRight(evaluateConstantOrReference(m_textArea[2]));
0518         r.setBottom(evaluateConstantOrReference(m_textArea[3]));
0519         r = m_viewMatrix.mapRect(r).translated(m_viewBoxOffset);
0520         setPreferredTextRect(r);
0521     }
0522 }
0523 
0524 void EnhancedPathShape::enableResultCache(bool enable)
0525 {
0526     m_resultCache.clear();
0527     m_cacheResults = enable;
0528 }
0529 
0530 void EnhancedPathShape::setPathStretchPointX(qreal pathStretchPointX)
0531 {
0532     if (m_pathStretchPointX != pathStretchPointX) {
0533         m_pathStretchPointX = pathStretchPointX;
0534     }
0535 
0536 }
0537 
0538 void EnhancedPathShape::setPathStretchPointY(qreal pathStretchPointY)
0539 {
0540     if (m_pathStretchPointY != pathStretchPointY) {
0541         m_pathStretchPointY = pathStretchPointY;
0542     }
0543 
0544 }