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
28
29CutThroughShapeStrategy::CutThroughShapeStrategy(KoToolBase *tool, KoSelection *selection, const QList<KoShape *> &shapes, QPointF startPoint, const GutterWidthsConfig &width)
31 , m_startPoint(startPoint)
32 , m_endPoint(startPoint)
33 , m_width(width)
34{
36 m_allShapes = shapes;
37}
38
43
45{
46 // TODO: undoing
47 return 0;
48}
49
50QPointF snapEndPoint(const QPointF &startPoint, const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) {
51
52 QPointF nicePoint = snapToClosestNiceAngle(mouseLocation, startPoint); // by default the function gives you 15 degrees increments
53
54 if (modifiers & Qt::KeyboardModifier::ShiftModifier) {
55 return nicePoint;
56 if (qAbs(mouseLocation.x() - startPoint.x()) >= qAbs(mouseLocation.y() - startPoint.y())) {
57 // do horizontal line
58 return QPointF(mouseLocation.x(), startPoint.y());
59 } else {
60 return QPointF(startPoint.x(), mouseLocation.y());
61 }
62 }
63 QLineF line = QLineF(startPoint, mouseLocation);
64 qreal angle = line.angleTo(QLineF(startPoint, nicePoint));
65 qreal eps = kisDegreesToRadians(2.0f);
66 if (angle < eps) {
67 return nicePoint;
68 }
69 return mouseLocation;
70}
71
72void CutThroughShapeStrategy::handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers)
73{
74 m_endPoint = snapEndPoint(m_startPoint, mouseLocation, modifiers);
75 QRectF dirtyRect;
78 dirtyRect = kisGrowRect(dirtyRect, gutterWidthInDocumentCoordinates(calculateLineAngle(m_startPoint, m_endPoint))); // twice as much as it should need to account for lines showing the effect
79
80 QRectF accumulatedWithPrevious = m_previousLineDirtyRect | dirtyRect;
81
82 tool()->canvas()->updateCanvas(accumulatedWithPrevious);
83 m_previousLineDirtyRect = dirtyRect;
84
85}
86
87void CutThroughShapeStrategy::finishInteraction(Qt::KeyboardModifiers modifiers)
88{
90
91
92 KisCanvas2 *kisCanvas = static_cast<KisCanvas2 *>(tool()->canvas());
94 const QTransform booleanWorkaroundTransform = KritaUtils::pathShapeBooleanSpaceWorkaround(kisCanvas->image());
95
96 QList<QPainterPath> srcOutlines;
97 QRectF outlineRect;
98
99 if (m_allShapes.length() == 0) {
100 qCritical() << "No shapes are available";
101 return;
102 }
103
104 Q_FOREACH (KoShape *shape, m_allShapes) {
105
106 QPainterPath outlineHere =
107 booleanWorkaroundTransform.map(
108 shape->absoluteTransformation().map(
109 shape->outline()));
110
111 srcOutlines << outlineHere;
112 outlineRect |= outlineHere.boundingRect();//booleanWorkaroundTransform.map(shape->absoluteOutlineRect()).boundingRect();
113 }
114
115 if (outlineRect.isEmpty()) {
116 //qCritical() << "The outline rect is empty";
117 return;
118 }
119
120 QRectF outlineRectBigger = kisGrowRect(outlineRect, 10);
121 QRect outlineRectBiggerInt = outlineRectBigger.toRect();
122
123 QLineF gapLine = QLineF(m_startPoint, m_endPoint);
124 qreal eps = 0.0000001;
125 if (gapLine.length() < eps) {
126 return;
127 }
128
130
131 QList<QLineF> gapLines = KisAlgebra2D::getParallelLines(gapLine, gutterWidth/2);
132
133 gapLine = booleanWorkaroundTransform.map(gapLine);
134 gapLines[0] = booleanWorkaroundTransform.map(gapLines[0]);
135 gapLines[1] = booleanWorkaroundTransform.map(gapLines[1]);
136
137 QLineF leftLine = gapLines[0];
138 QLineF rightLine = gapLines[1];
139
140 QLineF leftLineLong = leftLine;
141 QLineF rightLineLong = rightLine;
142
143
144
145 KisAlgebra2D::cropLineToRect(leftLineLong, outlineRectBiggerInt, true, true);
146 KisAlgebra2D::cropLineToRect(rightLineLong, outlineRectBiggerInt, true, true);
147
148 KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Knife tool: cut through shapes"));
149
150
151 new KoKeepShapesSelectedCommand(m_selectedShapes, {}, kisCanvas->selectedShapesProxy(), false, cmd);
152
153
154 if (leftLine.length() == 0 || rightLine.length() == 0) {
155 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");
156 // looks like *all* shapes need to be cut out
157
158 tool()->canvas()->shapeController()->removeShapes(m_allShapes, cmd);
159 tool()->canvas()->addCommand(cmd);
160 return;
161 }
162
163
164 QList<QPainterPath> paths = KisAlgebra2D::getPathsFromRectangleCutThrough(QRectF(outlineRectBiggerInt), leftLineLong, rightLineLong);
165 QPainterPath left = paths[0];
166 QPainterPath right = paths[1];
167
168 QList<QPainterPath> pathsOpposite = KisAlgebra2D::getPathsFromRectangleCutThrough(QRectF(outlineRectBiggerInt), rightLineLong, leftLineLong);
169 QPainterPath leftOpposite = pathsOpposite[0];
170 QPainterPath rightOpposite = pathsOpposite[1];
171
172 QList<KoShape*> newSelectedShapes;
173
174 QList<KoShape*> shapesToRemove;
175
176 QTransform booleanWorkaroundTransformInverted = booleanWorkaroundTransform.inverted();
177
178 QRectF gapLineLeftRect = KisAlgebra2D::createRectFromCorners(leftLine);
179 QRectF gapLineRightRect = KisAlgebra2D::createRectFromCorners(rightLine);
180
181
182 for (int i = 0; i < srcOutlines.size(); i++) {
183
184 KoShape* referenceShape = m_allShapes[i];
185 bool wasSelected = m_selectedShapes.contains(referenceShape);
186
187 if ((srcOutlines[i].boundingRect() & leftOpposite.boundingRect()).isEmpty()
188 || (srcOutlines[i].boundingRect() & rightOpposite.boundingRect()).isEmpty()) {
189 // there is nothing on one side
190 // everything is on the other, far away from the gap line
191 // it just makes it a bit faster when there is a whole lot of shapes
192
193 if (wasSelected) {
194 newSelectedShapes << referenceShape;
195 }
196 continue;
197 }
198
199 if ((srcOutlines[i].boundingRect() & gapLineLeftRect).isEmpty()
200 || (srcOutlines[i].boundingRect() & gapLineRightRect).isEmpty()) {
201 // the gap lines can't cross the shape since their bounding rects don't cross
202 if (wasSelected) {
203 newSelectedShapes << referenceShape;
204 }
205 continue;
206 }
207
208 // either one of the points is inside the path, or the line crosses one of the segments
209 bool containsGapLinePoint = srcOutlines[i].contains(leftLine.p1()) || srcOutlines[i].contains(leftLine.p2())
210 || srcOutlines[i].contains(rightLine.p1()) || srcOutlines[i].contains(rightLine.p2());
211 bool crossesGapLine = KisAlgebra2D::getLineSegmentCrossingLineIndexes(leftLine, srcOutlines[i]).count() > 0
212 || KisAlgebra2D::getLineSegmentCrossingLineIndexes(rightLine, srcOutlines[i]).count() > 0;
213 if (!containsGapLinePoint && !crossesGapLine) {
214
215 //qCritical() << "it doesn't cross the line!";
216 if (wasSelected) {
217 newSelectedShapes << referenceShape;
218 }
219 continue;
220 }
221
222
223
224
225 QPainterPath leftPath = srcOutlines[i] & left;
226 QPainterPath rightPath = srcOutlines[i] & right;
227
228 QList<QPainterPath> bothSides;
229 bothSides << leftPath << rightPath;
230
231
232 Q_FOREACH(QPainterPath path, bothSides) {
233 if (path.isEmpty()) {
234 continue;
235 }
236
237 // comment copied from another place:
238 // there is a bug in Qt, sometimes it leaves the resulting
239 // outline open, so just close it explicitly.
240 path.closeSubpath();
241 // this is needed because Qt linearize curves; this allows for a
242 // "sane" linearization instead of a very blocky appearance
243 path = booleanWorkaroundTransformInverted.map(path);
245
246 if (shape->boundingRect().isEmpty()) {
247 continue;
248 }
249
250 shape->setBackground(referenceShape->background());
251 shape->setStroke(referenceShape->stroke());
252 shape->setZIndex(referenceShape->zIndex());
253
254 KoShapeContainer *parent = referenceShape->parent();
255 tool()->canvas()->shapeController()->addShapeDirect(shape, parent, cmd);
256
257 if (wasSelected) {
258 newSelectedShapes << shape;
259 }
260
261
262 }
263
264 // that happens no matter if there was any non-empty shape
265 // because if there is none, maybe they just were underneath the gap
266 shapesToRemove << m_allShapes[i];
267
268 }
269
270 tool()->canvas()->shapeController()->removeShapes(shapesToRemove, cmd);
271 new KoKeepShapesSelectedCommand({}, newSelectedShapes, tool()->canvas()->selectedShapesProxy(), true, cmd);
272
273
274 tool()->canvas()->addCommand(cmd);
275
276
277
278}
279
280void CutThroughShapeStrategy::paint(QPainter &painter, const KoViewConverter &converter)
281{
282 painter.save();
283
284 QColor semitransparentGray = QColor(Qt::darkGray);
285 semitransparentGray.setAlphaF(0.6);
286 QPen pen = QPen(QBrush(semitransparentGray), 2);
287 painter.setPen(pen);
288
289 painter.setRenderHint(QPainter::RenderHint::Antialiasing, true);
290
292
293 QLineF gutterCenterLine = QLineF(m_startPoint, m_endPoint);
294 gutterCenterLine = converter.documentToView().map(gutterCenterLine);
295 QLineF gutterWidthHelperLine = QLineF(QPointF(0, 0), QPointF(gutterWidth, 0));
296 gutterWidthHelperLine = converter.documentToView().map(gutterWidthHelperLine);
297
298 gutterWidth = gutterWidthHelperLine.length();
299
300 QList<QLineF> gutterLines = KisAlgebra2D::getParallelLines(gutterCenterLine, gutterWidth/2);
301
302 QLineF gutterLine1 = gutterLines.length() > 0 ? gutterLines[0] : gutterCenterLine;
303 QLineF gutterLine2 = gutterLines.length() > 1 ? gutterLines[1] : gutterCenterLine;
304
305
306 painter.drawLine(gutterLine1);
307 painter.drawLine(gutterLine2);
308
309 QRectF arcRect1 = QRectF(gutterCenterLine.p1() - QPointF(gutterWidth/2, gutterWidth/2), gutterCenterLine.p1() + QPointF(gutterWidth/2, gutterWidth/2));
310 QRectF arcRect2 = QRectF(gutterCenterLine.p2() - QPointF(gutterWidth/2, gutterWidth/2), gutterCenterLine.p2() + QPointF(gutterWidth/2, gutterWidth/2));
311
312 int qtAngleFactor = 16;
313 int qtHalfCircle = qtAngleFactor*180;
314
315 painter.drawArc(arcRect1, -qtAngleFactor*kisRadiansToDegrees(KisAlgebra2D::directionBetweenPoints(gutterCenterLine.p1(), gutterLine1.p1(), 0)), qtHalfCircle);
316 painter.drawArc(arcRect2, -qtAngleFactor*kisRadiansToDegrees(KisAlgebra2D::directionBetweenPoints(gutterCenterLine.p2(), gutterLine1.p2(), 0)), -qtHalfCircle);
317
318
319 int xLength = 3;
320 qreal xLengthEllipse = 2*qSqrt(2);
321
322 if (false) { // drawing X
323 painter.drawLine({QLineF(gutterCenterLine.p1() - QPointF(xLength, xLength), gutterCenterLine.p1() + QPointF(xLength, xLength))});
324 painter.drawLine({QLineF(gutterCenterLine.p2() - QPointF(xLength, xLength), gutterCenterLine.p2() + QPointF(xLength, xLength))});
325
326 painter.drawLine({QLineF(gutterCenterLine.p1() - QPointF(xLength, -xLength), gutterCenterLine.p1() + QPointF(xLength, -xLength))});
327 painter.drawLine({QLineF(gutterCenterLine.p2() - QPointF(xLength, -xLength), gutterCenterLine.p2() + QPointF(xLength, -xLength))});
328 }
329
330 // ellipse at the both ends of the gutter center line
331 painter.drawEllipse(gutterCenterLine.p1(), xLengthEllipse, xLengthEllipse);
332 painter.drawEllipse(gutterCenterLine.p2(), xLengthEllipse, xLengthEllipse);
333
334
335
336 pen.setWidth(1);
337 semitransparentGray.setAlphaF(0.2);
338 pen.setColor(semitransparentGray);
339
340 painter.setPen(pen);
341
342 painter.drawLine(gutterCenterLine);
343
344 painter.restore();
345}
346
348{
349 KisCanvas2 *kisCanvas = static_cast<KisCanvas2 *>(tool()->canvas());
351 QLineF helperGapWidthLine = QLineF(QPointF(0, 0), QPointF(0, m_width.widthForAngleInPixels(lineAngle)));
352 QLineF helperGapWidthLineTransformed = kisCanvas->coordinatesConverter()->imageToDocument(helperGapWidthLine);
353 return helperGapWidthLineTransformed.length();
354}
355
356qreal CutThroughShapeStrategy::calculateLineAngle(QPointF start, QPointF end)
357{
358 QPointF vec = end - start;
359 qreal angleDegrees = KisAlgebra2D::wrapValue(kisRadiansToDegrees(std::atan2(vec.y(), vec.x())), 0.0, 360.0);
360 return angleDegrees;
361}
QPointF snapEndPoint(const QPointF &startPoint, const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers)
void finishInteraction(Qt::KeyboardModifiers modifiers) override
qreal calculateLineAngle(QPointF start, QPointF end)
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...
The position of a path point within a path shape.
Definition KoPathShape.h:63
QRectF boundingRect() const override
reimplemented
static KoPathShape * createShapeFromPainterPath(const QPainterPath &path)
Creates path shape from given QPainterPath.
const QList< KoShape * > selectedEditableShapes() const
void setZIndex(qint16 zIndex)
Definition KoShape.cpp:787
virtual QPainterPath outline() const
Definition KoShape.cpp:559
virtual KoShapeStrokeModelSP stroke() const
Definition KoShape.cpp:890
KoShapeContainer * parent() const
Definition KoShape.cpp:862
virtual void setStroke(KoShapeStrokeModelSP stroke)
Definition KoShape.cpp:904
QTransform absoluteTransformation() const
Definition KoShape.cpp:335
virtual void setBackground(QSharedPointer< KoShapeBackground > background)
Definition KoShape.cpp:751
virtual QSharedPointer< KoShapeBackground > background() const
Definition KoShape.cpp:759
qint16 zIndex() const
Definition KoShape.cpp:529
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)