]> Shamusworld >> Repos - architektonas/blob - src/arc.cpp
Fix object selection to work while in snap mode.
[architektonas] / src / arc.cpp
1 // arc.cpp: Arc object
2 //
3 // Part of the Architektonas Project
4 // (C) 2011 Underground Software
5 // See the README and GPLv3 files for licensing and warranty information
6 //
7 // JLH = James Hammons <jlhamm@acm.org>
8 //
9 // WHO  WHEN        WHAT
10 // ---  ----------  ------------------------------------------------------------
11 // JLH  03/30/2011  Created this file
12 // JLH  04/03/2011  Added information panel (angles) rendering
13 // JLH  08/16/2013  Added continuous user feedack like for Line and Circle
14 //
15
16 #include "arc.h"
17
18 #include <QtGui>
19 #include "mathconstants.h"
20 #include "painter.h"
21
22
23 Arc::Arc(Vector p1, double r, double a1, double a2, Object * p/*= NULL*/):
24         Object(p1, p), /*type(OTArc),*/ radius(r), startAngle(a1), angleSpan(a2),
25         draggingCenter(false), draggingEdge(false), draggingRotate(false),
26         draggingSpan(false),
27         hitCenter(false), hitArc(false), hitRotate(false), hitSpan(false)
28 {
29         // This is in the base class, why can't we use the contructor to fill it???
30         type = OTArc;
31         state = OSInactive;
32 }
33
34
35 Arc::~Arc()
36 {
37 }
38
39
40 /*virtual*/ void Arc::Draw(Painter * painter)
41 {
42         QPen pen;
43         painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
44
45         Point p1(cos(startAngle), sin(startAngle));
46         Point p2(cos(startAngle + angleSpan), sin(startAngle + angleSpan));
47         Vector handle2 = (p1 * radius) + position;
48         Vector handle3 = (p2 * radius) + position;
49
50         if ((state == OSSelected) || ((state == OSInactive) && hitRotate))
51                 painter->DrawHandle(handle2);
52
53         if ((state == OSSelected) || ((state == OSInactive) && hitSpan))
54                 painter->DrawHandle(handle3);
55
56         if ((state == OSSelected) || ((state == OSInactive) && hitCenter))
57                 painter->DrawHandle(position);
58
59         if ((state == OSInactive) && !hitArc)
60                 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
61
62         painter->DrawArc(position, radius, startAngle, angleSpan);
63
64         if (draggingRotate || draggingSpan)
65         {
66                 if (draggingRotate)
67                 {
68                         // If we rotating, we draw a guideline showing the angle we're
69                         // moving it from.
70                         Point p3(cos(oldAngle), sin(oldAngle));
71                         Vector oldLine = (p3 * (radius * 1.25)) + position;
72                         pen = QPen(QColor(0x80, 0x80, 0x80), 1.0, Qt::DashLine);
73                         painter->SetPen(pen);
74 //                      painter->DrawLine((int)position.x, (int)position.y, (int)oldLine.x, (int)oldLine.y);
75                         painter->DrawLine(position, oldLine);
76                 }
77
78                 // In rotating and setting the span, we draw a line showing where
79                 // we angle/span is that we're setting.
80                 pen = QPen(QColor(0x00, 0xC0, 0x80), 1.0, Qt::DashLine);
81                 painter->SetPen(pen);
82                 painter->DrawLine((int)position.x, (int)position.y, (int)oldPoint.x, (int)oldPoint.y);
83         }
84
85         // If we're rotating or setting the span, draw an information panel
86         // showing both absolute and relative angles being set.
87         if (draggingRotate || draggingSpan || draggingEdge)
88         {
89                 double absAngle = (Vector(oldPoint - position).Angle()) * RADIANS_TO_DEGREES;
90                 double relAngle = (startAngle >= oldAngle ? startAngle - oldAngle :
91                         startAngle - oldAngle + (2.0 * PI)) * RADIANS_TO_DEGREES;
92
93                 QString text;
94
95                 if (draggingRotate)
96                 {
97                         text = QObject::tr("Abs ") + QChar(0x2221) + ": %1" + QChar(0x00B0)
98                                 + QObject::tr("\nRel ") + QChar(0x2221) + ": %2" + QChar(0x00B0);
99                         text = text.arg(absAngle, 0, 'd', 4).arg(relAngle, 0, 'd', 4);
100                 }
101                 else if (draggingSpan)
102                 {
103                         text = QObject::tr("Abs ") + QChar(0x2221) + ": %1" + QChar(0x00B0)
104                                 + QObject::tr("\nSpan: %2") + QChar(0x00B0);
105                         text = text.arg(absAngle, 0, 'd', 4).arg(angleSpan * RADIANS_TO_DEGREES, 0, 'd', 4);
106                 }
107                 else if (draggingEdge)
108                 {
109                         text = QObject::tr("Radius: %1\nScale: %2%");
110                         text = text.arg(radius, 0, 'd', 4).arg(radius / oldRadius * 100.0, 0, 'd', 0);
111                 }
112
113                 pen = QPen(QColor(0x00, 0xFF, 0x00), 1.0, Qt::SolidLine);
114                 painter->SetPen(pen);
115                 painter->SetBrush(QBrush(QColor(0x40, 0xFF, 0x40, 0x9F)));
116                 QRectF textRect(10.0, 10.0, 270.0, 70.0);       // x, y, w, h
117                 painter->DrawRoundedRect(textRect, 7.0, 7.0);
118
119                 textRect.setLeft(textRect.left() + 14);
120                 painter->SetFont(*Object::font);
121                 pen = QPen(QColor(0x00, 0x5F, 0xDF));
122                 painter->SetPen(pen);
123                 painter->DrawText(textRect, Qt::AlignVCenter, text);
124         }
125 }
126
127
128 /*virtual*/ Vector Arc::Center(void)
129 {
130         return position;
131 }
132
133
134 /*
135  We need at least *four* handles for this object:
136  - one for moving
137  - one for resizing
138  - one for rotation
139  - one for setting the span of the arc
140
141 We need to think about the intuitive way (if there is any) to grab and
142 manipulate a complex object like this... Need to think, "What should happen when
143 I click here and drag there?"
144
145 Also: should put the snap logic into the Object base class (as a static method)...
146 */
147
148 /*virtual*/ bool Arc::Collided(Vector point)
149 {
150         objectWasDragged = false;
151         bool hitSomething = HitTest(point);
152         draggingCenter = hitCenter;
153         draggingEdge   = hitArc;
154         draggingRotate = hitRotate;
155         draggingSpan   = hitSpan;
156
157         // Now that we've done our hit testing on the non-snapped point, snap it if
158         // necessary...
159         if (snapToGrid)
160                 point = SnapPointToGrid(point);
161
162 /*
163 State Management:
164 We want the arc to go into OSSelected mode if we click on it but don't drag.
165 If we don't click anywhere on the arc, then we want it to go into OSInactive mode.
166 Otherwise, we hit a handle and we want to modify the object whether it's in
167 OSSelected mode or not.
168
169 If I, go to S. Can drag, but only handles 2, 3, & 4 (since the center is not highlighted
170 until we select it.
171         However, if dragging, revert to I once finished.
172
173 If S, stay in S. Can drag all.
174
175 So, we know this. If wasDragged is true, then there's a chance we need to revert to I.
176 How can we be sure?
177
178 So. We have a matrix that looks like this:
179
180         |Was I|Was S
181 --------+-----+-----
182 Inactive|     |   
183 --------+-----+-----
184 Selected|     |   
185
186         |h1|h2|h3|h4
187 --------+--+--+--+--
188 Inactive|  |  |  |
189 --------+--+--+--+--
190 Selected|  |  |  |
191
192 so let's do like this:
193 */
194         if (hitSomething)
195         {
196                 oldState = state;
197                 state = OSSelected;
198 //              oldPoint = point;
199                 oldPoint = position;
200                 oldAngle = startAngle;
201                 oldRadius = radius;
202                 return true;
203         }
204
205         state = OSInactive;
206         return false;
207 }
208
209
210 /*virtual*/ void Arc::PointerMoved(Vector point)
211 {
212 // one other thing to check here for is if a modifier key is being held as well,
213 // to allow for multi-selection
214         if (selectionInProgress)
215         {
216                 // Check for whether or not the rect contains this circle
217 //              if (selection.normalized().contains(Extents()))
218                 if (selection.contains(Extents()))
219                         state = OSSelected;
220                 else
221                         state = OSInactive;
222
223                 return;
224         }
225
226         // The TLC will send these messages if the object is selected but not clicked on.
227         // So we have to be careful with our assumptions here.
228         // This is actually untrue in that case, we need to come up with something better
229         // here...
230 //      objectWasDragged = true;
231 //      needUpdate = false;
232         SaveHitState();
233         HitTest(point);
234         needUpdate = HitStateChanged();
235         objectWasDragged = (draggingCenter | draggingEdge | draggingRotate | draggingSpan);
236
237         if (objectWasDragged)
238                 needUpdate = true;
239 //      if (!(hitHandle1 || hitHandle2 || hitHandle3 || hitHandle4))
240 //              return;
241
242 //      Vector delta = point - oldPoint;
243
244         if (draggingCenter)
245                 position = point;
246         else if (draggingEdge)
247                 radius = Vector::Magnitude(point, position);
248         else if (draggingRotate)
249         {
250                 startAngle = Vector(point - position).Angle();
251         }
252         else if (draggingSpan)
253         {
254                 double angle = Vector(point - position).Angle();
255
256                 if (angle < startAngle)
257                         angle += 2.0 * PI;
258
259                 angleSpan = angle - startAngle;
260         }
261
262         // Why save this? For rendering code?
263         oldPoint = point;
264 //      needUpdate = true;
265 }
266
267
268 /*virtual*/ void Arc::PointerReleased(void)
269 {
270         // Mouse went up, so our dragging is done (if any *was* done, that is)
271 //      hitHandle1 = hitHandle2 = hitHandle3 = hitHandle4 = false;
272         draggingCenter = draggingEdge = draggingRotate = draggingSpan = false;
273         hitCenter = hitArc = hitRotate = hitSpan = false;
274
275         // If the object was dragged, then revert to the old state.
276         // Otherwise, we were probably just clicked, and want to stay in the selected state.
277         if (objectWasDragged)
278                 state = oldState;
279 }
280
281
282 /*virtual*/ bool Arc::HitTest(Point point)
283 {
284         hitCenter = hitArc = hitRotate = hitSpan = false;
285
286 /*
287 What we have:
288 the center of the arc
289 the starting angle
290 the span of the arc
291 The point on a unit circle given an angle a is x = cos(a), y = sin(a)
292 This vector is already unitized, so all we need to do to get our point is to
293 multiply it by radius (to get the length correct) and add it to the center
294 point (to get the correct position).
295 */
296         Vector v1(point, position);     // Head minus tail (vector points at "point")
297         Point p1(cos(startAngle), sin(startAngle));
298         Point p2(cos(startAngle + angleSpan), sin(startAngle + angleSpan));
299         Vector handle2 = (p1 * radius) + position;
300         Vector handle3 = (p2 * radius) + position;
301         double pointerAngle = v1.Angle();
302         double length = v1.Magnitude();
303
304 #if 0
305         if (v1.Magnitude() < 10.0)
306                 hitCenter = true;
307         else if (Vector(handle3 - point).Magnitude() < 10.0)
308                 hitSpan = true;
309         else if (Vector(handle2 - point).Magnitude() < 10.0)
310                 hitRotate = true;
311         else if ((v1.Magnitude() < radius + 3.0) && (v1.Magnitude() > radius - 3.0)
312                 && AngleInArcSpan(pointerAngle))
313                 hitArc = true;
314 #else
315         if ((length * Painter::zoom) < 8.0)
316                 hitCenter = true;
317         else if (((fabs(length - radius) * Painter::zoom) < 2.0)
318                 && AngleInArcSpan(pointerAngle))
319                 hitArc = true;
320         else if ((Vector::Magnitude(handle2, point) * Painter::zoom) < 8.0)
321                 hitRotate = true;
322         else if ((Vector::Magnitude(handle3, point) * Painter::zoom) < 8.0)
323                 hitSpan = true;
324 #endif
325
326         return (hitCenter || hitArc || hitRotate || hitSpan ? true : false);
327 }
328
329
330 /*virtual*/ QRectF Arc::Extents(void)
331 {
332         double start = startAngle;
333         double end = start + angleSpan;
334         QPointF p1(cos(start), sin(start));
335         QPointF p2(cos(end), sin(end));
336         QRectF bounds(p1, p2);
337
338         // Swap X/Y coordinates if they're backwards...
339         if (bounds.left() > bounds.right())
340         {
341                 double temp = bounds.left();
342                 bounds.setLeft(bounds.right());
343                 bounds.setRight(temp);
344         }
345
346         if (bounds.bottom() > bounds.top())
347         {
348                 double temp = bounds.bottom();
349                 bounds.setBottom(bounds.top());
350                 bounds.setTop(temp);
351         }
352
353         // If the end of the arc is before the beginning, add 360 degrees to it
354         if (end < start)
355                 end += 2.0 * PI;
356
357         // Adjust the bounds depending on which axes are crossed
358         if ((start < PI_OVER_2) && (end > PI_OVER_2))
359                 bounds.setTop(1.0);
360
361         if ((start < PI) && (end > PI))
362                 bounds.setLeft(-1.0);
363
364         if ((start < (PI + PI_OVER_2)) && (end > (PI + PI_OVER_2)))
365                 bounds.setBottom(-1.0);
366
367         if ((start < (2.0 * PI)) && (end > (2.0 * PI)))
368                 bounds.setRight(1.0);
369
370         if ((start < ((2.0 * PI) + PI_OVER_2)) && (end > ((2.0 * PI) + PI_OVER_2)))
371                 bounds.setTop(1.0);
372
373         if ((start < (3.0 * PI)) && (end > (3.0 * PI)))
374                 bounds.setLeft(-1.0);
375
376         if ((start < ((3.0 * PI) + PI_OVER_2)) && (end > ((3.0 * PI) + PI_OVER_2)))
377                 bounds.setBottom(-1.0);
378
379         bounds.setTopLeft(QPointF(bounds.left() * radius, bounds.top() * radius));
380         bounds.setBottomRight(QPointF(bounds.right() * radius, bounds.bottom() * radius));
381         bounds.translate(position.x, position.y);
382         return bounds;
383 }
384
385
386 /*
387 start = 350, span = 20, end = 10, angle = 5
388 angle < start, so angle = 365
389 */
390 bool Arc::AngleInArcSpan(double angle)
391 {
392         // This should be simple except for a small complication: The start angle plus
393         // the angle span can end up being less than the start angle! So, to check
394         // for this possibility, we check to see if the angle passed in is less than
395         // the start angle and if so, add 2PI to the angle passed in. Then we take the
396         // difference between our start angle and this adjusted angle and see if it
397         // falls within our span.
398         if (angle < startAngle)
399                 angle += 2.0 * PI;
400
401         double passedInSpan = angle - startAngle;
402
403         return (passedInSpan <= angleSpan ?  true : false);
404 }
405
406
407 void Arc::SaveHitState(void)
408 {
409         oldHitCenter = hitCenter;
410         oldHitArc    = hitArc;
411         oldHitRotate = hitRotate;
412         oldHitSpan   = hitSpan;
413 }
414
415
416 bool Arc::HitStateChanged(void)
417 {
418         if ((hitCenter != oldHitCenter)
419                 || (hitArc != oldHitArc)
420                 || (hitRotate != oldHitRotate)
421                 || (hitSpan != oldHitSpan))
422                 return true;
423
424         return false;
425 }
426
427
428 /*virtual*/ void Arc::Enumerate(FILE * file)
429 {
430         fprintf(file, "ARC (%lf,%lf) %lf, %lf, %lf\n", position.x, position.y, radius, startAngle, angleSpan);
431 }
432
433
434 /*virtual*/ Object * Arc::Copy(void)
435 {
436 #warning "!!! This doesn't take care of attached Dimensions !!!"
437 /*
438 This is a real problem. While having a pointer in the Dimension to this line's points
439 is fast & easy, it creates a huge problem when trying to replicate an object like this.
440
441 Maybe a way to fix that then, is to have reference numbers instead of pointers. That
442 way, if you copy them, ... you might still have problems. Because you can't be sure if
443 a copy will be persistant or not, you then *definitely* do not want them to have the
444 same reference number.
445 */
446         return new Arc(position, radius, startAngle, angleSpan, parent);
447 }
448