File indexing completed on 2025-02-02 04:14:40
0001 /* This file is part of the KDE project 0002 * 0003 * SPDX-FileCopyrightText: 2006 Thorsten Zachmann <zachmann@kde.org> 0004 * SPDX-FileCopyrightText: 2008-2010 Jan Hambrecht <jaham@gmx.net> 0005 * 0006 * SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #ifndef KOCREATEPATHTOOL_P_H 0010 #define KOCREATEPATHTOOL_P_H 0011 0012 #include <QPainterPath> 0013 0014 #include "KoCreatePathTool.h" 0015 #include "KoPathPoint.h" 0016 #include "KoPathPointData.h" 0017 #include "KoPathPointMergeCommand.h" 0018 #include "KoParameterShape.h" 0019 #include "KoShapeManager.h" 0020 #include "KoSnapStrategy.h" 0021 #include "KoToolBase_p.h" 0022 #include <KoViewConverter.h> 0023 #include "kis_config.h" 0024 0025 #include "math.h" 0026 0027 class KoStrokeConfigWidget; 0028 class KoConverter; 0029 0030 /// Small helper to keep track of a path point and its parent path shape 0031 struct PathConnectionPoint { 0032 PathConnectionPoint() 0033 : path(0), point(0) { 0034 } 0035 0036 // reset state to invalid 0037 void reset() { 0038 path = 0; 0039 point = 0; 0040 } 0041 0042 PathConnectionPoint& operator =(KoPathPoint * pathPoint) { 0043 if (!pathPoint || ! pathPoint->parent()) { 0044 reset(); 0045 } else { 0046 path = pathPoint->parent(); 0047 point = pathPoint; 0048 } 0049 return *this; 0050 } 0051 0052 bool operator != (const PathConnectionPoint &rhs) const { 0053 return rhs.path != path || rhs.point != point; 0054 } 0055 0056 bool operator == (const PathConnectionPoint &rhs) const { 0057 return rhs.path == path && rhs.point == point; 0058 } 0059 0060 bool isValid() const { 0061 return path && point; 0062 } 0063 0064 // checks if the path and point are still valid 0065 void validate(KoCanvasBase *canvas) { 0066 // no point in validating an already invalid state 0067 if (!isValid()) { 0068 return; 0069 } 0070 // we need canvas to validate 0071 if (!canvas) { 0072 reset(); 0073 return; 0074 } 0075 // check if path is still part of the document 0076 if (!canvas->shapeManager()->shapes().contains(path)) { 0077 reset(); 0078 return; 0079 } 0080 // check if point is still part of the path 0081 if (path->pathPointIndex(point) == KoPathPointIndex(-1, -1)) { 0082 reset(); 0083 return; 0084 } 0085 } 0086 0087 KoPathShape * path; 0088 KoPathPoint * point; 0089 }; 0090 0091 inline qreal squareDistance(const QPointF &p1, const QPointF &p2) 0092 { 0093 qreal dx = p1.x() - p2.x(); 0094 qreal dy = p1.y() - p2.y(); 0095 return dx * dx + dy * dy; 0096 } 0097 0098 class AngleSnapStrategy : public KoSnapStrategy 0099 { 0100 public: 0101 explicit AngleSnapStrategy(qreal angleStep, bool active) 0102 : KoSnapStrategy(KoSnapGuide::CustomSnapping), m_angleStep(angleStep), m_active(active) { 0103 } 0104 0105 void setStartPoint(const QPointF &startPoint) { 0106 m_startPoint = startPoint; 0107 } 0108 0109 void setAngleStep(qreal angleStep) { 0110 m_angleStep = qAbs(angleStep); 0111 } 0112 0113 bool snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) override { 0114 Q_UNUSED(proxy); 0115 0116 if (!m_active) 0117 return false; 0118 0119 QLineF line(m_startPoint, mousePosition); 0120 qreal currentAngle = line.angle(); 0121 int prevStep = qAbs(currentAngle / m_angleStep); 0122 int nextStep = prevStep + 1; 0123 qreal prevAngle = prevStep * m_angleStep; 0124 qreal nextAngle = nextStep * m_angleStep; 0125 0126 if (qAbs(currentAngle - prevAngle) <= qAbs(currentAngle - nextAngle)) { 0127 line.setAngle(prevAngle); 0128 } else { 0129 line.setAngle(nextAngle); 0130 } 0131 0132 qreal maxSquareSnapDistance = maxSnapDistance * maxSnapDistance; 0133 qreal snapDistance = squareDistance(mousePosition, line.p2()); 0134 if (snapDistance > maxSquareSnapDistance) 0135 return false; 0136 0137 setSnappedPosition(line.p2()); 0138 return true; 0139 } 0140 0141 QPainterPath decoration(const KoViewConverter &converter) const override { 0142 Q_UNUSED(converter); 0143 0144 QPainterPath decoration; 0145 decoration.moveTo(m_startPoint); 0146 decoration.lineTo(snappedPosition()); 0147 return decoration; 0148 } 0149 0150 void deactivate() { 0151 m_active = false; 0152 } 0153 0154 void activate() { 0155 m_active = true; 0156 } 0157 0158 private: 0159 QPointF m_startPoint; 0160 qreal m_angleStep; 0161 bool m_active; 0162 }; 0163 0164 0165 class KoCreatePathToolPrivate : public KoToolBasePrivate 0166 { 0167 KoCreatePathTool * const q; 0168 public: 0169 KoCreatePathToolPrivate(KoCreatePathTool * const qq, KoCanvasBase* canvas) 0170 : KoToolBasePrivate(qq, canvas), 0171 q(qq), 0172 shape(0), 0173 activePoint(0), 0174 firstPoint(0), 0175 handleRadius(3), 0176 decorationThickness(1), 0177 mouseOverFirstPoint(false), 0178 pointIsDragged(false), 0179 finishAfterThisPoint(false), 0180 hoveredPoint(0), 0181 angleSnapStrategy(0), 0182 angleSnappingDelta(15), 0183 angleSnapStatus(false), 0184 enableClosePathShortcut(true) 0185 { 0186 } 0187 0188 KoPathShape *shape; 0189 KoPathPoint *activePoint; 0190 KoPathPoint *firstPoint; 0191 int handleRadius; 0192 int decorationThickness; 0193 bool mouseOverFirstPoint; 0194 bool pointIsDragged; 0195 bool finishAfterThisPoint; 0196 PathConnectionPoint existingStartPoint; ///< an existing path point we started a new path at 0197 PathConnectionPoint existingEndPoint; ///< an existing path point we finished a new path at 0198 KoPathPoint *hoveredPoint; ///< an existing path end point the mouse is hovering on 0199 bool prevPointWasDragged = false; 0200 bool autoSmoothCurves = false; 0201 0202 QPointF dragStartPoint; 0203 0204 AngleSnapStrategy *angleSnapStrategy; 0205 int angleSnappingDelta; 0206 bool angleSnapStatus; 0207 bool enableClosePathShortcut; 0208 0209 void repaintActivePoint() const { 0210 const bool isFirstPoint = (activePoint == firstPoint); 0211 0212 if (!isFirstPoint && !pointIsDragged) 0213 return; 0214 0215 QRectF rect = activePoint->boundingRect(false); 0216 0217 // make sure that we have the second control point inside our 0218 // update rect, as KoPathPoint::boundingRect will not include 0219 // the second control point of the last path point if the path 0220 // is not closed 0221 const QPointF &point = activePoint->point(); 0222 const QPointF &controlPoint = activePoint->controlPoint2(); 0223 rect = rect.united(QRectF(point, controlPoint).normalized()); 0224 0225 // when painting the first point we want the 0226 // first control point to be painted as well 0227 if (isFirstPoint) { 0228 const QPointF &controlPoint = activePoint->controlPoint1(); 0229 rect = rect.united(QRectF(point, controlPoint).normalized()); 0230 } 0231 0232 QPointF border = q->canvas()->viewConverter() 0233 ->viewToDocument(QPointF(handleRadius, handleRadius)); 0234 0235 rect.adjust(-border.x(), -border.y(), border.x(), border.y()); 0236 q->canvas()->updateCanvas(rect); 0237 } 0238 0239 /// returns the nearest existing path point 0240 KoPathPoint* endPointAtPosition(const QPointF &position) const { 0241 QRectF roi = q->handleGrabRect(position); 0242 QList<KoShape *> shapes = q->canvas()->shapeManager()->shapesAt(roi); 0243 0244 KoPathPoint * nearestPoint = 0; 0245 qreal minDistance = HUGE_VAL; 0246 uint grabSensitivity = q->grabSensitivity(); 0247 qreal maxDistance = q->canvas()->viewConverter()->viewToDocumentX(grabSensitivity); 0248 0249 Q_FOREACH(KoShape * s, shapes) { 0250 KoPathShape * path = dynamic_cast<KoPathShape*>(s); 0251 if (!path) 0252 continue; 0253 KoParameterShape *paramShape = dynamic_cast<KoParameterShape*>(s); 0254 if (paramShape && paramShape->isParametricShape()) 0255 continue; 0256 0257 KoPathPoint * p = 0; 0258 uint subpathCount = path->subpathCount(); 0259 for (uint i = 0; i < subpathCount; ++i) { 0260 if (path->isClosedSubpath(i)) 0261 continue; 0262 p = path->pointByIndex(KoPathPointIndex(i, 0)); 0263 // check start of subpath 0264 qreal d = squareDistance(position, path->shapeToDocument(p->point())); 0265 if (d < minDistance && d < maxDistance) { 0266 nearestPoint = p; 0267 minDistance = d; 0268 } 0269 // check end of subpath 0270 p = path->pointByIndex(KoPathPointIndex(i, path->subpathPointCount(i) - 1)); 0271 d = squareDistance(position, path->shapeToDocument(p->point())); 0272 if (d < minDistance && d < maxDistance) { 0273 nearestPoint = p; 0274 minDistance = d; 0275 } 0276 } 0277 } 0278 0279 return nearestPoint; 0280 } 0281 0282 /// Connects given path with the ones we hit when starting/finishing 0283 bool connectPaths(KoPathShape *pathShape, const PathConnectionPoint &pointAtStart, const PathConnectionPoint &pointAtEnd) const { 0284 KoPathShape * startShape = 0; 0285 KoPathShape * endShape = 0; 0286 KoPathPoint * startPoint = 0; 0287 KoPathPoint * endPoint = 0; 0288 0289 if (pointAtStart.isValid()) { 0290 startShape = pointAtStart.path; 0291 startPoint = pointAtStart.point; 0292 } 0293 if (pointAtEnd.isValid()) { 0294 endShape = pointAtEnd.path; 0295 endPoint = pointAtEnd.point; 0296 } 0297 0298 // at least one point must be valid 0299 if (!startPoint && !endPoint) 0300 return false; 0301 // do not allow connecting to the same point twice 0302 if (startPoint == endPoint) 0303 endPoint = 0; 0304 0305 // we have hit an existing path point on start/finish 0306 // what we now do is: 0307 // 1. combine the new created path with the ones we hit on start/finish 0308 // 2. merge the endpoints of the corresponding subpaths 0309 0310 uint newPointCount = pathShape->subpathPointCount(0); 0311 KoPathPointIndex newStartPointIndex(0, 0); 0312 KoPathPointIndex newEndPointIndex(0, newPointCount - 1); 0313 KoPathPoint * newStartPoint = pathShape->pointByIndex(newStartPointIndex); 0314 KoPathPoint * newEndPoint = pathShape->pointByIndex(newEndPointIndex); 0315 0316 // combine with the path we hit on start 0317 KoPathPointIndex startIndex(-1, -1); 0318 if (startShape && startPoint) { 0319 startIndex = startShape->pathPointIndex(startPoint); 0320 pathShape->combine(startShape); 0321 pathShape->moveSubpath(0, pathShape->subpathCount() - 1); 0322 } 0323 // combine with the path we hit on finish 0324 KoPathPointIndex endIndex(-1, -1); 0325 if (endShape && endPoint) { 0326 endIndex = endShape->pathPointIndex(endPoint); 0327 if (endShape != startShape) { 0328 endIndex.first += pathShape->subpathCount(); 0329 pathShape->combine(endShape); 0330 } 0331 } 0332 // do we connect twice to a single subpath ? 0333 bool connectToSingleSubpath = (startShape == endShape && startIndex.first == endIndex.first); 0334 0335 if (startIndex.second == 0 && !connectToSingleSubpath) { 0336 pathShape->reverseSubpath(startIndex.first); 0337 startIndex.second = pathShape->subpathPointCount(startIndex.first) - 1; 0338 } 0339 if (endIndex.second > 0 && !connectToSingleSubpath) { 0340 pathShape->reverseSubpath(endIndex.first); 0341 endIndex.second = 0; 0342 } 0343 0344 // after combining we have a path where with the subpaths in the following 0345 // order: 0346 // 1. the subpaths of the pathshape we started the new path at 0347 // 2. the subpath we just created 0348 // 3. the subpaths of the pathshape we finished the new path at 0349 0350 // get the path points we want to merge, as these are not going to 0351 // change while merging 0352 KoPathPoint * existingStartPoint = pathShape->pointByIndex(startIndex); 0353 KoPathPoint * existingEndPoint = pathShape->pointByIndex(endIndex); 0354 0355 // merge first two points 0356 if (existingStartPoint) { 0357 KoPathPointData pd1(pathShape, pathShape->pathPointIndex(existingStartPoint)); 0358 KoPathPointData pd2(pathShape, pathShape->pathPointIndex(newStartPoint)); 0359 KoPathPointMergeCommand cmd1(pd1, pd2); 0360 cmd1.redo(); 0361 } 0362 // merge last two points 0363 if (existingEndPoint) { 0364 KoPathPointData pd3(pathShape, pathShape->pathPointIndex(newEndPoint)); 0365 KoPathPointData pd4(pathShape, pathShape->pathPointIndex(existingEndPoint)); 0366 KoPathPointMergeCommand cmd2(pd3, pd4); 0367 cmd2.redo(); 0368 } 0369 0370 return true; 0371 } 0372 0373 void addPathShape() { 0374 if (!shape) return; 0375 0376 if (shape->pointCount() < 2) { 0377 cleanUp(); 0378 return; 0379 } 0380 0381 // this is done so that nothing happens when the mouseReleaseEvent for the this event is received 0382 KoPathShape *pathShape = shape; 0383 shape = 0; 0384 0385 q->addPathShape(pathShape); 0386 0387 cleanUp(); 0388 0389 return; 0390 } 0391 0392 void cleanUp() { 0393 // reset snap guide 0394 q->canvas()->updateCanvas(q->canvas()->snapGuide()->boundingRect()); 0395 q->canvas()->snapGuide()->reset(); 0396 angleSnapStrategy = 0; 0397 0398 delete shape; 0399 shape = 0; 0400 existingStartPoint = 0; 0401 existingEndPoint = 0; 0402 hoveredPoint = 0; 0403 activePoint = 0; 0404 } 0405 0406 void angleDeltaChanged(qreal value) { 0407 angleSnappingDelta = static_cast<int>(value); 0408 if (angleSnapStrategy) 0409 angleSnapStrategy->setAngleStep(angleSnappingDelta); 0410 } 0411 0412 void autoSmoothCurvesChanged(bool value) { 0413 autoSmoothCurves = value; 0414 0415 KisConfig cfg(false); 0416 cfg.setAutoSmoothBezierCurves(value); 0417 } 0418 0419 void loadAutoSmoothValueFromConfig() { 0420 KisConfig cfg(true); 0421 autoSmoothCurves = cfg.autoSmoothBezierCurves(); 0422 0423 emit q->sigUpdateAutoSmoothCurvesGUI(autoSmoothCurves); 0424 } 0425 0426 void angleSnapChanged(int angleSnap) { 0427 angleSnapStatus = ! angleSnapStatus; 0428 if (angleSnapStrategy) { 0429 if (angleSnap == Qt::Checked) 0430 angleSnapStrategy->activate(); 0431 else 0432 angleSnapStrategy->deactivate(); 0433 } 0434 } 0435 }; 0436 0437 #endif // KOCREATEPATHTOOL_P_H