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 L. 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"
24 Line::Line(Vector p1, Vector p2, Object * p/*= NULL*/): Object(p1, p), endpoint(p2),
25 draggingLine(false), draggingHandle1(false), draggingHandle2(false), //needUpdate(false),
26 length(Vector::Magnitude(p2, p1)), angle(Vector(endpoint - position).Unit()),
27 hitPoint1(false), hitPoint2(false), hitLine(false)
33 // If there are any attached Dimensions, we must set the attachment points
34 // to NULL since they will no longer be valid.
35 if (attachedDimension)
37 attachedDimension->SetPoint1(NULL);
38 attachedDimension->SetPoint2(NULL);
40 // IT WOULD BE NICE to have any object points attached to this line automagically
41 // connect to this dimension object at this point, instead of just becoming
45 /*virtual*/ void Line::Draw(QPainter * painter)
47 painter->setPen(QPen(Qt::red, 2.0, Qt::DotLine));
49 if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
50 painter->drawEllipse(QPointF(position.x, position.y), 4.0, 4.0);
52 if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
53 painter->drawEllipse(QPointF(endpoint.x, endpoint.y), 4.0, 4.0);
55 if ((state == OSInactive) && !hitLine)
56 painter->setPen(QPen(Qt::black, 1.0, Qt::SolidLine));
58 if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
60 Vector point1 = (draggingHandle1 ? endpoint : position);
61 Vector point2 = (draggingHandle1 ? position : endpoint);
63 Vector current(point2 - point1);
64 Vector v = current.Unit() * length;
65 Vector v2 = point1 + v;
66 painter->drawLine((int)point1.x, (int)point1.y, (int)v2.x, (int)v2.y);
68 if (current.Magnitude() > length)
70 painter->setPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
71 painter->drawLine((int)v2.x, (int)v2.y, (int)point2.x, (int)point2.y);
75 painter->drawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
78 /*virtual*/ Vector Line::Center(void)
80 // Technically, this is the midpoint but who are we to quibble? :-)
81 Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
85 /*virtual*/ bool Line::Collided(Vector point)
88 // Actually, we can, since this is a mouse down event here.
89 objectWasDragged = false;
93 There's a small problem here with the implementation: You can have a dimension tied
94 to only one point while at the same time you can have a dimension sitting on this line.
95 Since there's only *one* dimPoint for each point, this can be problematic...
97 We solve this by allowing only *one* Dimension object to be attached to the Line,
98 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
100 Problem still arises when we delete this object; The attached Dimension object will
101 then have bad pointers! What it *should* do is delete the object if and only if this
102 line is not attached to any other object. If it is, then one of those attachment
103 points should be sent to the dimension object (done for position & endpoint).
105 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
108 Also: It would be nice to have a preview of the dimension being drawn, with a modifier
109 key to make it draw/show on the other side...
111 TODO: Make Dimension preview with modifier keys for showing on other side
113 // Is the dimension tool active? Let's use it:
116 // User clicked on the line itself (endpoint checks should preceed this one):
117 // (Priorities are taken care of in HitTest()...)
120 if (attachedDimension == NULL)
122 // How to get this object into the top level container???
124 The real question is do we care. I think so, because if this isn't in the top
125 level container, it won't get drawn...
126 But we can fix that by making this object call any attached object's (like
127 a dimension only) Draw() function... :-/
129 attachedDimension = new Dimension(&position, &endpoint, this);
132 parent->Add(attachedDimension);
136 // If there's one already there, tell it to flip sides...
137 attachedDimension->FlipSides();
145 if (state == OSInactive)
147 //printf("Line: pp = %lf, length = %lf, distance = %lf\n", parameterizedPoint, lineSegment.Magnitude(), distance);
148 //printf(" v1.Magnitude = %lf, v2.Magnitude = %lf\n", v1.Magnitude(), v2.Magnitude());
149 //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);
151 //How to translate this into pixels from Document space???
152 //Maybe we need to pass a scaling factor in here from the caller? That would make sense, as
153 //the caller knows about the zoom factor and all that good kinda crap
154 //I think what's needed is an Object class variable/method that can be changed by the TLC and
155 //called in derived classes to properly scale the location to the current zoom level. That *should* work.
157 // ALSO: Need to code a global (read: Object class) variable that tells use whether a modifier
158 // key was pressed in addition to the mouse click, so we can do stuff like, say, hold
159 // down CTRL and be able to do multiple selecting of objects (in that case, we would
160 // keep the Object state from changing).
165 oldPoint = position; //maybe "position"?
166 draggingHandle1 = true;
173 oldPoint = endpoint; //maybe "position"?
174 draggingHandle2 = true;
186 else if (state == OSSelected)
188 // Here we test for collision with handles as well! (SOON!) [I think it works...NOPE]
191 if (v1.Magnitude() < 2.0) // Handle #1
192 else if (v2.Magnitude() < 2.0) // Handle #2
197 // state = OSInactive;
204 // If we got here, we clicked on nothing, so set the object to inactive.
205 // (Once we can read key modifiers, we can override this to allow multiple selection.)
210 /*virtual*/ void Line::PointerMoved(Vector point)
212 // Hit test tells us what we hit (if anything) through boolean variables. It
213 // also tells us whether or not the state changed.
214 needUpdate = HitTest(point);
216 objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
218 if (objectWasDragged)
220 Vector delta = point - oldPoint;
222 if (draggingHandle1 || draggingLine)
225 if (draggingHandle2 || draggingLine)
233 We can't count on any coupling between the dimension object and us, so how do we do this???
234 Also, there may be more than one Dimension object connected to a single endpoint!
237 - Keep track of the state of the connected dimension
238 - Pass the Dimension the point that's being changed and the delta
241 - Pass the point in a notification function (how?)
242 - Pass the point as a reference to the class instance object (&endpoint). This way, the line
243 doesn't have to care about keeping track of Dimensions connected to it. But still have to
244 care about other connected entities (other Lines, Circles, Arcs, Splines, Texts, etc). I
245 think I'd be OK with this.
246 Since the Dimension has a pointer to our object, all we have to do is update our coordinates
247 and the Dimension object will adjust itself on the next repaint. Problem solved, and we don't
248 have to know anything about how many Dimensions are connected to us, or where! \o/
249 The question then becomes, how do we do this kind of coupling???
251 We need to know about connected entities so that we can have them either move in expected ways
252 or constrain the movement of this Line object. This is how we will be a cut above all other CAD
253 software currently out there: the GUI will try to do the right thing, most of the time. :-)
257 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
258 Vector point1 = (draggingHandle1 ? endpoint : position);
259 Vector point2 = (draggingHandle1 ? position : endpoint);
262 Vector current(point2, point1);
263 Vector v = current.Unit() * length;
264 Vector v2 = point1 + v;
267 if (!Object::fixedLength)
271 if (Object::fixedAngle)
273 // Here we calculate the component of the current vector along the fixed angle.
274 // A_compB = (A . Bu) * Bu
275 double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
278 position = endpoint + (angle * magnitudeAlongB);
281 endpoint = position + (angle * magnitudeAlongB);
286 //If we tell the dimension to flip sides, this is no longer a valid
287 //assumption. !!! FIX !!!
288 //Ideally, we should just send the point that's changing to the Dimension object
289 //and have it figure out which point needs to move... Or is it???
290 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
291 // so that we don't have to wait until the dragging is done to correct the position of the
292 // point in question, but we'd need another variable tho.
295 dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
298 dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
303 /*virtual*/ void Line::PointerReleased(void)
305 if (draggingHandle1 || draggingHandle2)
307 // Set the length (in case the global state was set to fixed (or not))
308 if (Object::fixedLength)
310 if (draggingHandle1) // startpoint
312 Vector v = Vector(position - endpoint).Unit() * length;
313 position = endpoint + v;
317 // Vector v1 = endpoint - position;
318 Vector v = Vector(endpoint - position).Unit() * length;
319 endpoint = position + v;
324 // Otherwise, we calculate the new length, just in case on the next move
325 // it turns out to have a fixed length. :-)
326 length = Vector(endpoint - position).Magnitude();
329 if (!Object::fixedAngle)
331 // Calculate the new angle, just in case on the next move it turns out to
333 angle = Vector(endpoint - position).Unit();
337 draggingLine = false;
338 draggingHandle1 = false;
339 draggingHandle2 = false;
341 // hitPoint1 = hitPoint2 = hitLine = false;
343 // Here we check for just a click: If object was clicked and dragged, then
344 // revert to the old state (OSInactive). Otherwise, keep the new state that
346 /*Maybe it would be better to just check for "object was dragged" state and not have to worry
347 about keeping track of old states...
349 if (objectWasDragged)
354 void Line::SetDimensionOnPoint1(Dimension * dimension)
356 dimPoint1 = dimension;
359 dimension->SetPoint1(position);
362 void Line::SetDimensionOnPoint2(Dimension * dimension)
364 dimPoint2 = dimension;
367 dimension->SetPoint2(endpoint);
370 void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
372 // If they don't pass one in, create it for the caller.
373 if (dimension == NULL)
375 dimension = new Dimension(&position, &endpoint, this);
378 parent->Add(dimension);
381 attachedDimension = dimension;
383 // After we set the points here, we don't have to care about them anymore.
386 dimension->SetPoint1(&position);
387 dimension->SetPoint2(&endpoint);
392 bool Line::HitTest(Point point)
396 hitPoint1 = hitPoint2 = hitLine = false;
397 Vector lineSegment = endpoint - position;
398 Vector v1 = point - position;
399 Vector v2 = point - endpoint;
400 double parameterizedPoint = lineSegment.Dot(v1) / lineSegment.Magnitude(), distance;
402 // Geometric interpretation:
403 // The parameterized point on the vector lineSegment is where the perpendicular
404 // intersects lineSegment. If pp < 0, then the perpendicular lies beyond the 1st
405 // endpoint. If pp > length of ls, then the perpendicular lies beyond the 2nd endpoint.
407 if (parameterizedPoint < 0.0)
408 distance = v1.Magnitude();
409 else if (parameterizedPoint > lineSegment.Magnitude())
410 distance = v2.Magnitude();
412 // distance = ?Det?(ls, v1) / |ls|
413 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y) / lineSegment.Magnitude());
415 // Geometric interpretation of the above:
416 // If the segment endpoints are s and e, and the point is p, then the test
417 // for the perpendicular intercepting the segment is equivalent to insisting
418 // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
419 // Perpendicular distance from the point to the segment is computed by first
420 // computing the area of the triangle the three points form, then dividing by
421 // the length of the segment. Distances are done just by the Pythagorean
422 // theorem. Twice the area of the triangle formed by three points is the
423 // determinant of the following matrix:
425 // sx sy 1 0 0 1 0 0 0
426 // ex ey 1 ==> ex ey 1 ==> ex ey 0
427 // px py 1 px py 1 px py 0
429 // By translating the start point to the origin, and subtracting row 1 from
430 // all other rows, we end up with the matrix on the right which greatly
431 // simplifies the calculation of the determinant.
433 //How do we determine distance here? Especially if zoomed in or out???
434 #warning "!!! Distances tested for may not be valid if zoomed in or out !!!"
435 if (v1.Magnitude() < 8.0)
437 else if (v2.Magnitude() < 8.0)
439 else if (distance < 5.0)
442 return StateChanged();
445 void Line::SaveState(void)
447 oldHitPoint1 = hitPoint1;
448 oldHitPoint2 = hitPoint2;
449 oldHitLine = hitLine;
452 bool Line::StateChanged(void)
454 if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
461 Intersection of two lines:
463 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
465 When they intersect, we can set the equations equal to one another:
467 i + j + t (3i - j) = -i + s (j)
469 Equating coefficients:
470 1 + 3t = -1 and 1 - t = s
471 So t = -2/3 and s = 5/3
473 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 .
476 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
477 and v2(p2x, p2y), v3(p3x, p3y) for l2.
479 d1 = v1 - v0, d2 = v3 - v2
481 Our parametric equations for the line then are:
486 Set r1 = r2, thus we have:
488 v0 + t(d1) = v2 + s(d2)
490 Taking coefficients, we have:
492 p0x + t(d1x) = p2x + s(d2x)
493 p0y + t(d1y) = p2y + s(d2y)
497 t(d1x) - s(d2x) = p2x - p0x
498 t(d1y) - s(d2y) = p2y - p0y
500 Determinant D is ad - bc where the matrix look like:
505 so D = (d1x)(d2y) - (d2x)(d1y)
506 if D = 0, the lines are parallel.
507 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
508 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
511 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
513 ---------------------------------------------------------------------------------------------------
515 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:
521 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
528 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
529 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
532 crossing vector = v1 * ratio;
536 -----------------------------------
538 So... to code this, let's say we have two Lines: l1 & l2.
540 Vector v1 = l1.endpoint - l1.position;
541 Vector v2 = l2.endpoint - l2.position;
544 Vector normal1(-v1.y, v1.x);
545 Vector normal3(-v3.y, v3.x);
547 double dotProduct1 = v2.Dot(normal1);
548 double dotProduct2 = v2.Dot(normal3);
550 if (dotProduct2 == 0)
551 return ParallelLines;
554 // I think we'd still have to add the intersection to the position point to get the intersection...
555 Point intersection = v1 * (dotProduct1 / dotProduct2);