1 /*
2 Drawpile - a collaborative drawing program.
3
4 Copyright (C) 2015-2020 Calle Laakkonen
5
6 Drawpile is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Drawpile is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Drawpile. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "selection.h"
21 #include "net/commands.h"
22 #include "../tools/selection.h" // for selection utilities
23
24 #include "../libshared/net/undo.h"
25 #include "../libshared/net/image.h"
26
27 #include <QPainter>
28 #include <QtMath>
29
30 namespace canvas {
31
Selection(QObject * parent)32 Selection::Selection(QObject *parent)
33 : QObject(parent)
34 {
35
36 }
37
saveShape()38 void Selection::saveShape()
39 {
40 m_originalCenter = m_shape.boundingRect().center();
41 m_originalShape = m_shape;
42 }
43
44
isTransformed() const45 bool Selection::isTransformed() const
46 {
47 return m_shape != m_originalShape;
48 }
49
reset()50 void Selection::reset()
51 {
52 m_shape = m_originalShape;
53 emit shapeChanged(m_shape);
54 }
55
resetShape()56 void Selection::resetShape()
57 {
58 const QPointF center = m_shape.boundingRect().center();
59 m_shape.clear();
60 for(const QPointF &p : m_originalShape)
61 m_shape << p + center - m_originalCenter;
62
63 emit shapeChanged(m_shape);
64 }
65
setShape(const QPolygonF & shape)66 void Selection::setShape(const QPolygonF &shape)
67 {
68 m_shape = shape;
69 m_preAdjustmentShape = shape;
70 emit shapeChanged(shape);
71 }
72
setShapeRect(const QRect & rect)73 void Selection::setShapeRect(const QRect &rect)
74 {
75 setShape(QPolygonF({
76 rect.topLeft(),
77 QPointF(rect.left() + rect.width(), rect.top()),
78 QPointF(rect.left() + rect.width(), rect.top() + rect.height()),
79 QPointF(rect.left(), rect.top() + rect.height())
80 }));
81 saveShape();
82 }
83
84
translate(const QPoint & offset)85 void Selection::translate(const QPoint &offset)
86 {
87 if(offset.x() == 0 && offset.y() == 0)
88 return;
89
90 m_shape.translate(offset);
91 emit shapeChanged(m_shape);
92 }
93
scale(qreal x,qreal y)94 void Selection::scale(qreal x, qreal y)
95 {
96 if(x==1.0 && y==1.0)
97 return;
98
99 QPointF center = m_shape.boundingRect().center();
100 for(QPointF &p : m_shape) {
101 QPointF sp = (p - center);
102 sp.rx() *= x;
103 sp.ry() *= y;
104 p = sp + center;
105 }
106 emit shapeChanged(m_shape);
107 }
108
handleAt(const QPointF & point,qreal zoom) const109 Selection::Handle Selection::handleAt(const QPointF &point, qreal zoom) const
110 {
111 const qreal H = handleSize() / zoom;
112
113 const QRectF R = m_shape.boundingRect().adjusted(-H/2, -H/2, H/2, H/2);
114
115 if(!R.contains(point))
116 return Handle::Outside;
117
118 const QPointF p = point - R.topLeft();
119
120 if(p.x() < H) {
121 if(p.y() < H)
122 return Handle::TopLeft;
123 else if(p.y() > R.height()-H)
124 return Handle::BottomLeft;
125 return Handle::Left;
126 } else if(p.x() > R.width() - H) {
127 if(p.y() < H)
128 return Handle::TopRight;
129 else if(p.y() > R.height()-H)
130 return Handle::BottomRight;
131 return Handle::Right;
132 } else if(p.y() < H)
133 return Handle::Top;
134 else if(p.y() > R.height()-H)
135 return Handle::Bottom;
136
137 return Handle::Center;
138 }
139
setAdjustmentMode(AdjustmentMode mode)140 void Selection::setAdjustmentMode(AdjustmentMode mode)
141 {
142 if(m_adjustmentMode != mode) {
143 m_adjustmentMode = mode;
144 emit adjustmentModeChanged(mode);
145 }
146 }
147
beginAdjustment(Handle handle)148 void Selection::beginAdjustment(Handle handle)
149 {
150 m_adjustmentHandle = handle;
151 m_preAdjustmentShape = m_shape;
152 }
153
adjustGeometry(const QPointF & start,const QPointF & point,bool constrain)154 void Selection::adjustGeometry(const QPointF &start, const QPointF &point, bool constrain)
155 {
156 switch(m_adjustmentMode) {
157 case AdjustmentMode::Scale:
158 adjustGeometryScale((point - start).toPoint(), constrain);
159 break;
160 case AdjustmentMode::Rotate:
161 adjustGeometryRotate(start, point, constrain);
162 break;
163 case AdjustmentMode::Distort:
164 adjustGeometryDistort((point - start).toPoint());
165 break;
166 }
167 }
168
adjustGeometryScale(const QPoint & delta,bool keepAspect)169 void Selection::adjustGeometryScale(const QPoint &delta, bool keepAspect)
170 {
171 if(keepAspect) {
172 const int dxy = (qAbs(delta.x()) > qAbs(delta.y())) ? delta.x() : delta.y();
173
174 const QRectF bounds = m_preAdjustmentShape.boundingRect();
175 const qreal aspect = bounds.width() / bounds.height();
176 const qreal dx = dxy * aspect;
177 const qreal dy = dxy;
178
179 switch(m_adjustmentHandle) {
180 case Handle::Outside: return;
181 case Handle::Center: adjustTranslation(delta); break;
182
183 case Handle::TopLeft: adjustScale(dx, dy, 0, 0); break;
184 case Handle::TopRight: adjustScale(0, -dx, dy, 0); break;
185 case Handle::BottomRight: adjustScale(0, 0, dx, dy); break;
186 case Handle::BottomLeft: adjustScale(dx, 0, 0, -dy); break;
187
188 case Handle::Top:
189 case Handle::Left: adjustScale(dx, dy, -dx, -dy); break;
190 case Handle::Right:
191 case Handle::Bottom: adjustScale(-dx, -dy, dx, dy); break;
192 }
193 } else {
194 switch(m_adjustmentHandle) {
195 case Handle::Outside: return;
196 case Handle::Center: adjustTranslation(delta); break;
197 case Handle::TopLeft: adjustScale(delta.x(), delta.y(), 0, 0); break;
198 case Handle::TopRight: adjustScale(0, delta.y(), delta.x(), 0); break;
199 case Handle::BottomRight: adjustScale(0, 0, delta.x(), delta.y()); break;
200 case Handle::BottomLeft: adjustScale(delta.x(), 0, 0, delta.y()); break;
201 case Handle::Top: adjustScale(0, delta.y(), 0, 0); break;
202 case Handle::Right: adjustScale(0, 0, delta.x(), 0); break;
203 case Handle::Bottom: adjustScale(0, 0, 0, delta.y()); break;
204 case Handle::Left: adjustScale(delta.x(), 0, 0, 0); break;
205 }
206 }
207 }
208
adjustGeometryRotate(const QPointF & start,const QPointF & point,bool constrain)209 void Selection::adjustGeometryRotate(const QPointF &start, const QPointF &point, bool constrain)
210 {
211 switch(m_adjustmentHandle) {
212 case Handle::Outside: return;
213 case Handle::Center: adjustTranslation(point - start); break;
214 case Handle::TopLeft:
215 case Handle::TopRight:
216 case Handle::BottomRight:
217 case Handle::BottomLeft: {
218 const QPointF center = boundingRect().center();
219 const qreal a0 = atan2(start.y() - center.y(), start.x() - center.x());
220 const qreal a1 = atan2(point.y() - center.y(), point.x() - center.x());
221 qreal a = a1 - a0;
222 if(constrain) {
223 const auto STEP = M_PI / 12;
224 a = qRound(a / STEP) * STEP;
225 }
226
227 adjustRotation(a);
228 break;
229 }
230 case Handle::Top: adjustShear((start.x() - point.x()) / 100.0, 0); break;
231 case Handle::Bottom: adjustShear((point.x() - start.x()) / 100.0, 0); break;
232 case Handle::Right: adjustShear(0, (point.y() - start.y()) / 100.0); break;
233 case Handle::Left: adjustShear(0, (start.y() - point.y()) / 100.0); break;
234 }
235 }
236
adjustGeometryDistort(const QPointF & delta)237 void Selection::adjustGeometryDistort(const QPointF &delta)
238 {
239 switch(m_adjustmentHandle) {
240 case Handle::Outside: return;
241 case Handle::Center: adjustTranslation(delta); break;
242 case Handle::TopLeft: adjustDistort(delta, 0); break;
243 case Handle::Top: adjustDistort(delta, 0, 1); break;
244 case Handle::TopRight: adjustDistort(delta, 1); break;
245 case Handle::Right: adjustDistort(delta, 1, 2); break;
246 case Handle::BottomRight: adjustDistort(delta, 2); break;
247 case Handle::Bottom: adjustDistort(delta, 2, 3); break;
248 case Handle::BottomLeft: adjustDistort(delta, 3); break;
249 case Handle::Left: adjustDistort(delta, 3, 0); break;
250 }
251 }
252
adjustTranslation(const QPointF & start,const QPointF & point)253 void Selection::adjustTranslation(const QPointF &start, const QPointF &point)
254 {
255 adjustTranslation(point - start);
256 }
257
adjustTranslation(const QPointF & delta)258 void Selection::adjustTranslation(const QPointF &delta)
259 {
260 m_shape = m_preAdjustmentShape.translated(delta);
261 emit shapeChanged(m_shape);
262 }
263
adjustScale(qreal dx1,qreal dy1,qreal dx2,qreal dy2)264 void Selection::adjustScale(qreal dx1, qreal dy1, qreal dx2, qreal dy2)
265 {
266 Q_ASSERT(m_preAdjustmentShape.size() == m_shape.size());
267
268 const QRectF bounds = m_preAdjustmentShape.boundingRect();
269
270 const qreal sx = (bounds.width() - dx1 + dx2) / bounds.width();
271 const qreal sy = (bounds.height() - dy1 + dy2) / bounds.height();
272
273 for(int i=0;i<m_preAdjustmentShape.size();++i) {
274 m_shape[i] = QPointF(
275 bounds.x() + (m_preAdjustmentShape[i].x() - bounds.x()) * sx + dx1,
276 bounds.y() + (m_preAdjustmentShape[i].y() - bounds.y()) * sy + dy1
277 );
278 if(std::isnan(m_shape[i].x()) || std::isnan(m_shape[i].y())) {
279 qWarning("Selection shape[%d] is Not a Number!", i);
280 m_shape = m_preAdjustmentShape;
281 break;
282 }
283 }
284
285 emit shapeChanged(m_shape);
286 }
287
adjustRotation(qreal angle)288 void Selection::adjustRotation(qreal angle)
289 {
290 Q_ASSERT(m_preAdjustmentShape.size() == m_shape.size());
291
292 const QPointF origin = m_preAdjustmentShape.boundingRect().center();
293 QTransform t;
294 t.translate(origin.x(), origin.y());
295 t.rotateRadians(angle);
296
297 for(int i=0;i<m_shape.size();++i) {
298 const QPointF p = m_preAdjustmentShape[i] - origin;
299 m_shape[i] = t.map(p);
300 }
301
302 emit shapeChanged(m_shape);
303 }
304
adjustShear(qreal sh,qreal sv)305 void Selection::adjustShear(qreal sh, qreal sv)
306 {
307 Q_ASSERT(m_preAdjustmentShape.size() == m_shape.size());
308
309 if(qAbs(sh) < 0.0001 && qAbs(sv) < 0.0001)
310 return;
311
312 const QPointF origin = m_preAdjustmentShape.boundingRect().center();
313 QTransform t;
314 t.translate(origin.x(), origin.y());
315 t.shear(sh, sv);
316
317 for(int i=0;i<m_shape.size();++i) {
318 const QPointF p = m_preAdjustmentShape[i] - origin;
319 m_shape[i] = t.map(p);
320 }
321
322 emit shapeChanged(m_shape);
323 }
324
adjustDistort(const QPointF & delta,int corner1,int corner2)325 void Selection::adjustDistort(const QPointF &delta, int corner1, int corner2)
326 {
327 Q_ASSERT(m_preAdjustmentShape.size() == m_shape.size());
328
329 if(corner1 < 0 || corner1 > 3 || corner2 > 3) {
330 qWarning("Selection::adjustDistort: corner index out of bounds");
331 return;
332 }
333
334 // There exists a constructor to create a QPolygonF from a QRectF, but it
335 // creates a closed polygon with 5 points. QTransform::quadToQuad takes an
336 // open polygon with 4 points though, so we're initializing it like this.
337 auto bounds = m_preAdjustmentShape.boundingRect();
338 auto source = QPolygonF{QVector<QPointF>{{
339 bounds.topLeft(),
340 bounds.topRight(),
341 bounds.bottomRight(),
342 bounds.bottomLeft(),
343 }}};
344
345 auto target = QPolygonF{source};
346 target[corner1] += delta;
347 if (corner1 != corner2 && corner2 >= 0) {
348 target[corner2] += delta;
349 }
350
351 QTransform t;
352 if(!QTransform::quadToQuad(source, target, t)) {
353 qDebug("Selection::adjustDistort: no transformation possible");
354 return;
355 }
356
357 for(int i=0;i<m_shape.size();++i) {
358 // No need to adjust the origin like in the other transformation
359 // functions, the quadToQuad call deals with that already.
360 m_shape[i] = t.map(m_preAdjustmentShape[i]);
361 }
362
363 emit shapeChanged(m_shape);
364 }
365
366
addPointToShape(const QPointF & point)367 void Selection::addPointToShape(const QPointF &point)
368 {
369 if(m_closedPolygon) {
370 qWarning("Selection::addPointToShape: shape is closed!");
371
372 } else {
373 m_shape << point;
374 emit shapeChanged(m_shape);
375 }
376 }
377
closeShape()378 void Selection::closeShape()
379 {
380 if(!m_closedPolygon) {
381 m_closedPolygon = true;
382 saveShape();
383 emit closed();
384 }
385 }
386
closeShape(const QRectF & clipRect)387 bool Selection::closeShape(const QRectF &clipRect)
388 {
389 if(!m_closedPolygon) {
390 QPolygonF intersection = m_shape.intersected(clipRect);
391 if(intersection.isEmpty())
392 return false;
393 intersection.pop_back(); // We use implicitly closed polygons
394 m_shape = intersection;
395
396 m_closedPolygon = true;
397 saveShape();
398 emit closed();
399 }
400
401 return true;
402 }
403
isAxisAlignedRectangle(const QPolygon & p)404 static bool isAxisAlignedRectangle(const QPolygon &p)
405 {
406 if(p.size() != 4)
407 return false;
408
409 // When we create a rectangular polygon (see above), the points
410 // are in clockwise order, starting from the top left.
411 // 0-----1
412 // | |
413 // 3-----2
414 // This can be rotated 90, 180 or 210 degrees and still remain an
415 // axis aligned rectangle, so we need to check:
416 // 0==1 and 2==3 (X plane) and 0==3 and 1==2 (Y plane)
417 // OR
418 // 0==3 and 1==2 (X plane) and 0==1 and 2==3 (Y plane)
419 return
420 (
421 // case 1
422 p.at(0).y() == p.at(1).y() &&
423 p.at(2).y() == p.at(3).y() &&
424 p.at(0).x() == p.at(3).x() &&
425 p.at(1).x() == p.at(2).x()
426 ) || (
427 // case 2
428 p.at(0).y() == p.at(3).y() &&
429 p.at(1).y() == p.at(2).y() &&
430 p.at(0).x() == p.at(1).x() &&
431 p.at(2).x() == p.at(3).x()
432 );
433 }
434
isAxisAlignedRectangle() const435 bool Selection::isAxisAlignedRectangle() const
436 {
437 if(m_shape.size() != 4)
438 return false;
439
440 return canvas::isAxisAlignedRectangle(m_shape.toPolygon());
441 }
442
shapeMask(const QColor & color,QRect * maskBounds) const443 QImage Selection::shapeMask(const QColor &color, QRect *maskBounds) const
444 {
445 return tools::SelectionTool::shapeMask(color, m_shape, maskBounds);
446 }
447
setPasteImage(const QImage & image)448 void Selection::setPasteImage(const QImage &image)
449 {
450 m_moveRegion = QPolygonF();
451
452 const QRect selectionBounds = m_shape.boundingRect().toRect();
453 if(selectionBounds.size() != image.size() || !isAxisAlignedRectangle()) {
454 const QPoint c = selectionBounds.center();
455 setShapeRect(QRect(c.x() - image.width()/2, c.y()-image.height()/2, image.width(), image.height()));
456 }
457
458 closeShape();
459 m_pasteImage = image;
460 emit pasteImageChanged(image);
461 }
462
setMoveImage(const QImage & image,const QRect & imageRect,const QSize & canvasSize,int sourceLayerId)463 void Selection::setMoveImage(const QImage &image, const QRect &imageRect, const QSize &canvasSize, int sourceLayerId)
464 {
465 m_moveRegion = m_shape;
466 m_sourceLayerId = sourceLayerId;
467 m_pasteImage = image;
468
469 setShapeRect(imageRect);
470
471 for(QPointF &p : m_moveRegion) {
472 p.setX(qBound(0.0, p.x(), qreal(canvasSize.width())));
473 p.setY(qBound(0.0, p.y(), qreal(canvasSize.height())));
474 }
475
476 emit pasteImageChanged(image);
477 }
478
pasteOrMoveToCanvas(uint8_t contextId,int layer) const479 protocol::MessageList Selection::pasteOrMoveToCanvas(uint8_t contextId, int layer) const
480 {
481 protocol::MessageList msgs;
482
483 if(m_pasteImage.isNull()) {
484 qWarning("Selection::pasteToCanvas: nothing to paste");
485 return msgs;
486 }
487
488 if(m_shape.size()!=4) {
489 qWarning("Paste selection is not a quad!");
490 return msgs;
491 }
492
493 // Merge image
494 msgs << protocol::MessagePtr(new protocol::UndoPoint(contextId));
495
496 if(!m_moveRegion.isEmpty()) {
497 qDebug("Moving instead of pasting");
498 // Get source pixel mask
499 QRect moveBounds;
500 QByteArray mask;
501
502 if(!canvas::isAxisAlignedRectangle(m_moveRegion.toPolygon())) {
503 QImage maskimg = tools::SelectionTool::shapeMask(Qt::white, m_moveRegion, &moveBounds, true);
504 #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
505 mask = qCompress(maskimg.constBits(), maskimg.byteCount());
506 #else
507 mask = qCompress(maskimg.constBits(), maskimg.sizeInBytes());
508 #endif
509 } else {
510 moveBounds = m_moveRegion.boundingRect().toRect();
511 }
512
513 // Send move command
514 QPolygon s = m_shape.toPolygon();
515 msgs << protocol::MessagePtr(new protocol::MoveRegion(contextId,
516 uint16_t(m_sourceLayerId),
517 moveBounds.x(), moveBounds.y(), moveBounds.width(), moveBounds.height(),
518 s[0].x(), s[0].y(),
519 s[1].x(), s[1].y(),
520 s[2].x(), s[2].y(),
521 s[3].x(), s[3].y(),
522 mask
523 ));
524
525 } else {
526 QImage image;
527 QPoint offset;
528
529 image = tools::SelectionTool::transformSelectionImage(m_pasteImage, m_shape.toPolygon(), &offset);
530
531 msgs << net::command::putQImage(contextId, layer, offset.x(), offset.y(), image, paintcore::BlendMode::MODE_NORMAL);
532 }
533 return msgs;
534 }
535
transformedPasteImage() const536 QImage Selection::transformedPasteImage() const
537 {
538 return tools::SelectionTool::transformSelectionImage(m_pasteImage, m_shape.toPolygon(), nullptr);
539 }
540
fillCanvas(uint8_t contextId,const QColor & color,paintcore::BlendMode::Mode mode,int layer) const541 protocol::MessageList Selection::fillCanvas(uint8_t contextId, const QColor &color, paintcore::BlendMode::Mode mode, int layer) const
542 {
543 QRect area;
544 QImage mask;
545 QRect maskBounds;
546
547 if(isAxisAlignedRectangle())
548 area = boundingRect();
549 else
550 mask = shapeMask(color, &maskBounds);
551
552 protocol::MessageList msgs;
553
554 if(!area.isEmpty() || !mask.isNull()) {
555 if(mask.isNull())
556 msgs << protocol::MessagePtr(new protocol::FillRect(contextId, layer, int(mode), area.x(), area.y(), area.width(), area.height(), color.rgba()));
557 else
558 msgs << net::command::putQImage(contextId, layer, maskBounds.left(), maskBounds.top(), mask, mode);
559 }
560
561 if(!msgs.isEmpty())
562 msgs.prepend(protocol::MessagePtr(new protocol::UndoPoint(contextId)));
563
564 return msgs;
565 }
566
567 }
568