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