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>
29
30
31CutThroughShapeStrategy::CutThroughShapeStrategy(KoToolBase *tool, KoSelection *selection, const QList<KoShape *> &shapes, QPointF startPoint, const GutterWidthsConfig &width)
33 , m_startPoint(startPoint)
34 , m_endPoint(startPoint)
35 , m_width(width)
36{
38 m_allShapes = shapes;
39}
40
45
47{
48 // TODO: undoing
49 return 0;
50}
51
52QPointF snapEndPoint(const QPointF &startPoint, const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) {
53
54 QPointF nicePoint = snapToClosestNiceAngle(mouseLocation, startPoint); // by default the function gives you 15 degrees increments
55
56 if (modifiers & Qt::KeyboardModifier::ShiftModifier) {
57 return nicePoint;
58 if (qAbs(mouseLocation.x() - startPoint.x()) >= qAbs(mouseLocation.y() - startPoint.y())) {
59 // do horizontal line
60 return QPointF(mouseLocation.x(), startPoint.y());
61 } else {
62 return QPointF(startPoint.x(), mouseLocation.y());
63 }
64 }
65 QLineF line = QLineF(startPoint, mouseLocation);
66 qreal angle = line.angleTo(QLineF(startPoint, nicePoint));
67 qreal eps = kisDegreesToRadians(2.0f);
68 if (angle < eps) {
69 return nicePoint;
70 }
71 return mouseLocation;
72}
73
74void CutThroughShapeStrategy::handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers)
75{
76 m_endPoint = snapEndPoint(m_startPoint, mouseLocation, modifiers);
77 QRectF dirtyRect;
80 dirtyRect = kisGrowRect(dirtyRect, gutterWidthInDocumentCoordinates(calculateLineAngle(m_startPoint, m_endPoint))); // twice as much as it should need to account for lines showing the effect
81
82 QRectF accumulatedWithPrevious = m_previousLineDirtyRect | dirtyRect;
83
84 if (tool() && tool()->canvas()) {
85 tool()->canvas()->updateCanvas(accumulatedWithPrevious);
86 }
87 m_previousLineDirtyRect = dirtyRect;
88
89}
90
91
92bool CutThroughShapeStrategy::willShapeBeCutGeneral(KoShape* referenceShape, const QPainterPath& srcOutline, bool checkGapLineRect, const QRectF& gapLineRect)
93{
94 if (dynamic_cast<KoSvgTextShape*>(referenceShape)) {
95 // skip all text
96 return false;
97 }
98
99
100 if (checkGapLineRect && (srcOutline.boundingRect() & gapLineRect).isEmpty()) {
101 // the gap lines can't cross the shape since their bounding rects don't cross it
102 return false;
103 }
104
105 return true;
106}
107
108bool CutThroughShapeStrategy::willShapeBeCutPrecise(const QPainterPath& srcOutline, const QLineF gapLine, const QLineF& leftLine, const QLineF& rightLine, const QPolygonF& gapLinePolygon)
109{
110 bool containsGapLinePointStart = srcOutline.contains(gapLine.p1());
111 bool containsGapLinePointEnd = srcOutline.contains(gapLine.p2());
112
113 // if should skip if there is exactly one gap line point inside the shape
114 bool exactlyOneGapLinePointInside = (containsGapLinePointStart != containsGapLinePointEnd);
115 bool bothGapLinePointsInside = containsGapLinePointStart && containsGapLinePointEnd;
116
117 if (exactlyOneGapLinePointInside) {
118 return false;
119 }
120
121 bool crossesGapLine = KisAlgebra2D::getLineSegmentCrossingLineIndexes(leftLine, srcOutline).count() > 0
122 || KisAlgebra2D::getLineSegmentCrossingLineIndexes(rightLine, srcOutline).count() > 0;
123
124
125 // it doesn't contain exactly one point, therefore it contains either both or none.
126 // if it contains both, it will be true.
127 // if it contains none:
128 // if it crosses either gap line, it will be true
129 // if any of the shape points are inside the gap, it will be true
130 // otherwise it's false
131
132 if (bothGapLinePointsInside) {
133 return true;
134 }
135
136 if (crossesGapLine) {
137 return true;
138 }
139
140 Q_FOREACH(QPointF p, srcOutline.toFillPolygon()) {
141 if (gapLinePolygon.containsPoint(p, Qt::WindingFill)) {
142 // a shape point is inside the gap shape
143 return true;
144 }
145 }
146
147 return false;
148
149}
150
151void CutThroughShapeStrategy::initializeOutlineObjects(const QTransform &booleanWorkaroundTransform, QList<KoShape *> allShapes, QList<QPainterPath> &outSrcOutlines, QRectF &outOutlineRect)
152{
153 Q_FOREACH (KoShape *shape, allShapes) {
154
155 QPainterPath outlineHere =
156 booleanWorkaroundTransform.map(
157 shape->absoluteTransformation().map(
158 shape->outline()));
159
160 outSrcOutlines << outlineHere;
161 outOutlineRect |= outlineHere.boundingRect();
162 }
163}
164
165void CutThroughShapeStrategy::initializeGapShapes(QRectF outlineRect, QLineF leftLine, QLineF rightLine, QPainterPath& outLeft, QPainterPath& outRight,
166 QRectF& outGapLineRect, QPolygonF& outGapLinePolygon)
167{
168
169
170 QRect outlineRectBiggerInt = kisGrowRect(outlineRect, 10).toRect();
171 QLineF leftLineLong = leftLine;
172 QLineF rightLineLong = rightLine;
173
174
175 KisAlgebra2D::cropLineToRect(leftLineLong, outlineRectBiggerInt, true, true);
176 KisAlgebra2D::cropLineToRect(rightLineLong, outlineRectBiggerInt, true, true);
177
178
179 QList<QPainterPath> paths = KisAlgebra2D::getPathsFromRectangleCutThrough(QRectF(outlineRectBiggerInt), leftLineLong, rightLineLong);
180 outLeft = paths[0];
181 outRight = paths[1];
182
183 outGapLineRect = KisAlgebra2D::createRectFromCorners(leftLine) | KisAlgebra2D::createRectFromCorners(rightLine); // will not be empty if the gutterWidth > 0
184
185 outGapLinePolygon = QPolygonF({leftLine.p1(), leftLine.p2(), rightLine.p2(), rightLine.p1(), leftLine.p1()});
186
187}
188
189void CutThroughShapeStrategy::finishInteraction(Qt::KeyboardModifiers modifiers)
190{
192
193
194 KisCanvas2 *kisCanvas = static_cast<KisCanvas2 *>(tool()->canvas());
196 const QTransform booleanWorkaroundTransform = KritaUtils::pathShapeBooleanSpaceWorkaround(kisCanvas->image());
197
198 QList<QPainterPath> srcOutlines;
199 QRectF outlineRect;
200
201 if (m_allShapes.length() == 0) {
202 qCritical() << "No shapes are available";
203 return;
204 }
205
206 initializeOutlineObjects(booleanWorkaroundTransform, m_allShapes, srcOutlines, outlineRect);
207
208
209
210 if (outlineRect.isEmpty()) {
211 //qCritical() << "The outline rect is empty";
212 return;
213 }
214
215
216 QLineF gapLine = QLineF(m_startPoint, m_endPoint);
217 qreal eps = 0.0000001;
218 if (gapLine.length() < eps) {
219 return;
220 }
221
223
224 QList<QLineF> gapLines = KisAlgebra2D::getParallelLines(gapLine, gutterWidth/2);
225
226 gapLine = booleanWorkaroundTransform.map(gapLine);
227 gapLines[0] = booleanWorkaroundTransform.map(gapLines[0]);
228 gapLines[1] = booleanWorkaroundTransform.map(gapLines[1]);
229
230 QLineF leftLine = gapLines[0];
231 QLineF rightLine = gapLines[1];
232
233
234 if (leftLine.length() == 0 || rightLine.length() == 0) {
235 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");
236 return;
237 }
238
239 // -------------
240
241 QPainterPath left, right;
242
243 QRectF gapLineRect;
244 QPolygonF gapLinePolygon;
245 initializeGapShapes(outlineRect, leftLine, rightLine, left, right, gapLineRect, gapLinePolygon);
246
247
248 bool checkGapLineRect = !gapLineRect.isEmpty();
249
250 QList<KoShape*> newSelectedShapes;
251 QList<KoShape*> shapesToRemove;
252 int affectedShapes = 0;
253 QTransform booleanWorkaroundTransformInverted = booleanWorkaroundTransform.inverted();
254
255
256 std::unique_ptr<KUndo2Command> cmd = std::unique_ptr<KUndo2Command>(new KUndo2Command(kundo2_i18n("Knife tool: cut through shapes")));
257 new KoKeepShapesSelectedCommand(m_selectedShapes, {}, kisCanvas->selectedShapesProxy(), false, cmd.get());
258
259
260 for (int i = 0; i < srcOutlines.size(); i++) {
261
262 KoShape* referenceShape = m_allShapes[i];
263 bool wasSelected = m_selectedShapes.contains(referenceShape);
264
265 bool skipThisShape = !willShapeBeCutGeneral(referenceShape, srcOutlines[i], checkGapLineRect, gapLineRect);
266 skipThisShape = skipThisShape || !willShapeBeCutPrecise(srcOutlines[i], gapLine, leftLine, rightLine, gapLinePolygon);
267
268 if (skipThisShape) {
269 if (wasSelected) {
270 newSelectedShapes << referenceShape;
271 }
272 continue;
273 }
274
275 affectedShapes++;
276
277
278 QPainterPath leftPath = srcOutlines[i] & left;
279 QPainterPath rightPath = srcOutlines[i] & right;
280
281 QList<QPainterPath> bothSides;
282 bothSides << leftPath << rightPath;
283
284
285 Q_FOREACH(QPainterPath path, bothSides) {
286 if (path.isEmpty()) {
287 continue;
288 }
289
290 // comment copied from another place:
291 // there is a bug in Qt, sometimes it leaves the resulting
292 // outline open, so just close it explicitly.
293 path.closeSubpath();
294 // this is needed because Qt linearize curves; this allows for a
295 // "sane" linearization instead of a very blocky appearance
296 path = booleanWorkaroundTransformInverted.map(path);
297 std::unique_ptr<KoPathShape> shape = std::unique_ptr<KoPathShape>(KoPathShape::createShapeFromPainterPath(path));
298 shape->closeMerge();
299
300 if (shape->boundingRect().isEmpty()) {
301 continue;
302 }
303
304 shape->setBackground(referenceShape->background());
305 shape->setStroke(referenceShape->stroke());
306 shape->setZIndex(referenceShape->zIndex());
307
308 KoShapeContainer *parent = referenceShape->parent();
309
310 if (wasSelected) {
311 newSelectedShapes << shape.get();
312 }
313
314 tool()->canvas()->shapeController()->addShapeDirect(shape.release(), parent, cmd.get());
315
316 }
317
318 // that happens no matter if there was any non-empty shape
319 // because if there is none, maybe they just were underneath the gap
320 shapesToRemove << m_allShapes[i];
321
322 }
323
324 if (affectedShapes > 0) {
325 tool()->canvas()->shapeController()->removeShapes(shapesToRemove, cmd.get());
326 new KoKeepShapesSelectedCommand({}, newSelectedShapes, tool()->canvas()->selectedShapesProxy(), true, cmd.get());
327 tool()->canvas()->addCommand(cmd.release());
328 }
329
330
331
332}
333
334void CutThroughShapeStrategy::paint(QPainter &painter, const KoViewConverter &converter, const KoColorDisplayRendererInterface *displayRendererInterface)
335{
336 painter.save();
337
338 KoColor c;
339 c.fromQColor(Qt::darkGray);
340 QColor semitransparentGray = displayRendererInterface->convertColorToDisplayColorSpace(c);
341 semitransparentGray.setAlphaF(0.6);
342 QPen pen = QPen(QBrush(semitransparentGray), 2);
343 painter.setPen(pen);
344
345 painter.setRenderHint(QPainter::RenderHint::Antialiasing, true);
346
348
349 QLineF gutterCenterLine = QLineF(m_startPoint, m_endPoint);
350 gutterCenterLine = converter.documentToView().map(gutterCenterLine);
351 QLineF gutterWidthHelperLine = QLineF(QPointF(0, 0), QPointF(gutterWidth, 0));
352 gutterWidthHelperLine = converter.documentToView().map(gutterWidthHelperLine);
353
354 gutterWidth = gutterWidthHelperLine.length();
355
356 QList<QLineF> gutterLines = KisAlgebra2D::getParallelLines(gutterCenterLine, gutterWidth/2);
357
358 QLineF gutterLine1 = gutterLines.length() > 0 ? gutterLines[0] : gutterCenterLine;
359 QLineF gutterLine2 = gutterLines.length() > 1 ? gutterLines[1] : gutterCenterLine;
360
361
362 painter.drawLine(gutterLine1);
363 painter.drawLine(gutterLine2);
364
365 QRectF arcRect1 = QRectF(gutterCenterLine.p1() - QPointF(gutterWidth/2, gutterWidth/2), gutterCenterLine.p1() + QPointF(gutterWidth/2, gutterWidth/2));
366 QRectF arcRect2 = QRectF(gutterCenterLine.p2() - QPointF(gutterWidth/2, gutterWidth/2), gutterCenterLine.p2() + QPointF(gutterWidth/2, gutterWidth/2));
367
368 int qtAngleFactor = 16;
369 int qtHalfCircle = qtAngleFactor*180;
370
371 painter.drawArc(arcRect1, -qtAngleFactor*kisRadiansToDegrees(KisAlgebra2D::directionBetweenPoints(gutterCenterLine.p1(), gutterLine1.p1(), 0)), qtHalfCircle);
372 painter.drawArc(arcRect2, -qtAngleFactor*kisRadiansToDegrees(KisAlgebra2D::directionBetweenPoints(gutterCenterLine.p2(), gutterLine1.p2(), 0)), -qtHalfCircle);
373
374
375 int xLength = 3;
376 qreal xLengthEllipse = 2*qSqrt(2);
377
378 if (false) { // drawing X
379 painter.drawLine({QLineF(gutterCenterLine.p1() - QPointF(xLength, xLength), gutterCenterLine.p1() + QPointF(xLength, xLength))});
380 painter.drawLine({QLineF(gutterCenterLine.p2() - QPointF(xLength, xLength), gutterCenterLine.p2() + QPointF(xLength, xLength))});
381
382 painter.drawLine({QLineF(gutterCenterLine.p1() - QPointF(xLength, -xLength), gutterCenterLine.p1() + QPointF(xLength, -xLength))});
383 painter.drawLine({QLineF(gutterCenterLine.p2() - QPointF(xLength, -xLength), gutterCenterLine.p2() + QPointF(xLength, -xLength))});
384 }
385
386 // ellipse at the both ends of the gutter center line
387 painter.drawEllipse(gutterCenterLine.p1(), xLengthEllipse, xLengthEllipse);
388 painter.drawEllipse(gutterCenterLine.p2(), xLengthEllipse, xLengthEllipse);
389
390
391
392 pen.setWidth(1);
393 semitransparentGray.setAlphaF(0.2);
394 pen.setColor(semitransparentGray);
395
396 painter.setPen(pen);
397
398 painter.drawLine(gutterCenterLine);
399
400 painter.restore();
401}
402
404{
405 KisCanvas2 *kisCanvas = static_cast<KisCanvas2 *>(tool()->canvas());
407 QLineF helperGapWidthLine = QLineF(QPointF(0, 0), QPointF(0, m_width.widthForAngleInPixels(lineAngle)));
408 QLineF helperGapWidthLineTransformed = kisCanvas->coordinatesConverter()->imageToDocument(helperGapWidthLine);
409 return helperGapWidthLineTransformed.length();
410}
411
412qreal CutThroughShapeStrategy::calculateLineAngle(QPointF start, QPointF end)
413{
414 QPointF vec = end - start;
415 qreal angleDegrees = KisAlgebra2D::wrapValue(kisRadiansToDegrees(std::atan2(vec.y(), vec.x())), 0.0, 360.0);
416 return angleDegrees;
417}
QPointF snapEndPoint(const QPointF &startPoint, const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers)
const Params2D p
void finishInteraction(Qt::KeyboardModifiers modifiers) override
static void initializeGapShapes(QRectF outlineRect, QLineF leftLine, QLineF rightLine, QPainterPath &outLeft, QPainterPath &outRight, QRectF &outGapLineRect, QPolygonF &outGapLinePolygon)
qreal calculateLineAngle(QPointF start, QPointF end)
static void initializeOutlineObjects(const QTransform &booleanWorkaroundTransform, QList< KoShape * > allShapes, QList< QPainterPath > &outSrcOutlines, QRectF &outOutlineRect)
static 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
void paint(QPainter &painter, const KoViewConverter &converter, const KoColorDisplayRendererInterface *displayRendererInterface) override
KUndo2Command * createCommand() override
CutThroughShapeStrategy(KoToolBase *tool, KoSelection *selection, const QList< KoShape * > &allShapes, QPointF startPoint, const GutterWidthsConfig &width)
static bool willShapeBeCutGeneral(KoShape *referenceShape, const QPainterPath &srcOutline, bool checkGapLineRect, const QRectF &gapLineRect)
qreal gutterWidthInDocumentCoordinates(qreal lineAngle)
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...
virtual QColor convertColorToDisplayColorSpace(const KoColor color) const =0
convertColorToDisplayColorSpace
void fromQColor(const QColor &c)
Convenient function for converting from a QColor.
Definition KoColor.cpp:213
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)