1 // line.cpp: Line object
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/22/2011 Created this file
12 // JLH 04/11/2011 Fixed attached dimensions to stay at correct length when
13 // "Fixed Length" button is down
14 // JLH 04/27/2011 Fixed attached dimension to stay a correct length when
15 // "Fixed Length" button is *not* down ;-)
16 // JLH 05/29/2011 Added (some) mouseover hints
22 #include "container.h"
23 #include "dimension.h"
24 #include "mathconstants.h"
28 Line::Line(Vector p1, Vector p2, Object * p/*= NULL*/): Object(p1, p),
29 /*type(OTLine),*/ endpoint(p2),
30 draggingLine(false), draggingHandle1(false), draggingHandle2(false), //needUpdate(false),
31 length(Vector::Magnitude(p2, p1)), angle(Vector(endpoint - position).Unit()),
32 hitPoint1(false), hitPoint2(false), hitLine(false)
40 // Taking care of connections should be done by the Container, as we don't know
41 // anything about any other object connected to this one.
43 // If there are any attached Dimensions, we must set the attachment points
44 // to NULL since they will no longer be valid.
45 if (attachedDimension)
47 attachedDimension->SetPoint1(NULL);
48 attachedDimension->SetPoint2(NULL);
50 // IT WOULD BE NICE to have any object points attached to this line automagically
51 // connect to this dimension object at this point, instead of just becoming
57 /*virtual*/ void Line::Draw(Painter * painter)
59 painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
61 if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
62 painter->DrawHandle(position);
64 if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
65 painter->DrawHandle(endpoint);
67 if ((state == OSInactive) && !hitLine)
68 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
70 if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
72 Vector point1 = (draggingHandle1 ? endpoint : position);
73 Vector point2 = (draggingHandle1 ? position : endpoint);
75 Vector current(point2 - point1);
76 Vector v = current.Unit() * length;
77 Vector v2 = point1 + v;
78 // painter->DrawLine((int)point1.x, (int)point1.y, (int)v2.x, (int)v2.y);
79 painter->DrawLine(point1, v2);
81 if (current.Magnitude() > length)
83 painter->SetPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
84 // painter->DrawLine((int)v2.x, (int)v2.y, (int)point2.x, (int)point2.y);
85 painter->DrawLine(v2, point2);
88 // Problem: when drawing at large zoom levels, this throws away precision thus
89 // causing the line to rendered too short. !!! FIX !!! [DONE]
91 // painter->DrawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
92 painter->DrawLine(position, endpoint);
94 // If we're rotating or setting the span, draw an information panel
95 // showing both absolute and relative angles being set.
96 if (draggingHandle1 || draggingHandle2)
98 double absAngle = (Vector(endpoint - position).Angle()) * RADIANS_TO_DEGREES;
99 // double relAngle = (startAngle >= oldAngle ? startAngle - oldAngle :
100 // startAngle - oldAngle + (2.0 * PI)) * RADIANS_TO_DEGREES;
101 double absLength = Vector(position - endpoint).Magnitude();
105 text = QObject::tr("Length: %1 in.\n") + QChar(0x2221) + QObject::tr(": %2");
106 text = text.arg(absLength).arg(absAngle);
108 QPen pen = QPen(QColor(0x00, 0xFF, 0x00), 1.0, Qt::SolidLine);
109 painter->SetPen(pen);
110 painter->SetBrush(QBrush(QColor(0x40, 0xFF, 0x40, 0x9F)));
111 QRectF textRect(10.0, 10.0, 270.0, 70.0); // x, y, w, h
112 painter->DrawRoundedRect(textRect, 7.0, 7.0);
114 textRect.setLeft(textRect.left() + 14);
115 painter->SetFont(*Object::font);
116 // pen = QPen(QColor(0xDF, 0x5F, 0x00), 1.0, Qt::SolidLine);
117 pen = QPen(QColor(0x00, 0x5F, 0xDF));
118 painter->SetPen(pen);
119 painter->DrawText(textRect, Qt::AlignVCenter, text);
120 // painter->SetPen(QPen(QColor(0xDF, 0x5F, 0x00)));
124 /*virtual*/ Vector Line::Center(void)
126 // Technically, this is the midpoint but who are we to quibble? :-)
127 Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
131 /*virtual*/ bool Line::Collided(Vector point)
133 // We can assume this, since this is a mouse down event here.
134 objectWasDragged = false;
137 // If we're part of a non-top-level container, send this signal to it
138 if (parent->type == OTContainer && !((Container *)parent)->isTopLevelContainer
139 && (hitLine || hitPoint1 || hitPoint2))
141 parent->state = OSSelected;
146 There's a small problem here with the implementation: You can have a dimension tied
147 to only one point while at the same time you can have a dimension sitting on this line.
148 Since there's only *one* dimPoint for each point, this can be problematic...
150 We solve this by allowing only *one* Dimension object to be attached to the Line,
151 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
153 Problem still arises when we delete this object; The attached Dimension object will
154 then have bad pointers! What it *should* do is delete the object if and only if this
155 line is not attached to any other object. If it is, then one of those attachment
156 points should be sent to the dimension object (done for position & endpoint).
158 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
161 Also: It would be nice to have a preview of the dimension being drawn, with a modifier
162 key to make it draw/show on the other side...
164 TODO: Make Dimension preview with modifier keys for showing on other side
166 // Is the dimension tool active? Let's use it:
169 // User clicked on the line itself (endpoint checks should preceed this one):
170 // (Priorities are taken care of in HitTest()...)
174 if (attachedDimension == NULL)
176 // How to get this object into the top level container???
178 The real question is do we care. I think so, because if this isn't in the top
179 level container, it won't get drawn...
180 But we can fix that by making this object call any attached object's (like
181 a dimension only) Draw() function... :-/
183 attachedDimension = new Dimension(&position, &endpoint, DTLinear, this);
186 parent->Add(attachedDimension);
190 // If there's one already there, tell it to flip sides...
191 attachedDimension->FlipSides();
194 // New approach here: We look for connected objects.
195 Object * attachedDimension = FindAttachedDimension();
197 if (attachedDimension)
199 // If there's an attached Dimension, tell it to switch sides...
200 ((Dimension *)attachedDimension)->FlipSides();
204 // Otherwise, we make a new one and attach it here.
205 attachedDimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
206 connected.push_back(Connection(attachedDimension, 0));
207 connected.push_back(Connection(attachedDimension, 1.0));
210 parent->Add(attachedDimension);
219 if (state == OSInactive)
221 //printf("Line: pp = %lf, length = %lf, distance = %lf\n", parameterizedPoint, lineSegment.Magnitude(), distance);
222 //printf(" v1.Magnitude = %lf, v2.Magnitude = %lf\n", v1.Magnitude(), v2.Magnitude());
223 //printf(" point = %lf,%lf,%lf; p1 = %lf,%lf,%lf; p2 = %lf,%lf,%lf\n", point.x, point.y, point.z, position.x, position.y, position.z, endpoint.x, endpoint.y, endpoint.z);
225 //How to translate this into pixels from Document space???
226 //Maybe we need to pass a scaling factor in here from the caller? That would make sense, as
227 //the caller knows about the zoom factor and all that good kinda crap
228 //I think what's needed is an Object class variable/method that can be changed by the TLC and
229 //called in derived classes to properly scale the location to the current zoom level. That *should* work.
231 // ALSO: Need to code a global (read: Object class) variable that tells use whether a modifier
232 // key was pressed in addition to the mouse click, so we can do stuff like, say, hold
233 // down CTRL and be able to do multiple selecting of objects (in that case, we would
234 // keep the Object state from changing).
239 oldPoint = position; //maybe "position"?
240 draggingHandle1 = true;
247 oldPoint = endpoint; //maybe "position"?
248 draggingHandle2 = true;
260 else if (state == OSSelected)
262 // Here we test for collision with handles as well! (SOON!) [I think it works...NOPE]
265 if (v1.Magnitude() < 2.0) // Handle #1
266 else if (v2.Magnitude() < 2.0) // Handle #2
271 // state = OSInactive;
275 // Toggle selected state if CTRL held
276 if (qApp->keyboardModifiers() == Qt::ControlModifier)
283 // If CTRL is held, then we bypass the "turn off" code. Still didn't hit
284 // *this* object though. :-)
285 if (qApp->keyboardModifiers() == Qt::ControlModifier)
288 // If we got here, we clicked on nothing, so set the object to inactive.
289 // (Once we can read key modifiers, we can override this to allow multiple selection.)
295 /*virtual*/ void Line::PointerMoved(Vector point)
297 if (selectionInProgress)
299 // Check for whether or not the rect contains this line
301 if (selection.normalized().contains(Extents()))
303 if (selection.normalized().contains(position.x, position.y)
304 && selection.normalized().contains(endpoint.x, endpoint.y))
313 // Hit test tells us what we hit (if anything) through boolean variables. It
314 // also tells us whether or not the state changed.
315 needUpdate = HitTest(point);
317 objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
319 if (objectWasDragged)
321 Vector delta = point - oldPoint;
323 if (draggingHandle1 || draggingLine)
326 if (draggingHandle2 || draggingLine)
332 //doesn't work QMainWindow::statusBar()->setText("You are manipulating a line");
336 We can't count on any coupling between the dimension object and us, so how do we do this???
337 Also, there may be more than one Dimension object connected to a single endpoint!
340 - Keep track of the state of the connected dimension
341 - Pass the Dimension the point that's being changed and the delta
344 - Pass the point in a notification function (how?)
345 - Pass the point as a reference to the class instance object (&endpoint). This
346 way, the line doesn't have to care about keeping track of Dimensions
347 connected to it. But still have to care about other connected entities
348 (other Lines, Circles, Arcs, Splines, Texts, etc). I think I'd be OK with
349 this. Since the Dimension has a pointer to our object, all we have to do is
350 update our coordinates and the Dimension object will adjust itself on the
351 next repaint. Problem solved, and we don't have to know anything about how
352 many Dimensions are connected to us, or where! \o/
353 The question then becomes, how do we do this kind of coupling???
355 We need to know about connected entities so that we can have them either move
356 in expected ways or constrain the movement of this Line object. This is how we
357 will be a cut above all other CAD software currently out there: the GUI will
358 try to do the right thing, most of the time. :-)
362 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
363 Vector point1 = (draggingHandle1 ? endpoint : position);
364 Vector point2 = (draggingHandle1 ? position : endpoint);
367 Vector current(point2, point1);
368 Vector v = current.Unit() * length;
369 Vector v2 = point1 + v;
372 if (!Object::fixedLength)
376 if (Object::fixedAngle)
378 // Here we calculate the component of the current vector along the fixed angle.
379 // A_compB = (A . Bu) * Bu
380 double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
382 Actually, this isn't quite right. What we want to do is look for the intersection along either
383 the horizontal line or vertical line that intersects from the current mouse position.
387 position = endpoint + (angle * magnitudeAlongB);
390 endpoint = position + (angle * magnitudeAlongB);
395 //If we tell the dimension to flip sides, this is no longer a valid
396 //assumption. !!! FIX !!!
397 //Ideally, we should just send the point that's changing to the Dimension object
398 //and have it figure out which point needs to move... Or is it???
399 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
400 // so that we don't have to wait until the dragging is done to correct the position of the
401 // point in question, but we'd need another variable tho.
404 dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
407 dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
413 /*virtual*/ void Line::PointerReleased(void)
415 if (draggingHandle1 || draggingHandle2)
417 // Set the length (in case the global state was set to fixed (or not))
418 if (Object::fixedLength)
420 if (draggingHandle1) // startpoint
422 Vector v = Vector(position - endpoint).Unit() * length;
423 position = endpoint + v;
427 // Vector v1 = endpoint - position;
428 Vector v = Vector(endpoint - position).Unit() * length;
429 endpoint = position + v;
434 // Otherwise, we calculate the new length, just in case on the next move
435 // it turns out to have a fixed length. :-)
436 length = Vector(endpoint - position).Magnitude();
439 if (!Object::fixedAngle)
441 // Calculate the new angle, just in case on the next move it turns out to
443 angle = Vector(endpoint - position).Unit();
447 draggingLine = false;
448 draggingHandle1 = false;
449 draggingHandle2 = false;
451 // hitPoint1 = hitPoint2 = hitLine = false;
453 // Here we check for just a click: If object was clicked and dragged, then
454 // revert to the old state (OSInactive). Otherwise, keep the new state that
456 /*Maybe it would be better to just check for "object was dragged" state and not have to worry
457 about keeping track of old states...
459 if (objectWasDragged)
464 /*virtual*/ bool Line::HitTest(Point point)
468 hitPoint1 = hitPoint2 = hitLine = false;
469 Vector lineSegment = endpoint - position;
470 Vector v1 = point - position;
471 Vector v2 = point - endpoint;
472 double parameterizedPoint = lineSegment.Dot(v1) / lineSegment.Magnitude(), distance;
474 // Geometric interpretation:
475 // The parameterized point on the vector lineSegment is where the perpendicular
476 // intersects lineSegment. If pp < 0, then the perpendicular lies beyond the 1st
477 // endpoint. If pp > length of ls, then the perpendicular lies beyond the 2nd endpoint.
479 if (parameterizedPoint < 0.0)
480 distance = v1.Magnitude();
481 else if (parameterizedPoint > lineSegment.Magnitude())
482 distance = v2.Magnitude();
484 // distance = ?Det?(ls, v1) / |ls|
485 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y) / lineSegment.Magnitude());
487 // Geometric interpretation of the above:
488 // If the segment endpoints are s and e, and the point is p, then the test
489 // for the perpendicular intercepting the segment is equivalent to insisting
490 // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
491 // Perpendicular distance from the point to the segment is computed by first
492 // computing the area of the triangle the three points form, then dividing by
493 // the length of the segment. Distances are done just by the Pythagorean
494 // theorem. Twice the area of the triangle formed by three points is the
495 // determinant of the following matrix:
497 // sx sy 1 0 0 1 0 0 0
498 // ex ey 1 ==> ex ey 1 ==> ex ey 0
499 // px py 1 px py 1 px py 0
501 // By translating the start point to the origin, and subtracting row 1 from
502 // all other rows, we end up with the matrix on the right which greatly
503 // simplifies the calculation of the determinant.
505 //How do we determine distance here? Especially if zoomed in or out???
506 //#warning "!!! Distances tested for may not be valid if zoomed in or out !!!"
508 if ((v1.Magnitude() * Painter::zoom) < 8.0)
510 else if ((v2.Magnitude() * Painter::zoom) < 8.0)
512 else if ((distance * Painter::zoom) < 5.0)
515 return StateChanged();
519 // Check to see if the point passed in coincides with any we have. If so, return a
520 // pointer to it; otherwise, return NULL.
521 /*virtual*/ Vector * Line::GetPointAt(Vector v)
525 else if (v == endpoint)
532 /*virtual*/ void Line::Enumerate(FILE * file)
534 fprintf(file, "LINE (%lf,%lf) (%lf,%lf)\n", position.x, position.y, endpoint.x, endpoint.y);
538 /*virtual*/ Object * Line::Copy(void)
540 #warning "!!! This doesn't take care of attached Dimensions !!!"
542 This is a real problem. While having a pointer in the Dimension to this line's points is fast & easy,
543 it creates a huge problem when trying to replicate an object like this.
545 Maybe a way to fix that then, is to have reference numbers instead of pointers. That way, if you copy
546 them, ... you might still have problems. Because you can't be sure if a copy will be persistant or not,
547 you then *definitely* do not want them to have the same reference number.
549 return new Line(position, endpoint, parent);
553 /*virtual*/ Vector Line::GetPointAtParameter(double parameter)
557 else if (parameter >= 1.0)
560 // Our parameter lies between zero and one, so calculate it!
561 Vector v(endpoint, position);
562 double length = v.Magnitude();
563 // We scale the magnitude of v so that it lies between 0 and 1...
564 // By multiplying the parameter by the magnitude, we obtain the point we
565 // want. No scaling necessary as it's inherent in the approach!
566 double spotOnLength = length * parameter;
568 // To get our point, we use the initial point of the line and add in our
570 Vector result = position + (v * spotOnLength);
575 /*virtual*/ QRectF Line::Extents(void)
577 QRectF rect(QPointF(position.x, position.y), QPointF(endpoint.x, endpoint.y));
578 return rect.normalized();
582 void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
584 // If they don't pass one in, create it for the caller.
585 if (dimension == NULL)
587 //printf("Line::SetDimensionOnLine(): Creating new dimension...\n");
588 // dimension = new Dimension(position, endpoint, DTLinear, this);
589 dimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
593 //printf("Line::SetDimensionOnLine(): Adding to parent...\n");
594 parent->Add(dimension);
599 dimension->Connect(this, 0);
600 dimension->Connect(this, 1.0);
603 // Make sure the Dimension is connected to us...
604 Connect(dimension, 0);
605 Connect(dimension, 1.0);
609 Object * Line::FindAttachedDimension(void)
611 // Is there anything connected to this line? If not, return NULL
612 if (connected.size() < 2)
615 // Otherwise, we have to search our objects to see if there's a likely
616 // candidate. In this case, we're looking for a pointer to the same object
617 // with a parameter of 0 and 1 respectively. This is O((n^2)/2).
618 for(uint i=0; i<connected.size(); i++)
620 for(uint j=i+1; j<connected.size(); j++)
622 //printf("Line: connected[i]=%X, connected[j]=%X, connected[i].t=%lf, connected[j].t=%lf\n", connected[i].object, connected[j].object, connected[i].t, connected[j].t);
623 if ((connected[i].object == connected[j].object)
624 && ((connected[i].t == 0 && connected[j].t == 1.0)
625 || (connected[i].t == 1.0 && connected[j].t == 0)))
626 return connected[i].object;
630 // Didn't find anything, so return NULL
635 void Line::SaveState(void)
637 oldHitPoint1 = hitPoint1;
638 oldHitPoint2 = hitPoint2;
639 oldHitLine = hitLine;
643 bool Line::StateChanged(void)
645 if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
653 Intersection of two lines:
655 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
657 When they intersect, we can set the equations equal to one another:
659 i + j + t (3i - j) = -i + s (j)
661 Equating coefficients:
662 1 + 3t = -1 and 1 - t = s
663 So t = -2/3 and s = 5/3
665 The position vector of the intersection point is therefore given by putting t = -2/3 or s = 5/3 into one of the above equations. This gives -i +5j/3 .
668 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
669 and v2(p2x, p2y), v3(p3x, p3y) for l2.
671 d1 = v1 - v0, d2 = v3 - v2
673 Our parametric equations for the line then are:
678 Set r1 = r2, thus we have:
680 v0 + t(d1) = v2 + s(d2)
682 Taking coefficients, we have:
684 p0x + t(d1x) = p2x + s(d2x)
685 p0y + t(d1y) = p2y + s(d2y)
689 t(d1x) - s(d2x) = p2x - p0x
690 t(d1y) - s(d2y) = p2y - p0y
692 Determinant D is ad - bc where the matrix looks like:
697 so D = (d1x)(d2y) - (d2x)(d1y)
698 if D = 0, the lines are parallel.
699 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
700 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
703 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
705 ---------------------------------------------------------------------------------------------------
707 The first and most preferred method for intersection calculation is the perp-product calculation. There are two vectors, v1 and v2. Create a third vector vector between the starting points of these vectors, and calculate the perp product of v2 and the two other vectors. These two scalars have to be divided to get the mulitplication ratio of v1 to reach intersection point. So:
713 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
720 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
721 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
724 crossing vector = v1 * ratio;
728 -----------------------------------
730 So... to code this, let's say we have two Lines: l1 & l2.
732 Vector v1 = l1.endpoint - l1.position;
733 Vector v2 = l2.endpoint - l2.position;
736 Vector normal1(-v1.y, v1.x);
737 Vector normal3(-v3.y, v3.x);
739 double dotProduct1 = v2.Dot(normal1);
740 double dotProduct2 = v2.Dot(normal3);
742 if (dotProduct2 == 0)
743 return ParallelLines;
746 // I think we'd still have to add the intersection to the position point to get the intersection...
747 Point intersection = v1 * (dotProduct1 / dotProduct2);