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 "dimension.h"
26 Line::Line(Vector p1, Vector p2, Object * p/*= NULL*/): Object(p1, p),
27 /*type(OTLine),*/ endpoint(p2),
28 draggingLine(false), draggingHandle1(false), draggingHandle2(false), //needUpdate(false),
29 length(Vector::Magnitude(p2, p1)), angle(Vector(endpoint - position).Unit()),
30 hitPoint1(false), hitPoint2(false), hitLine(false)
38 // Taking care of connections should be done by the Container, as we don't know
39 // anything about any other object connected to this one.
41 // If there are any attached Dimensions, we must set the attachment points
42 // to NULL since they will no longer be valid.
43 if (attachedDimension)
45 attachedDimension->SetPoint1(NULL);
46 attachedDimension->SetPoint2(NULL);
48 // IT WOULD BE NICE to have any object points attached to this line automagically
49 // connect to this dimension object at this point, instead of just becoming
55 /*virtual*/ void Line::Draw(Painter * painter)
57 painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
59 if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
60 painter->DrawHandle(position);
62 if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
63 painter->DrawHandle(endpoint);
65 if ((state == OSInactive) && !hitLine)
66 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
68 if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
70 Vector point1 = (draggingHandle1 ? endpoint : position);
71 Vector point2 = (draggingHandle1 ? position : endpoint);
73 Vector current(point2 - point1);
74 Vector v = current.Unit() * length;
75 Vector v2 = point1 + v;
76 // painter->DrawLine((int)point1.x, (int)point1.y, (int)v2.x, (int)v2.y);
77 painter->DrawLine(point1, v2);
79 if (current.Magnitude() > length)
81 painter->SetPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
82 // painter->DrawLine((int)v2.x, (int)v2.y, (int)point2.x, (int)point2.y);
83 painter->DrawLine(v2, point2);
86 // Problem: when drawing at large zoom levels, this throws away precision thus
87 // causing the line to rendered too short. !!! FIX !!! [DONE]
89 // painter->DrawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
90 painter->DrawLine(position, endpoint);
93 /*virtual*/ Vector Line::Center(void)
95 // Technically, this is the midpoint but who are we to quibble? :-)
96 Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
100 /*virtual*/ bool Line::Collided(Vector point)
102 // We can assume this, since this is a mouse down event here.
103 objectWasDragged = false;
107 There's a small problem here with the implementation: You can have a dimension tied
108 to only one point while at the same time you can have a dimension sitting on this line.
109 Since there's only *one* dimPoint for each point, this can be problematic...
111 We solve this by allowing only *one* Dimension object to be attached to the Line,
112 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
114 Problem still arises when we delete this object; The attached Dimension object will
115 then have bad pointers! What it *should* do is delete the object if and only if this
116 line is not attached to any other object. If it is, then one of those attachment
117 points should be sent to the dimension object (done for position & endpoint).
119 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
122 Also: It would be nice to have a preview of the dimension being drawn, with a modifier
123 key to make it draw/show on the other side...
125 TODO: Make Dimension preview with modifier keys for showing on other side
127 // Is the dimension tool active? Let's use it:
130 // User clicked on the line itself (endpoint checks should preceed this one):
131 // (Priorities are taken care of in HitTest()...)
135 if (attachedDimension == NULL)
137 // How to get this object into the top level container???
139 The real question is do we care. I think so, because if this isn't in the top
140 level container, it won't get drawn...
141 But we can fix that by making this object call any attached object's (like
142 a dimension only) Draw() function... :-/
144 attachedDimension = new Dimension(&position, &endpoint, DTLinear, this);
147 parent->Add(attachedDimension);
151 // If there's one already there, tell it to flip sides...
152 attachedDimension->FlipSides();
155 // New approach here: We look for connected objects.
156 Object * attachedDimension = FindAttachedDimension();
158 if (attachedDimension)
160 // If there's an attached Dimension, tell it to switch sides...
161 ((Dimension *)attachedDimension)->FlipSides();
165 // Otherwise, we make a new one and attach it here.
166 attachedDimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
167 connected.push_back(Connection(attachedDimension, 0));
168 connected.push_back(Connection(attachedDimension, 1.0));
171 parent->Add(attachedDimension);
180 if (state == OSInactive)
182 //printf("Line: pp = %lf, length = %lf, distance = %lf\n", parameterizedPoint, lineSegment.Magnitude(), distance);
183 //printf(" v1.Magnitude = %lf, v2.Magnitude = %lf\n", v1.Magnitude(), v2.Magnitude());
184 //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);
186 //How to translate this into pixels from Document space???
187 //Maybe we need to pass a scaling factor in here from the caller? That would make sense, as
188 //the caller knows about the zoom factor and all that good kinda crap
189 //I think what's needed is an Object class variable/method that can be changed by the TLC and
190 //called in derived classes to properly scale the location to the current zoom level. That *should* work.
192 // ALSO: Need to code a global (read: Object class) variable that tells use whether a modifier
193 // key was pressed in addition to the mouse click, so we can do stuff like, say, hold
194 // down CTRL and be able to do multiple selecting of objects (in that case, we would
195 // keep the Object state from changing).
200 oldPoint = position; //maybe "position"?
201 draggingHandle1 = true;
208 oldPoint = endpoint; //maybe "position"?
209 draggingHandle2 = true;
221 else if (state == OSSelected)
223 // Here we test for collision with handles as well! (SOON!) [I think it works...NOPE]
226 if (v1.Magnitude() < 2.0) // Handle #1
227 else if (v2.Magnitude() < 2.0) // Handle #2
232 // state = OSInactive;
236 // Toggle selected state if CTRL held
237 if (qApp->keyboardModifiers() == Qt::ControlModifier)
244 // If CTRL is held, then we bypass the "turn off" code. Still didn't hit
245 // *this* object though. :-)
246 if (qApp->keyboardModifiers() == Qt::ControlModifier)
249 // If we got here, we clicked on nothing, so set the object to inactive.
250 // (Once we can read key modifiers, we can override this to allow multiple selection.)
256 /*virtual*/ void Line::PointerMoved(Vector point)
258 // Hit test tells us what we hit (if anything) through boolean variables. It
259 // also tells us whether or not the state changed.
260 needUpdate = HitTest(point);
262 objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
264 if (objectWasDragged)
266 Vector delta = point - oldPoint;
268 if (draggingHandle1 || draggingLine)
271 if (draggingHandle2 || draggingLine)
279 We can't count on any coupling between the dimension object and us, so how do we do this???
280 Also, there may be more than one Dimension object connected to a single endpoint!
283 - Keep track of the state of the connected dimension
284 - Pass the Dimension the point that's being changed and the delta
287 - Pass the point in a notification function (how?)
288 - Pass the point as a reference to the class instance object (&endpoint). This way, the line
289 doesn't have to care about keeping track of Dimensions connected to it. But still have to
290 care about other connected entities (other Lines, Circles, Arcs, Splines, Texts, etc). I
291 think I'd be OK with this.
292 Since the Dimension has a pointer to our object, all we have to do is update our coordinates
293 and the Dimension object will adjust itself on the next repaint. Problem solved, and we don't
294 have to know anything about how many Dimensions are connected to us, or where! \o/
295 The question then becomes, how do we do this kind of coupling???
297 We need to know about connected entities so that we can have them either move in expected ways
298 or constrain the movement of this Line object. This is how we will be a cut above all other CAD
299 software currently out there: the GUI will try to do the right thing, most of the time. :-)
303 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
304 Vector point1 = (draggingHandle1 ? endpoint : position);
305 Vector point2 = (draggingHandle1 ? position : endpoint);
308 Vector current(point2, point1);
309 Vector v = current.Unit() * length;
310 Vector v2 = point1 + v;
313 if (!Object::fixedLength)
317 if (Object::fixedAngle)
319 // Here we calculate the component of the current vector along the fixed angle.
320 // A_compB = (A . Bu) * Bu
321 double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
323 Actually, this isn't quite right. What we want to do is look for the intersection along either
324 the horizontal line or vertical line that intersects from the current mouse position.
328 position = endpoint + (angle * magnitudeAlongB);
331 endpoint = position + (angle * magnitudeAlongB);
336 //If we tell the dimension to flip sides, this is no longer a valid
337 //assumption. !!! FIX !!!
338 //Ideally, we should just send the point that's changing to the Dimension object
339 //and have it figure out which point needs to move... Or is it???
340 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
341 // so that we don't have to wait until the dragging is done to correct the position of the
342 // point in question, but we'd need another variable tho.
345 dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
348 dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
354 /*virtual*/ void Line::PointerReleased(void)
356 if (draggingHandle1 || draggingHandle2)
358 // Set the length (in case the global state was set to fixed (or not))
359 if (Object::fixedLength)
361 if (draggingHandle1) // startpoint
363 Vector v = Vector(position - endpoint).Unit() * length;
364 position = endpoint + v;
368 // Vector v1 = endpoint - position;
369 Vector v = Vector(endpoint - position).Unit() * length;
370 endpoint = position + v;
375 // Otherwise, we calculate the new length, just in case on the next move
376 // it turns out to have a fixed length. :-)
377 length = Vector(endpoint - position).Magnitude();
380 if (!Object::fixedAngle)
382 // Calculate the new angle, just in case on the next move it turns out to
384 angle = Vector(endpoint - position).Unit();
388 draggingLine = false;
389 draggingHandle1 = false;
390 draggingHandle2 = false;
392 // hitPoint1 = hitPoint2 = hitLine = false;
394 // Here we check for just a click: If object was clicked and dragged, then
395 // revert to the old state (OSInactive). Otherwise, keep the new state that
397 /*Maybe it would be better to just check for "object was dragged" state and not have to worry
398 about keeping track of old states...
400 if (objectWasDragged)
405 // Check to see if the point passed in coincides with any we have. If so, return a
406 // pointer to it; otherwise, return NULL.
407 /*virtual*/ Vector * Line::GetPointAt(Vector v)
411 else if (v == endpoint)
418 /*virtual*/ void Line::Enumerate(FILE * file)
420 fprintf(file, "LINE (%lf,%lf) (%lf,%lf)\n", position.x, position.y, endpoint.x, endpoint.y);
424 /*virtual*/ Object * Line::Copy(void)
426 #warning "!!! This doesn't take care of attached Dimensions !!!"
428 This is a real problem. While having a pointer in the Dimension to this line's points is fast & easy,
429 it creates a huge problem when trying to replicate an object like this.
431 Maybe a way to fix that then, is to have reference numbers instead of pointers. That way, if you copy
432 them, ... you might still have problems. Because you can't be sure if a copy will be persistant or not,
433 you then *definitely* do not want them to have the same reference number.
435 return new Line(position, endpoint, parent);
439 /*virtual*/ Vector Line::GetPointAtParameter(double parameter)
443 else if (parameter >= 1.0)
446 // Our parameter lies between zero and one, so calculate it!
447 Vector v(endpoint, position);
448 double length = v.Magnitude();
449 // We scale the magnitude of v so that it lies between 0 and 1...
450 // By multiplying the parameter by the magnitude, we obtain the point we
451 // want. No scaling necessary as it's inherent in the approach!
452 double spotOnLength = length * parameter;
454 // To get our point, we use the initial point of the line and add in our
456 Vector result = position + (v * spotOnLength);
461 /*virtual*/ QRectF Line::Extents(void)
463 QRectF rect(QPointF(position.x, position.y), QPointF(endpoint.x, endpoint.y));
464 return rect.normalized();
469 /*virtual*/ ObjectType Line::Type(void)
476 void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
478 // If they don't pass one in, create it for the caller.
479 if (dimension == NULL)
481 //printf("Line::SetDimensionOnLine(): Creating new dimension...\n");
482 // dimension = new Dimension(position, endpoint, DTLinear, this);
483 dimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
487 //printf("Line::SetDimensionOnLine(): Adding to parent...\n");
488 parent->Add(dimension);
493 dimension->Connect(this, 0);
494 dimension->Connect(this, 1.0);
497 // Make sure the Dimension is connected to us...
498 Connect(dimension, 0);
499 Connect(dimension, 1.0);
503 Object * Line::FindAttachedDimension(void)
505 // Is there anything connected to this line? If not, return NULL
506 if (connected.size() < 2)
509 // Otherwise, we have to search our objects to see if there's a likely
510 // candidate. In this case, we're looking for a pointer to the same object
511 // with a parameter of 0 and 1 respectively. This is O((n^2)/2).
512 for(uint i=0; i<connected.size(); i++)
514 for(uint j=i+1; j<connected.size(); j++)
516 //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);
517 if ((connected[i].object == connected[j].object)
518 && ((connected[i].t == 0 && connected[j].t == 1.0)
519 || (connected[i].t == 1.0 && connected[j].t == 0)))
520 return connected[i].object;
524 // Didn't find anything, so return NULL
529 bool Line::HitTest(Point point)
533 hitPoint1 = hitPoint2 = hitLine = false;
534 Vector lineSegment = endpoint - position;
535 Vector v1 = point - position;
536 Vector v2 = point - endpoint;
537 double parameterizedPoint = lineSegment.Dot(v1) / lineSegment.Magnitude(), distance;
539 // Geometric interpretation:
540 // The parameterized point on the vector lineSegment is where the perpendicular
541 // intersects lineSegment. If pp < 0, then the perpendicular lies beyond the 1st
542 // endpoint. If pp > length of ls, then the perpendicular lies beyond the 2nd endpoint.
544 if (parameterizedPoint < 0.0)
545 distance = v1.Magnitude();
546 else if (parameterizedPoint > lineSegment.Magnitude())
547 distance = v2.Magnitude();
549 // distance = ?Det?(ls, v1) / |ls|
550 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y) / lineSegment.Magnitude());
552 // Geometric interpretation of the above:
553 // If the segment endpoints are s and e, and the point is p, then the test
554 // for the perpendicular intercepting the segment is equivalent to insisting
555 // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
556 // Perpendicular distance from the point to the segment is computed by first
557 // computing the area of the triangle the three points form, then dividing by
558 // the length of the segment. Distances are done just by the Pythagorean
559 // theorem. Twice the area of the triangle formed by three points is the
560 // determinant of the following matrix:
562 // sx sy 1 0 0 1 0 0 0
563 // ex ey 1 ==> ex ey 1 ==> ex ey 0
564 // px py 1 px py 1 px py 0
566 // By translating the start point to the origin, and subtracting row 1 from
567 // all other rows, we end up with the matrix on the right which greatly
568 // simplifies the calculation of the determinant.
570 //How do we determine distance here? Especially if zoomed in or out???
571 //#warning "!!! Distances tested for may not be valid if zoomed in or out !!!"
573 if ((v1.Magnitude() * Painter::zoom) < 8.0)
575 else if ((v2.Magnitude() * Painter::zoom) < 8.0)
577 else if ((distance * Painter::zoom) < 5.0)
580 return StateChanged();
583 void Line::SaveState(void)
585 oldHitPoint1 = hitPoint1;
586 oldHitPoint2 = hitPoint2;
587 oldHitLine = hitLine;
590 bool Line::StateChanged(void)
592 if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
599 Intersection of two lines:
601 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
603 When they intersect, we can set the equations equal to one another:
605 i + j + t (3i - j) = -i + s (j)
607 Equating coefficients:
608 1 + 3t = -1 and 1 - t = s
609 So t = -2/3 and s = 5/3
611 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 .
614 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
615 and v2(p2x, p2y), v3(p3x, p3y) for l2.
617 d1 = v1 - v0, d2 = v3 - v2
619 Our parametric equations for the line then are:
624 Set r1 = r2, thus we have:
626 v0 + t(d1) = v2 + s(d2)
628 Taking coefficients, we have:
630 p0x + t(d1x) = p2x + s(d2x)
631 p0y + t(d1y) = p2y + s(d2y)
635 t(d1x) - s(d2x) = p2x - p0x
636 t(d1y) - s(d2y) = p2y - p0y
638 Determinant D is ad - bc where the matrix looks like:
643 so D = (d1x)(d2y) - (d2x)(d1y)
644 if D = 0, the lines are parallel.
645 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
646 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
649 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
651 ---------------------------------------------------------------------------------------------------
653 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:
659 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
666 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
667 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
670 crossing vector = v1 * ratio;
674 -----------------------------------
676 So... to code this, let's say we have two Lines: l1 & l2.
678 Vector v1 = l1.endpoint - l1.position;
679 Vector v2 = l2.endpoint - l2.position;
682 Vector normal1(-v1.y, v1.x);
683 Vector normal3(-v3.y, v3.x);
685 double dotProduct1 = v2.Dot(normal1);
686 double dotProduct2 = v2.Dot(normal3);
688 if (dotProduct2 == 0)
689 return ParallelLines;
692 // I think we'd still have to add the intersection to the position point to get the intersection...
693 Point intersection = v1 * (dotProduct1 / dotProduct2);