3 // Part of the Architektonas Project
4 // (C) 2011 Underground Software
5 // See the README and GPLv3 files for licensing and warranty information
7 // JLH = James Hammons <jlhamm@acm.org>
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
19 #include "mathconstants.h"
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),
27 hitCenter(false), hitArc(false), hitRotate(false), hitSpan(false)
29 // This is in the base class, why can't we use the contructor to fill it???
40 /*virtual*/ void Arc::Draw(Painter * painter)
43 painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
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;
50 if ((state == OSSelected) || ((state == OSInactive) && hitRotate))
51 painter->DrawHandle(handle2);
53 if ((state == OSSelected) || ((state == OSInactive) && hitSpan))
54 painter->DrawHandle(handle3);
56 if ((state == OSSelected) || ((state == OSInactive) && hitCenter))
57 painter->DrawHandle(position);
59 if ((state == OSInactive) && !hitArc)
60 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
62 painter->DrawArc(position, radius, startAngle, angleSpan);
64 if (draggingRotate || draggingSpan)
68 // If we rotating, we draw a guideline showing the angle we're
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);
74 // painter->DrawLine((int)position.x, (int)position.y, (int)oldLine.x, (int)oldLine.y);
75 painter->DrawLine(position, oldLine);
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);
82 painter->DrawLine((int)position.x, (int)position.y, (int)oldPoint.x, (int)oldPoint.y);
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)
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;
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);
101 else if (draggingSpan)
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);
107 else if (draggingEdge)
109 text = QObject::tr("Radius: %1\nScale: %2%");
110 text = text.arg(radius, 0, 'd', 4).arg(radius / oldRadius * 100.0, 0, 'd', 0);
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);
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);
128 /*virtual*/ Vector Arc::Center(void)
135 We need at least *four* handles for this object:
139 - one for setting the span of the arc
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?"
145 Also: should put the snap logic into the Object base class (as a static method)...
148 /*virtual*/ bool Arc::Collided(Vector point)
150 objectWasDragged = false;
151 bool hitSomething = HitTest(point);
152 draggingCenter = hitCenter;
153 draggingEdge = hitArc;
154 draggingRotate = hitRotate;
155 draggingSpan = hitSpan;
157 // Now that we've done our hit testing on the non-snapped point, snap it if
160 point = SnapPointToGrid(point);
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.
169 If I, go to S. Can drag, but only handles 2, 3, & 4 (since the center is not highlighted
171 However, if dragging, revert to I once finished.
173 If S, stay in S. Can drag all.
175 So, we know this. If wasDragged is true, then there's a chance we need to revert to I.
178 So. We have a matrix that looks like this:
192 so let's do like this:
200 oldAngle = startAngle;
210 /*virtual*/ void Arc::PointerMoved(Vector point)
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)
216 // Check for whether or not the rect contains this circle
217 // if (selection.normalized().contains(Extents()))
218 if (selection.contains(Extents()))
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
230 // objectWasDragged = true;
231 // needUpdate = false;
234 needUpdate = HitStateChanged();
235 objectWasDragged = (draggingCenter | draggingEdge | draggingRotate | draggingSpan);
237 if (objectWasDragged)
239 // if (!(hitHandle1 || hitHandle2 || hitHandle3 || hitHandle4))
242 // Vector delta = point - oldPoint;
246 else if (draggingEdge)
247 radius = Vector::Magnitude(point, position);
248 else if (draggingRotate)
250 startAngle = Vector(point - position).Angle();
252 else if (draggingSpan)
254 double angle = Vector(point - position).Angle();
256 if (angle < startAngle)
259 angleSpan = angle - startAngle;
262 // Why save this? For rendering code?
264 // needUpdate = true;
268 /*virtual*/ void Arc::PointerReleased(void)
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;
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)
282 /*virtual*/ bool Arc::HitTest(Point point)
284 hitCenter = hitArc = hitRotate = hitSpan = false;
288 the center 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).
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();
305 if (v1.Magnitude() < 10.0)
307 else if (Vector(handle3 - point).Magnitude() < 10.0)
309 else if (Vector(handle2 - point).Magnitude() < 10.0)
311 else if ((v1.Magnitude() < radius + 3.0) && (v1.Magnitude() > radius - 3.0)
312 && AngleInArcSpan(pointerAngle))
315 if ((length * Painter::zoom) < 8.0)
317 else if (((fabs(length - radius) * Painter::zoom) < 2.0)
318 && AngleInArcSpan(pointerAngle))
320 else if ((Vector::Magnitude(handle2, point) * Painter::zoom) < 8.0)
322 else if ((Vector::Magnitude(handle3, point) * Painter::zoom) < 8.0)
326 return (hitCenter || hitArc || hitRotate || hitSpan ? true : false);
330 /*virtual*/ QRectF Arc::Extents(void)
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);
338 // Swap X/Y coordinates if they're backwards...
339 if (bounds.left() > bounds.right())
341 double temp = bounds.left();
342 bounds.setLeft(bounds.right());
343 bounds.setRight(temp);
346 if (bounds.bottom() > bounds.top())
348 double temp = bounds.bottom();
349 bounds.setBottom(bounds.top());
353 // If the end of the arc is before the beginning, add 360 degrees to it
357 // Adjust the bounds depending on which axes are crossed
358 if ((start < PI_OVER_2) && (end > PI_OVER_2))
361 if ((start < PI) && (end > PI))
362 bounds.setLeft(-1.0);
364 if ((start < (PI + PI_OVER_2)) && (end > (PI + PI_OVER_2)))
365 bounds.setBottom(-1.0);
367 if ((start < (2.0 * PI)) && (end > (2.0 * PI)))
368 bounds.setRight(1.0);
370 if ((start < ((2.0 * PI) + PI_OVER_2)) && (end > ((2.0 * PI) + PI_OVER_2)))
373 if ((start < (3.0 * PI)) && (end > (3.0 * PI)))
374 bounds.setLeft(-1.0);
376 if ((start < ((3.0 * PI) + PI_OVER_2)) && (end > ((3.0 * PI) + PI_OVER_2)))
377 bounds.setBottom(-1.0);
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);
387 start = 350, span = 20, end = 10, angle = 5
388 angle < start, so angle = 365
390 bool Arc::AngleInArcSpan(double angle)
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)
401 double passedInSpan = angle - startAngle;
403 return (passedInSpan <= angleSpan ? true : false);
407 void Arc::SaveHitState(void)
409 oldHitCenter = hitCenter;
411 oldHitRotate = hitRotate;
412 oldHitSpan = hitSpan;
416 bool Arc::HitStateChanged(void)
418 if ((hitCenter != oldHitCenter)
419 || (hitArc != oldHitArc)
420 || (hitRotate != oldHitRotate)
421 || (hitSpan != oldHitSpan))
428 /*virtual*/ void Arc::Enumerate(FILE * file)
430 fprintf(file, "ARC (%lf,%lf) %lf, %lf, %lf\n", position.x, position.y, radius, startAngle, angleSpan);
434 /*virtual*/ Object * Arc::Copy(void)
436 #warning "!!! This doesn't take care of attached Dimensions !!!"
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.
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.
446 return new Arc(position, radius, startAngle, angleSpan, parent);