Krita Source Code Documentation
Loading...
Searching...
No Matches
CutThroughShapeStrategy.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2025 Agata Cacko
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <QDebug>
10#include <QPainter>
11
12#include <kis_algebra_2d.h>
13#include <KoToolBase.h>
14#include <KoCanvasBase.h>
15#include <KoViewConverter.h>
16#include <KoSelection.h>
17#include <kis_global.h>
18#include "kis_debug.h"
19#include <KoPathShape.h>
20#include <krita_utils.h>
21#include <kis_canvas2.h>
22#include <QPainterPath>
23#include <KoShapeController.h>
24#include <kundo2command.h>
26#include <QtMath>
27#include <KoSvgTextShape.h>
28
29
30CutThroughShapeStrategy::CutThroughShapeStrategy(KoToolBase *tool, KoSelection *selection, const QList<KoShape *> &shapes, QPointF startPoint, const GutterWidthsConfig &width)
32 , m_startPoint(startPoint)
33 , m_endPoint(startPoint)
34 , m_width(width)
35{
37 m_allShapes = shapes;
38}
39
44
46{
47 // TODO: undoing
48 return 0;
49}
50
51QPointF snapEndPoint(const QPointF &startPoint, const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) {
52
53 QPointF nicePoint = snapToClosestNiceAngle(mouseLocation, startPoint); // by default the function gives you 15 degrees increments
54
55 if (modifiers & Qt::KeyboardModifier::ShiftModifier) {
56 return nicePoint;
57 if (qAbs(mouseLocation.x() - startPoint.x()) >= qAbs(mouseLocation.y() - startPoint.y())) {
58 // do horizontal line
59 return QPointF(mouseLocation.x(), startPoint.y());
60 } else {
61 return QPointF(startPoint.x(), mouseLocation.y());
62 }
63 }
64 QLineF line = QLineF(startPoint, mouseLocation);
65 qreal angle = line.angleTo(QLineF(startPoint, nicePoint));
66 qreal eps = kisDegreesToRadians(2.0f);
67 if (angle < eps) {
68 return nicePoint;
69 }
70 return mouseLocation;
71}
72
73void CutThroughShapeStrategy::handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers)
74{
75 m_endPoint = snapEndPoint(m_startPoint, mouseLocation, modifiers);
76 QRectF dirtyRect;
79 dirtyRect = kisGrowRect(dirtyRect, gutterWidthInDocumentCoordinates(calculateLineAngle(m_startPoint, m_endPoint))); // twice as much as it should need to account for lines showing the effect
80
81 QRectF accumulatedWithPrevious = m_previousLineDirtyRect | dirtyRect;
82
83 tool()->canvas()->updateCanvas(accumulatedWithPrevious);
84 m_previousLineDirtyRect = dirtyRect;
85
86}
87
88
89bool CutThroughShapeStrategy::willShapeBeCutGeneral(KoShape* referenceShape, const QPainterPath& srcOutline, const QRectF& leftOppositeRect, const QRectF& rightOppositeRect, bool checkGapLineRect, const QRectF& gapLineRect)
90{
91 if (dynamic_cast<KoSvgTextShape*>(referenceShape)) {
92 // skip all text
93 return false;
94 }
95
96 if ((srcOutline.boundingRect() & leftOppositeRect).isEmpty()
97 || (srcOutline.boundingRect() & rightOppositeRect).isEmpty()) {
98 // there is nothing on one side
99 // everything is on the other, far away from the gap line
100 // it just makes it a bit faster when there is a whole lot of shapes
101
102 return false;
103 }
104
105 if (checkGapLineRect && (srcOutline.boundingRect() & gapLineRect).isEmpty()) {
106 // the gap lines can't cross the shape since their bounding rects don't cross it
107 return false;
108 }
109
110 return true;
111}
112
113bool CutThroughShapeStrategy::willShapeBeCutPrecise(const QPainterPath& srcOutline, const QLineF gapLine, const QLineF& leftLine, const QLineF& rightLine, const QPolygonF& gapLinePolygon)
114{
115 bool containsGapLinePointStart = srcOutline.contains(gapLine.p1());
116 bool containsGapLinePointEnd = srcOutline.contains(gapLine.p2());
117
118 // if should skip if there is exactly one gap line point inside the shape
119 bool exactlyOneGapLinePointInside = (containsGapLinePointStart != containsGapLinePointEnd);
120 bool bothGapLinePointsInside = containsGapLinePointStart && containsGapLinePointEnd;
121
122 if (exactlyOneGapLinePointInside) {
123 return false;
124 }
125
126 bool crossesGapLine = KisAlgebra2D::getLineSegmentCrossingLineIndexes(leftLine, srcOutline).count() > 0
127 || KisAlgebra2D::getLineSegmentCrossingLineIndexes(rightLine, srcOutline).count() > 0;
128
129
130 bool containsPointWithinGap = false;
131 Q_FOREACH(QPointF p, srcOutline.toFillPolygon()) {
132 if (gapLinePolygon.containsPoint(p, Qt::WindingFill)) {
133 containsPointWithinGap = true;
134 break;
135 }
136 }
137
138 if (!bothGapLinePointsInside && !crossesGapLine && !containsPointWithinGap) {
139 return false;
140 }
141 return true;
142}
143
144void CutThroughShapeStrategy::finishInteraction(Qt::KeyboardModifiers modifiers)
145{
147
148
149 KisCanvas2 *kisCanvas = static_cast<KisCanvas2 *>(tool()->canvas());
151 const QTransform booleanWorkaroundTransform = KritaUtils::pathShapeBooleanSpaceWorkaround(kisCanvas->image());
152
153 QList<QPainterPath> srcOutlines;
154 QRectF outlineRect;
155
156 if (m_allShapes.length() == 0) {
157 qCritical() << "No shapes are available";
158 return;
159 }
160
161 Q_FOREACH (KoShape *shape, m_allShapes) {
162
163 QPainterPath outlineHere =
164 booleanWorkaroundTransform.map(
165 shape->absoluteTransformation().map(
166 shape->outline()));
167
168 srcOutlines << outlineHere;
169 outlineRect |= outlineHere.boundingRect();//booleanWorkaroundTransform.map(shape->absoluteOutlineRect()).boundingRect();
170 }
171
172 if (outlineRect.isEmpty()) {
173 //qCritical() << "The outline rect is empty";
174 return;
175 }
176
177 QRectF outlineRectBigger = kisGrowRect(outlineRect, 10);
178 QRect outlineRectBiggerInt = outlineRectBigger.toRect();
179
180 QLineF gapLine = QLineF(m_startPoint, m_endPoint);
181 qreal eps = 0.0000001;
182 if (gapLine.length() < eps) {
183 return;
184 }
185
187
188 QList<QLineF> gapLines = KisAlgebra2D::getParallelLines(gapLine, gutterWidth/2);
189
190 gapLine = booleanWorkaroundTransform.map(gapLine);
191 gapLines[0] = booleanWorkaroundTransform.map(gapLines[0]);
192 gapLines[1] = booleanWorkaroundTransform.map(gapLines[1]);
193
194 QLineF leftLine = gapLines[0];
195 QLineF rightLine = gapLines[1];
196
197 QLineF leftLineLong = leftLine;
198 QLineF rightLineLong = rightLine;
199
200
201
202 KisAlgebra2D::cropLineToRect(leftLineLong, outlineRectBiggerInt, true, true);
203 KisAlgebra2D::cropLineToRect(rightLineLong, outlineRectBiggerInt, true, true);
204
205 QScopedPointer<KUndo2Command> cmd = QScopedPointer<KUndo2Command>(new KUndo2Command(kundo2_i18n("Knife tool: cut through shapes")));
206
207
208 new KoKeepShapesSelectedCommand(m_selectedShapes, {}, kisCanvas->selectedShapesProxy(), false, cmd.data());
209
210
211 if (leftLine.length() == 0 || rightLine.length() == 0) {
212 KIS_SAFE_ASSERT_RECOVER_RETURN(gapLine.length() != 0 && gapLines[0].length() != 0 && gapLines[1].length() != 0 && "Original gap lines shouldn't be empty at this point");
213 // looks like *all* shapes need to be cut out
214
215 tool()->canvas()->shapeController()->removeShapes(m_allShapes, cmd.data());
216 tool()->canvas()->addCommand(cmd.take());
217 return;
218 }
219
220
221 QList<QPainterPath> paths = KisAlgebra2D::getPathsFromRectangleCutThrough(QRectF(outlineRectBiggerInt), leftLineLong, rightLineLong);
222 QPainterPath left = paths[0];
223 QPainterPath right = paths[1];
224
225 QList<QPainterPath> pathsOpposite = KisAlgebra2D::getPathsFromRectangleCutThrough(QRectF(outlineRectBiggerInt), rightLineLong, leftLineLong);
226 QPainterPath leftOpposite = pathsOpposite[0];
227 QPainterPath rightOpposite = pathsOpposite[1];
228
229 QList<KoShape*> newSelectedShapes;
230
231 QList<KoShape*> shapesToRemove;
232
233 QTransform booleanWorkaroundTransformInverted = booleanWorkaroundTransform.inverted();
234
235 QRectF gapLineLeftRect = KisAlgebra2D::createRectFromCorners(leftLine); // warning! can be empty for perfectly horizontal/vertical lines
236 QRectF gapLineRightRect = KisAlgebra2D::createRectFromCorners(rightLine);
237 QRectF gapLineRect = gapLineLeftRect | gapLineRightRect; // will not be empty if the gutterWidth > 0
238 bool checkGapLineRect = !gapLineRect.isEmpty();
239 QPolygonF gapLinePolygon = QPolygonF({leftLine.p1(), leftLine.p2(), rightLine.p2(), rightLine.p1(), leftLine.p1()});
240
241 int affectedShapes = 0;
242
243 for (int i = 0; i < srcOutlines.size(); i++) {
244
245 KoShape* referenceShape = m_allShapes[i];
246 bool wasSelected = m_selectedShapes.contains(referenceShape);
247
248 bool skipThisShape = !willShapeBeCutGeneral(referenceShape, srcOutlines[i], leftOpposite.boundingRect(), rightOpposite.boundingRect(), checkGapLineRect, gapLineRect);
249 skipThisShape = skipThisShape || !willShapeBeCutPrecise(srcOutlines[i], gapLine, leftLine, rightLine, gapLinePolygon);
250
251 if (skipThisShape) {
252 if (wasSelected) {
253 newSelectedShapes << referenceShape;
254 }
255 continue;
256 }
257
258 affectedShapes++;
259
260
261 QPainterPath leftPath = srcOutlines[i] & left;
262 QPainterPath rightPath = srcOutlines[i] & right;
263
264 QList<QPainterPath> bothSides;
265 bothSides << leftPath << rightPath;
266
267
268 Q_FOREACH(QPainterPath path, bothSides) {
269 if (path.isEmpty()) {
270 continue;
271 }
272
273 // comment copied from another place:
274 // there is a bug in Qt, sometimes it leaves the resulting
275 // outline open, so just close it explicitly.
276 path.closeSubpath();
277 // this is needed because Qt linearize curves; this allows for a
278 // "sane" linearization instead of a very blocky appearance
279 path = booleanWorkaroundTransformInverted.map(path);
280 QScopedPointer<KoPathShape> shape = QScopedPointer<KoPathShape>(KoPathShape::createShapeFromPainterPath(path));
281 shape->closeMerge();
282
283 if (shape->boundingRect().isEmpty()) {
284 continue;
285 }
286
287 shape->setBackground(referenceShape->background());
288 shape->setStroke(referenceShape->stroke());
289 shape->setZIndex(referenceShape->zIndex());
290
291 KoShapeContainer *parent = referenceShape->parent();
292
293 if (wasSelected) {
294 newSelectedShapes << shape.data();
295 }
296
297 tool()->canvas()->shapeController()->addShapeDirect(shape.take(), parent, cmd.data());
298
299 }
300
301 // that happens no matter if there was any non-empty shape
302 // because if there is none, maybe they just were underneath the gap
303 shapesToRemove << m_allShapes[i];
304
305 }
306
307 if (affectedShapes > 0) {
308 tool()->canvas()->shapeController()->removeShapes(shapesToRemove, cmd.data());
309 new KoKeepShapesSelectedCommand({}, newSelectedShapes, tool()->canvas()->selectedShapesProxy(), true, cmd.data());
310 tool()->canvas()->addCommand(cmd.take());
311 }
312
313
314
315}
316
317void CutThroughShapeStrategy::paint(QPainter &painter, const KoViewConverter &converter)
318{
319 painter.save();
320
321 QColor semitransparentGray = QColor(Qt::darkGray);
322 semitransparentGray.setAlphaF(0.6);
323 QPen pen = QPen(QBrush(semitransparentGray), 2);
324 painter.setPen(pen);
325
326 painter.setRenderHint(QPainter::RenderHint::Antialiasing, true);
327
329
330 QLineF gutterCenterLine = QLineF(m_startPoint, m_endPoint);
331 gutterCenterLine = converter.documentToView().map(gutterCenterLine);
332 QLineF gutterWidthHelperLine = QLineF(QPointF(0, 0), QPointF(gutterWidth, 0));
333 gutterWidthHelperLine = converter.documentToView().map(gutterWidthHelperLine);
334
335 gutterWidth = gutterWidthHelperLine.length();
336
337 QList<QLineF> gutterLines = KisAlgebra2D::getParallelLines(gutterCenterLine, gutterWidth/2);
338
339 QLineF gutterLine1 = gutterLines.length() > 0 ? gutterLines[0] : gutterCenterLine;
340 QLineF gutterLine2 = gutterLines.length() > 1 ? gutterLines[1] : gutterCenterLine;
341
342
343 painter.drawLine(gutterLine1);
344 painter.drawLine(gutterLine2);
345
346 QRectF arcRect1 = QRectF(gutterCenterLine.p1() - QPointF(gutterWidth/2, gutterWidth/2), gutterCenterLine.p1() + QPointF(gutterWidth/2, gutterWidth/2));
347 QRectF arcRect2 = QRectF(gutterCenterLine.p2() - QPointF(gutterWidth/2, gutterWidth/2), gutterCenterLine.p2() + QPointF(gutterWidth/2, gutterWidth/2));
348
349 int qtAngleFactor = 16;
350 int qtHalfCircle = qtAngleFactor*180;
351
352 painter.drawArc(arcRect1, -qtAngleFactor*kisRadiansToDegrees(KisAlgebra2D::directionBetweenPoints(gutterCenterLine.p1(), gutterLine1.p1(), 0)), qtHalfCircle);
353 painter.drawArc(arcRect2, -qtAngleFactor*kisRadiansToDegrees(KisAlgebra2D::directionBetweenPoints(gutterCenterLine.p2(), gutterLine1.p2(), 0)), -qtHalfCircle);
354
355
356 int xLength = 3;
357 qreal xLengthEllipse = 2*qSqrt(2);
358
359 if (false) { // drawing X
360 painter.drawLine({QLineF(gutterCenterLine.p1() - QPointF(xLength, xLength), gutterCenterLine.p1() + QPointF(xLength, xLength))});
361 painter.drawLine({QLineF(gutterCenterLine.p2() - QPointF(xLength, xLength), gutterCenterLine.p2() + QPointF(xLength, xLength))});
362
363 painter.drawLine({QLineF(gutterCenterLine.p1() - QPointF(xLength, -xLength), gutterCenterLine.p1() + QPointF(xLength, -xLength))});
364 painter.drawLine({QLineF(gutterCenterLine.p2() - QPointF(xLength, -xLength), gutterCenterLine.p2() + QPointF(xLength, -xLength))});
365 }
366
367 // ellipse at the both ends of the gutter center line
368 painter.drawEllipse(gutterCenterLine.p1(), xLengthEllipse, xLengthEllipse);
369 painter.drawEllipse(gutterCenterLine.p2(), xLengthEllipse, xLengthEllipse);
370
371
372
373 pen.setWidth(1);
374 semitransparentGray.setAlphaF(0.2);
375 pen.setColor(semitransparentGray);
376
377 painter.setPen(pen);
378
379 painter.drawLine(gutterCenterLine);
380
381 painter.restore();
382}
383
385{
386 KisCanvas2 *kisCanvas = static_cast<KisCanvas2 *>(tool()->canvas());
388 QLineF helperGapWidthLine = QLineF(QPointF(0, 0), QPointF(0, m_width.widthForAngleInPixels(lineAngle)));
389 QLineF helperGapWidthLineTransformed = kisCanvas->coordinatesConverter()->imageToDocument(helperGapWidthLine);
390 return helperGapWidthLineTransformed.length();
391}
392
393qreal CutThroughShapeStrategy::calculateLineAngle(QPointF start, QPointF end)
394{
395 QPointF vec = end - start;
396 qreal angleDegrees = KisAlgebra2D::wrapValue(kisRadiansToDegrees(std::atan2(vec.y(), vec.x())), 0.0, 360.0);
397 return angleDegrees;
398}
QPointF snapEndPoint(const QPointF &startPoint, const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers)
const Params2D p
bool willShapeBeCutGeneral(KoShape *referenceShape, const QPainterPath &srcOutline, const QRectF &leftOppositeRect, const QRectF &rightOppositeRect, bool checkGapLineRect, const QRectF &gapLineRect)
void finishInteraction(Qt::KeyboardModifiers modifiers) override
qreal calculateLineAngle(QPointF start, QPointF end)
bool willShapeBeCutPrecise(const QPainterPath &srcOutline, const QLineF gapLine, const QLineF &leftLine, const QLineF &rightLine, const QPolygonF &gapLinePolygon)
QList< KoShape * > m_selectedShapes
void handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) override
KUndo2Command * createCommand() override
CutThroughShapeStrategy(KoToolBase *tool, KoSelection *selection, const QList< KoShape * > &allShapes, QPointF startPoint, const GutterWidthsConfig &width)
qreal gutterWidthInDocumentCoordinates(qreal lineAngle)
void paint(QPainter &painter, const KoViewConverter &converter) override
qreal widthForAngleInPixels(qreal lineAngleDegrees)
KisSelectedShapesProxy selectedShapesProxy
KisCoordinatesConverter * coordinatesConverter
KisImageWSP image() const
_Private::Traits< T >::Result imageToDocument(const T &obj) const
QPointer< KoShapeController > shapeController
virtual void updateCanvas(const QRectF &rc)=0
virtual void addCommand(KUndo2Command *command)=0
virtual KoSelectedShapesProxy * selectedShapesProxy() const =0
selectedShapesProxy() is a special interface for keeping a persistent connections to selectionChanged...
static KoPathShape * createShapeFromPainterPath(const QPainterPath &path)
Creates path shape from given QPainterPath.
const QList< KoShape * > selectedEditableShapes() const
virtual QPainterPath outline() const
Definition KoShape.cpp:554
virtual KoShapeStrokeModelSP stroke() const
Definition KoShape.cpp:885
KoShapeContainer * parent() const
Definition KoShape.cpp:857
QTransform absoluteTransformation() const
Definition KoShape.cpp:330
virtual QSharedPointer< KoShapeBackground > background() const
Definition KoShape.cpp:754
qint16 zIndex() const
Definition KoShape.cpp:524
KoCanvasBase * canvas() const
Returns the canvas the tool is working on.
virtual QPointF documentToView(const QPointF &documentPoint) const
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
const qreal eps
T kisGrowRect(const T &rect, U offset)
Definition kis_global.h:186
T kisRadiansToDegrees(T radians)
Definition kis_global.h:181
PointType snapToClosestNiceAngle(PointType point, PointType startPoint, qreal angle=(2 *M_PI)/24)
Definition kis_global.h:209
T kisDegreesToRadians(T degrees)
Definition kis_global.h:176
KUndo2MagicString kundo2_i18n(const char *text)
T wrapValue(T value, T wrapBounds)
QList< QPainterPath > getPathsFromRectangleCutThrough(const QRectF &rect, const QLineF &leftLine, const QLineF &rightLine)
getPathsFromRectangleCutThrough get paths defining both sides of a rectangle cut through using two (s...
void accumulateBounds(const Point &pt, Rect *bounds)
qreal directionBetweenPoints(const QPointF &p1, const QPointF &p2, qreal defaultAngle)
void cropLineToRect(QLineF &line, const QRect rect, bool extendFirst, bool extendSecond)
Crop line to rect; if it doesn't intersect, just return an empty line (QLineF()).
QList< QLineF > getParallelLines(const QLineF &line, const qreal distance)
QList< int > getLineSegmentCrossingLineIndexes(const QLineF &line, const QPainterPath &shape)
PointTypeTraits< Point >::rect_type createRectFromCorners(Point corner1, Point corner2)
QTransform pathShapeBooleanSpaceWorkaround(KisImageSP image)