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