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