Krita Source Code Documentation
Loading...
Searching...
No Matches
RemoveGutterStrategy.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 */
7
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>
27
28
29
30
31RemoveGutterStrategy::RemoveGutterStrategy(KoToolBase *tool, KoSelection *selection, const QList<KoShape *> &shapes, QPointF startPoint)
33 , m_startPoint(startPoint)
34 , m_endPoint(startPoint)
35{
37 m_allShapes = shapes;
38}
39
44
49
50void RemoveGutterStrategy::handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers)
51{
52 m_endPoint = mouseLocation;
53 QRectF dirtyRect;
56 QLineF l = QLine(QPoint(), QPoint(50, 50));
57
58 l = tool()->canvas()->viewConverter()->viewToDocument().map(l);
59 dirtyRect = kisGrowRect(dirtyRect, l.length()); // twice as much as it should need to account for lines showing the effect
60
61 QRectF accumulatedWithPrevious = m_previousLineDirtyRect | dirtyRect;
62
63 tool()->canvas()->updateCanvas(accumulatedWithPrevious);
64 m_previousLineDirtyRect = dirtyRect;
65}
66
67#ifdef KNIFE_DEBUG
68void convertShapeToDebugArray(const QPainterPath& shape) {
69 // useful to use with Geogebra
70 // TODO: to remove later
71 for (int i = 0; i < shape.elementCount(); i++) {
72 QPainterPath::Element el = shape.elementAt(i);
73 qCritical() << el.x << "\t" << el.y << "\t" << el.type;
74 }
75
76}
77
78void convertShapeToDebugArray(const QRectF& rect) {
79 // useful to use with Geogebra
80 // TODO: to remove later
81 QPolygonF poly = QPolygonF(rect);
82 for (int i = 0; i < poly.length(); i++) {
83 QPointF p = poly[i];
84 qCritical() << p.x() << "\t" << p.y();
85 }
86
87}
88
89void convertShapeToDebugArray(const QLineF& line) {
90 // useful to use with Geogebra
91 // TODO: to remove later
92 qCritical() << line.p1().x() << "\t" << line.p1().y();
93 qCritical() << line.p2().x() << "\t" << line.p2().y();
94
95}
96#endif
97
98void RemoveGutterStrategy::finishInteraction(Qt::KeyboardModifiers modifiers)
99{
101
102
103 KisCanvas2 *kisCanvas = static_cast<KisCanvas2 *>(tool()->canvas());
105 const QTransform booleanWorkaroundTransform = KritaUtils::pathShapeBooleanSpaceWorkaround(kisCanvas->image());
106
107 QList<QPainterPath> srcOutlines;
108 QList<QPainterPath> srcOutlinesOutside;
109
110 if (m_allShapes.length() == 0) {
111 qCritical() << "No shapes on the layer";
112 return;
113 }
114
115 QList<bool> isSelected = QList<bool>();
116 isSelected.reserve(m_allShapes.length());
117 for (int i = 0; i < m_allShapes.length(); i++) {
118 isSelected.append(m_selectedShapes.contains(m_allShapes[i]));
119 }
120
121 QLineF mouseLine = QLineF(m_startPoint, m_endPoint);
123
124 mouseLine = booleanWorkaroundTransform.map(mouseLine);
125 lineRect = KisAlgebra2D::createRectFromCorners(mouseLine);
126
127#ifdef KNIFE_DEBUG
128 convertShapeToDebugArray(mouseLine);
129 convertShapeToDebugArray(lineRect);
130#endif
131
132
133 QList<int> indexes;
134 QList<int> indexesOutside;
135
136 for (int i = 0; i < m_allShapes.length(); i++) {
137 KoShape* shape = m_allShapes[i];
138 QPainterPath outlineHere =
139 booleanWorkaroundTransform.map(
140 shape->absoluteTransformation().map(
141 shape->outline()));
142#ifdef KNIFE_DEBUG
143 convertShapeToDebugArray(outlineHere);
144#endif
145 if (outlineHere.boundingRect().intersects(lineRect)) {
146 srcOutlines << outlineHere;
147 indexes << i;
148 //outlineRect |= outlineHere.boundingRect();
149 } else {
150 srcOutlinesOutside << outlineHere;
151 indexesOutside << i;
152 }
153 }
154
155 if (srcOutlines.isEmpty()) {
156 return;
157 }
158
159
160 QList<int> shapeNewIndexes;
161 QList<int> shapeOrigIndexes;
162 QList<int> lineSegmentIndexes;
163
164
165 for (int i = 0; i < srcOutlines.length(); i++) {
166 QList<int> lineIndexes = KisAlgebra2D::getLineSegmentCrossingLineIndexes(mouseLine, srcOutlines[i]);
167 int shapeOrigIndex = indexes[i];
168
169 if (lineIndexes.length() > 0) {
170 Q_FOREACH(int lineIndex, lineIndexes) {
171 shapeNewIndexes << i;
172 lineSegmentIndexes << lineIndex;
173 shapeOrigIndexes << shapeOrigIndex;
174 }
175 } else {
176 srcOutlinesOutside << srcOutlines[i];
177 indexesOutside << shapeOrigIndex;
178 }
179 }
180
181 if (shapeNewIndexes.length() != 2) {
182#ifdef KNIFE_DEBUG
183 qCritical() << "Shape indexes count isn't correct: " << ppVar(shapeNewIndexes.length()) << ppVar(lineSegmentIndexes.length());
184 qCritical() << "Mouse line in used coordinates: " << mouseLine;
185 qCritical() << "Number of shapes even considered: " << srcOutlines.length();
186 Q_FOREACH(QPainterPath path, srcOutlines) {
187 qCritical() << "> A path: ";
188 convertShapeToDebugArray(path);
189 }
190 qCritical() << "That's the end of shapes considered.";
191 qCritical() << "Shapes not considered: " << srcOutlinesOutside.length();
192 Q_FOREACH(QPainterPath path, srcOutlinesOutside) {
193 qCritical() << "> A path: ";
194 convertShapeToDebugArray(path);
195 }
196 qCritical() << "That's the end of shapes not considered.";
197 for(int i = 0; i < shapeNewIndexes.length(); i++) {
198 int index = shapeNewIndexes[i];
199 qCritical() << "Shape: ";
200 convertShapeToDebugArray(srcOutlines[index]);
201 qCritical() << "Line index: " << lineSegmentIndexes[i] << " meaning it's: " << srcOutlines[index].elementAt(KisAlgebra2D::wrapValue(lineSegmentIndexes[i], 0, srcOutlines[index].elementCount()))
202 << srcOutlines[index].elementAt(KisAlgebra2D::wrapValue(lineSegmentIndexes[i] + 1, 0, srcOutlines[index].elementCount()));
203 }
204 // TODO: this if should end here; code below adds a debug line showing the mouse line
205
206 QPainterPath newLineShape = QPainterPath();
207 newLineShape.moveTo(mouseLine.p1());
208 newLineShape.lineTo(mouseLine.p2());
209
210 newLineShape = booleanWorkaroundTransform.inverted().map(newLineShape);
211 KoPathShape* newLineShapeToAdd = KoPathShape::createShapeFromPainterPath(newLineShape);
212
213 newLineShapeToAdd->setBackground(m_allShapes[0]->background());
214 newLineShapeToAdd->setStroke(m_allShapes[0]->stroke());
215 newLineShapeToAdd->setZIndex(m_allShapes[0]->zIndex() + 100);
216
217
218
219 KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Knife tool: cut through shapes"));
220 tool()->canvas()->shapeController()->addShapeDirect(newLineShapeToAdd, m_allShapes[0]->parent(), cmd);
221 tool()->canvas()->addCommand(cmd);
222
223#endif
224
225 return;
226 }
227
228
229#ifdef KNIFE_DEBUG
230 qCritical() << "Found two shapes.";
231 qCritical() << "Shape 1:";
232 convertShapeToDebugArray(srcOutlines[shapeNewIndexes[0]]);
233 qCritical() << ppVar(srcOutlines[shapeNewIndexes[0]].toFillPolygon());
234
235 qCritical() << "Shape 2:";
236 convertShapeToDebugArray(srcOutlines[shapeNewIndexes[1]]);
237 qCritical() << ppVar(srcOutlines[shapeNewIndexes[1]].toFillPolygon());
238#endif
239
240 if (shapeNewIndexes[0] == shapeNewIndexes[1]) {
241 // the same shape
242 // gotta ensure the mouseline starts and ends inside
243 bool insideP1 = KisAlgebra2D::isInsideShape(srcOutlines[shapeNewIndexes[0]], mouseLine.p1());
244 bool insideP2 = KisAlgebra2D::isInsideShape(srcOutlines[shapeNewIndexes[0]], mouseLine.p2());
245 if (!insideP1 || !insideP2) {
246 // it's from outside, it doesn't go over a gutter, then
247 return;
248 }
249 }
250
251
252
253 QPainterPath result = KisAlgebra2D::removeGutterSmart(srcOutlines[shapeNewIndexes[0]], lineSegmentIndexes[0], srcOutlines[shapeNewIndexes[1]], lineSegmentIndexes[1], shapeNewIndexes[0]==shapeNewIndexes[1]);
254
255#ifdef KNIFE_DEBUG
256 qCritical() << "Finally got a result:";
257 convertShapeToDebugArray(result);
258 qCritical() << ppVar(result.toFillPolygon());
259#endif
260
261 QList<KoShape*> resultSelectedShapes;
262
263 Q_FOREACH(int index, indexesOutside) {
264 if (isSelected[index]) {
265 resultSelectedShapes << m_allShapes[index];
266 }
267 }
268
269 // since we can't really decide which style to use, we're gonna use the style of the first found shape.
270 // if the user doesn't like it, they can change it.
271
272
273 result = booleanWorkaroundTransform.inverted().map(result);
275 resultShape->closeMerge();
276
277 if (resultShape->boundingRect().isEmpty()) {
278 return;
279 }
280
281 KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Knife tool: remove a gutter"));
282
283
284
286
287
288 KoShape* referenceShape = m_allShapes[shapeOrigIndexes[0]];
289 KoPathShape* koPathReferenceShape = dynamic_cast<KoPathShape*>(referenceShape);
290 resultShape->setBackground(referenceShape->background());
291 resultShape->setStroke(referenceShape->stroke());
292 resultShape->setZIndex(referenceShape->zIndex());
293 if (koPathReferenceShape) {
294 resultShape->setFillRule(koPathReferenceShape->fillRule());
295 }
296
297
298 KoShapeContainer *parent = referenceShape->parent();
299 tool()->canvas()->shapeController()->addShapeDirect(resultShape, parent, cmd);
300
301 if (isSelected[shapeOrigIndexes[0]] || isSelected[shapeOrigIndexes[1]]) { // if either is selected
302 resultSelectedShapes << resultShape;
303 }
304
305 QList<KoShape*> shapesToRemove;
306 shapesToRemove << m_allShapes[shapeOrigIndexes[0]];
307 if (shapeOrigIndexes[0] != shapeOrigIndexes[1]) { // there is a good reason in the workflow to allow doing this operation on the same shape
308 shapesToRemove << m_allShapes[shapeOrigIndexes[1]];
309 }
310
311
312 tool()->canvas()->shapeController()->removeShapes(shapesToRemove, cmd);
313 new KoKeepShapesSelectedCommand({}, resultSelectedShapes, tool()->canvas()->selectedShapesProxy(), true, cmd);
314 tool()->canvas()->addCommand(cmd);
315
316
317}
318
319void RemoveGutterStrategy::paint(QPainter &painter, const KoViewConverter &converter, const KoColorDisplayRendererInterface *displayRendererInterface)
320{
321 painter.save();
322 KoColor c;
323 c.fromQColor(Qt::darkGray);
324 painter.setPen(QPen(QBrush(displayRendererInterface->convertColorToDisplayColorSpace(c)), 2));
325
326 QLineF line = converter.documentToView().map(QLineF(m_startPoint, m_endPoint));
327 if (line.length() > 0) {
328 QPointF vector = line.p2() - line.p1();
329 vector = vector/line.length();
330 int arrowLength = 15;
331 int arrowThickness = 5;
332
333 QPointF before = line.p1() - vector*arrowLength;
334 QPointF after = line.p2() + vector*arrowLength;
335
336 QPointF perpendicular = QPointF(vector.y(), -vector.x());
337
338 painter.drawLine(QPointF(before + arrowThickness*perpendicular), line.p1());
339 painter.drawLine(QPointF(before - arrowThickness*perpendicular), line.p1());
340
341 painter.drawLine(QPointF(after + arrowThickness*perpendicular), line.p2());
342 painter.drawLine(QPointF(after - arrowThickness*perpendicular), line.p2());
343
344
345 }
346 painter.drawLine(line);
347
348 painter.restore();
349
350}
const Params2D p
KisImageWSP image() const
QPointer< KoShapeController > shapeController
virtual const KoViewConverter * viewConverter() const =0
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
The position of a path point within a path shape.
Definition KoPathShape.h:63
Qt::FillRule fillRule() const
Returns the fill rule for the path object.
void closeMerge()
Closes the current subpath.
void setFillRule(Qt::FillRule fillRule)
Sets the fill rule to be used for painting the background.
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:782
virtual QPainterPath outline() const
Definition KoShape.cpp:554
virtual void setStroke(KoShapeStrokeModelSP stroke)
Definition KoShape.cpp:899
QTransform absoluteTransformation() const
Definition KoShape.cpp:330
virtual void setBackground(QSharedPointer< KoShapeBackground > background)
Definition KoShape.cpp:746
KoCanvasBase * canvas() const
Returns the canvas the tool is working on.
virtual QPointF viewToDocument(const QPointF &viewPoint) const
virtual QPointF documentToView(const QPointF &documentPoint) const
QList< KoShape * > m_allShapes
void finishInteraction(Qt::KeyboardModifiers modifiers) override
void paint(QPainter &painter, const KoViewConverter &converter, const KoColorDisplayRendererInterface *displayRendererInterface) override
RemoveGutterStrategy(KoToolBase *tool, KoSelection *selection, const QList< KoShape * > &shapes, QPointF startPoint)
QList< KoShape * > m_selectedShapes
void handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) override
KUndo2Command * createCommand() override
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define ppVar(var)
Definition kis_debug.h:155
T kisGrowRect(const T &rect, U offset)
Definition kis_global.h:186
KUndo2MagicString kundo2_i18n(const char *text)
T wrapValue(T value, T wrapBounds)
void accumulateBounds(const Point &pt, Rect *bounds)
QPainterPath removeGutterSmart(const QPainterPath &shape1, int index1, const QPainterPath &shape2, int index2, bool isSameShape)
removeGutterSmart
bool isInsideShape(const VectorPath &path, const QPointF &point)
QList< int > getLineSegmentCrossingLineIndexes(const QLineF &line, const QPainterPath &shape)
PointTypeTraits< Point >::rect_type createRectFromCorners(Point corner1, Point corner2)
QTransform pathShapeBooleanSpaceWorkaround(KisImageSP image)