1 /*
2  *  Copyright (c) 2014 Dmitry Kazakov <dimula73@gmail.com>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include "kis_constrained_rect.h"
20 
21 #include <cmath>
22 #include "kis_debug.h"
23 #include "kis_algebra_2d.h"
24 
25 
KisConstrainedRect()26 KisConstrainedRect::KisConstrainedRect()
27     : m_centered(false),
28       m_canGrow(true),
29       m_ratio(1.0),
30       m_widthLocked(false),
31       m_heightLocked(false),
32       m_ratioLocked(false)
33 {
34 }
35 
~KisConstrainedRect()36 KisConstrainedRect::~KisConstrainedRect()
37 {
38 }
39 
setRectInitial(const QRect & rect)40 void KisConstrainedRect::setRectInitial(const QRect &rect)
41 {
42     m_rect = rect;
43     if (!ratioLocked()) {
44         storeRatioSafe(m_rect.size());
45     }
46     emit sigValuesChanged();
47 }
48 
setCropRect(const QRect & cropRect)49 void KisConstrainedRect::setCropRect(const QRect &cropRect)
50 {
51     m_cropRect = cropRect;
52 }
53 
centered() const54 bool KisConstrainedRect::centered() const {
55     return m_centered;
56 }
setCentered(bool value)57 void KisConstrainedRect::setCentered(bool value) {
58     m_centered = value;
59 }
60 
canGrow() const61 bool KisConstrainedRect::canGrow() const {
62     return m_canGrow;
63 }
setCanGrow(bool value)64 void KisConstrainedRect::setCanGrow(bool value) {
65     m_canGrow = value;
66 }
67 
rect() const68 QRect KisConstrainedRect::rect() const {
69     return m_rect.normalized();
70 }
71 
ratio() const72 qreal KisConstrainedRect::ratio() const {
73     return qAbs(m_ratio);
74 }
75 
moveHandle(HandleType handle,const QPoint & offset,const QRect & oldRect)76 void KisConstrainedRect::moveHandle(HandleType handle, const QPoint &offset, const QRect &oldRect)
77 {
78     const QSize oldSize = oldRect.size();
79     QSize newSize = oldSize;
80     QPoint newOffset = oldRect.topLeft();
81 
82     int xSizeCoeff = 1;
83     int ySizeCoeff = 1;
84 
85     qreal xOffsetFromSizeChange = 1.0;
86     qreal yOffsetFromSizeChange = 1.0;
87 
88     int baseSizeCoeff = 1;
89 
90     bool useMoveOnly = false;
91 
92     switch (handle) {
93     case UpperLeft:
94         xSizeCoeff = -1;
95         ySizeCoeff = -1;
96         xOffsetFromSizeChange = -1.0;
97         yOffsetFromSizeChange = -1.0;
98         break;
99     case UpperRight:
100         xSizeCoeff =  1;
101         ySizeCoeff = -1;
102         xOffsetFromSizeChange =  0.0;
103         yOffsetFromSizeChange = -1.0;
104         break;
105     case Creation:
106         baseSizeCoeff = 0;
107         Q_FALLTHROUGH();
108     case LowerRight:
109         xSizeCoeff =  1;
110         ySizeCoeff =  1;
111         xOffsetFromSizeChange =  0.0;
112         yOffsetFromSizeChange =  0.0;
113         break;
114     case LowerLeft:
115         xSizeCoeff = -1;
116         ySizeCoeff =  1;
117         xOffsetFromSizeChange = -1.0;
118         yOffsetFromSizeChange =  0.0;
119         break;
120     case Upper:
121         xSizeCoeff =  0;
122         ySizeCoeff = -1;
123         xOffsetFromSizeChange = -0.5;
124         yOffsetFromSizeChange = -1.0;
125         break;
126     case Right:
127         xSizeCoeff =  1;
128         ySizeCoeff =  0;
129         xOffsetFromSizeChange =  0.0;
130         yOffsetFromSizeChange = -0.5;
131         break;
132     case Lower:
133         xSizeCoeff =  0;
134         ySizeCoeff =  1;
135         xOffsetFromSizeChange = -0.5;
136         yOffsetFromSizeChange =  0.0;
137         break;
138     case Left:
139         xSizeCoeff = -1;
140         ySizeCoeff =  0;
141         xOffsetFromSizeChange = -1.0;
142         yOffsetFromSizeChange = -0.5;
143         break;
144     case Inside:
145         useMoveOnly = true;
146         break;
147     case None: // should never happen
148         break;
149     }
150 
151     if (!useMoveOnly) {
152         const int centeringSizeCoeff = m_centered ? 2 : 1;
153         if (m_centered) {
154             xOffsetFromSizeChange = -0.5;
155             yOffsetFromSizeChange = -0.5;
156         }
157 
158 
159         QSize sizeDiff(offset.x() * xSizeCoeff * centeringSizeCoeff,
160                        offset.y() * ySizeCoeff * centeringSizeCoeff);
161 
162         QSize tempSize = baseSizeCoeff * oldSize + sizeDiff;
163         bool widthPreferrable = qAbs(tempSize.width()) > qAbs(tempSize.height() * m_ratio);
164 
165         if (ratioLocked() && ((widthPreferrable && xSizeCoeff != 0) || ySizeCoeff == 0)) {
166             newSize.setWidth(tempSize.width());
167             newSize.setHeight(heightFromWidthUnsignedRatio(newSize.width(), m_ratio, tempSize.height()));
168         } else if (ratioLocked() && ((!widthPreferrable && ySizeCoeff != 0) || xSizeCoeff == 0)) {
169             newSize.setHeight(tempSize.height());
170             newSize.setWidth(widthFromHeightUnsignedRatio(newSize.height(), m_ratio, tempSize.width()));
171         } else if (widthLocked() && heightLocked()) {
172             newSize.setWidth(KisAlgebra2D::copysign(newSize.width(), tempSize.width()));
173             newSize.setHeight(KisAlgebra2D::copysign(newSize.height(), tempSize.height()));
174         } else if (widthLocked()) {
175             newSize.setWidth(KisAlgebra2D::copysign(newSize.width(), tempSize.width()));
176             newSize.setHeight(tempSize.height());
177             storeRatioSafe(newSize);
178         } else if (heightLocked()) {
179             newSize.setHeight(KisAlgebra2D::copysign(newSize.height(), tempSize.height()));
180             newSize.setWidth(tempSize.width());
181             storeRatioSafe(newSize);
182         } else {
183             newSize = baseSizeCoeff * oldSize + sizeDiff;
184             storeRatioSafe(newSize);
185         }
186 
187         QSize realSizeDiff = newSize - baseSizeCoeff * oldSize;
188         QPoint offsetDiff(realSizeDiff.width() * xOffsetFromSizeChange,
189                           realSizeDiff.height() * yOffsetFromSizeChange);
190 
191         newOffset = oldRect.topLeft() + offsetDiff;
192     } else {
193         newOffset = oldRect.topLeft() + offset;
194     }
195 
196     QPoint prevOffset = newOffset;
197 
198     if (!m_canGrow) {
199         if (newOffset.x() + newSize.width() > m_cropRect.width()) {
200             newOffset.setX(m_cropRect.width() - newSize.width());
201         }
202 
203         if (newOffset.y() + newSize.height() > m_cropRect.height()) {
204             newOffset.setY(m_cropRect.height() - newSize.height());
205         }
206         if (newOffset.x() < m_cropRect.x()) {
207             newOffset.setX(m_cropRect.x());
208         }
209         if (newOffset.y() < m_cropRect.y()) {
210             newOffset.setY(m_cropRect.y());
211         }
212     }
213 
214     if (!m_ratioLocked && !useMoveOnly) {
215         newOffset = prevOffset;
216     }
217 
218     m_rect = QRect(newOffset, newSize);
219 
220     if (!m_canGrow) {
221         m_rect &= m_cropRect;
222     }
223 
224     emit sigValuesChanged();
225 }
226 
handleSnapPoint(HandleType handle,const QPointF & cursorPos)227 QPointF KisConstrainedRect::handleSnapPoint(HandleType handle, const QPointF &cursorPos)
228 {
229     QPointF snapPoint = cursorPos;
230 
231     switch (handle) {
232     case UpperLeft:
233         snapPoint = m_rect.topLeft();
234         break;
235     case UpperRight:
236         snapPoint = m_rect.topRight() + QPointF(1, 0);
237         break;
238     case Creation:
239         break;
240     case LowerRight:
241         snapPoint = m_rect.bottomRight() + QPointF(1, 1);
242         break;
243     case LowerLeft:
244         snapPoint = m_rect.bottomLeft() + QPointF(0, 1);
245         break;
246     case Upper:
247         snapPoint.ry() = m_rect.y();
248         break;
249     case Right:
250         snapPoint.rx() = m_rect.right() + 1;
251         break;
252     case Lower:
253         snapPoint.ry() = m_rect.bottom() + 1;
254         break;
255     case Left:
256         snapPoint.rx() = m_rect.x();
257         break;
258     case Inside:
259         break;
260     case None: // should never happen
261         break;
262     }
263 
264     return snapPoint;
265 }
266 
normalize()267 void KisConstrainedRect::normalize()
268 {
269     setRectInitial(m_rect.normalized());
270 }
271 
setOffset(const QPoint & offset)272 void KisConstrainedRect::setOffset(const QPoint &offset)
273 {
274     QRect newRect = m_rect;
275     newRect.moveTo(offset);
276 
277     if (!m_canGrow) {
278         newRect &= m_cropRect;
279     }
280 
281     if (!newRect.isEmpty()) {
282         m_rect = newRect;
283     }
284 
285     emit sigValuesChanged();
286 }
287 
setRatio(qreal value)288 void KisConstrainedRect::setRatio(qreal value) {
289     KIS_ASSERT_RECOVER_RETURN(value >= 0);
290 
291     const qreal eps = 1e-7;
292     const qreal invEps = 1.0 / eps;
293 
294     if (value < eps || value > invEps) {
295         emit sigValuesChanged();
296         return;
297     }
298 
299     const QSize oldSize = m_rect.size();
300     QSize newSize = oldSize;
301 
302     if (widthLocked() && heightLocked()) {
303         setHeightLocked(false);
304     }
305 
306     m_ratio = value;
307 
308     if (!widthLocked() && !heightLocked()) {
309         int area = oldSize.width() * oldSize.height();
310         newSize.setWidth(qRound(std::sqrt(area * m_ratio)));
311         newSize.setHeight(qRound(newSize.width() / m_ratio));
312     } else if (widthLocked()) {
313         newSize.setHeight(newSize.width() / m_ratio);
314     } else if (heightLocked()) {
315         newSize.setWidth(newSize.height() * m_ratio);
316     }
317 
318     assignNewSize(newSize);
319 }
320 
setWidth(int value)321 void KisConstrainedRect::setWidth(int value)
322 {
323     KIS_ASSERT_RECOVER_RETURN(value >= 0);
324 
325     const QSize oldSize = m_rect.size();
326     QSize newSize = oldSize;
327 
328     if (ratioLocked()) {
329         newSize.setWidth(value);
330         newSize.setHeight(newSize.width() / m_ratio);
331     } else {
332         newSize.setWidth(value);
333         storeRatioSafe(newSize);
334     }
335 
336     assignNewSize(newSize);
337 }
338 
setHeight(int value)339 void KisConstrainedRect::setHeight(int value)
340 {
341     KIS_ASSERT_RECOVER_RETURN(value >= 0);
342 
343     const QSize oldSize = m_rect.size();
344     QSize newSize = oldSize;
345 
346     if (ratioLocked()) {
347         newSize.setHeight(value);
348         newSize.setWidth(newSize.height() * m_ratio);
349     } else {
350         newSize.setHeight(value);
351         storeRatioSafe(newSize);
352     }
353 
354     assignNewSize(newSize);
355 }
356 
assignNewSize(const QSize & newSize)357 void KisConstrainedRect::assignNewSize(const QSize &newSize)
358 {
359     if (!m_centered) {
360         m_rect.setSize(newSize);
361     } else {
362         QSize sizeDiff = newSize - m_rect.size();
363         m_rect.translate(-qRound(sizeDiff.width() / 2.0), -qRound(sizeDiff.height() / 2.0));
364         m_rect.setSize(newSize);
365     }
366 
367     if (!m_canGrow) {
368         m_rect &= m_cropRect;
369     }
370 
371     emit sigValuesChanged();
372 }
373 
storeRatioSafe(const QSize & newSize)374 void KisConstrainedRect::storeRatioSafe(const QSize &newSize)
375 {
376     m_ratio = qAbs(qreal(newSize.width()) / newSize.height());
377 }
378 
widthFromHeightUnsignedRatio(int height,qreal ratio,int oldWidth) const379 int KisConstrainedRect::widthFromHeightUnsignedRatio(int height, qreal ratio, int oldWidth) const
380 {
381     int newWidth = qRound(height * ratio);
382     return KisAlgebra2D::copysign(newWidth, oldWidth);
383 }
384 
heightFromWidthUnsignedRatio(int width,qreal ratio,int oldHeight) const385 int KisConstrainedRect::heightFromWidthUnsignedRatio(int width, qreal ratio, int oldHeight) const
386 {
387     int newHeight = qRound(width / ratio);
388     return KisAlgebra2D::copysign(newHeight, oldHeight);
389 }
390 
widthLocked() const391 bool KisConstrainedRect::widthLocked() const {
392     return m_widthLocked;
393 }
heightLocked() const394 bool KisConstrainedRect::heightLocked() const {
395     return m_heightLocked;
396 }
ratioLocked() const397 bool KisConstrainedRect::ratioLocked() const {
398     return m_ratioLocked;
399 }
400 
setWidthLocked(bool value)401 void KisConstrainedRect::setWidthLocked(bool value) {
402     m_widthLocked = value;
403     m_ratioLocked &= !(m_widthLocked || m_heightLocked);
404 
405     emit sigLockValuesChanged();
406 }
407 
setHeightLocked(bool value)408 void KisConstrainedRect::setHeightLocked(bool value) {
409     m_heightLocked = value;
410     m_ratioLocked &= !(m_widthLocked || m_heightLocked);
411 
412     emit sigLockValuesChanged();
413 }
414 
setRatioLocked(bool value)415 void KisConstrainedRect::setRatioLocked(bool value) {
416     m_ratioLocked = value;
417 
418     m_widthLocked &= !m_ratioLocked;
419     m_heightLocked &= !m_ratioLocked;
420 
421     emit sigLockValuesChanged();
422 }
423 
424