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), endpoint(p2),
27 draggingLine(false), draggingHandle1(false), draggingHandle2(false), //needUpdate(false),
28 length(Vector::Magnitude(p2, p1)), angle(Vector(endpoint - position).Unit()),
29 hitPoint1(false), hitPoint2(false), hitLine(false)
35 // If there are any attached Dimensions, we must set the attachment points
36 // to NULL since they will no longer be valid.
37 if (attachedDimension)
39 attachedDimension->SetPoint1(NULL);
40 attachedDimension->SetPoint2(NULL);
42 // IT WOULD BE NICE to have any object points attached to this line automagically
43 // connect to this dimension object at this point, instead of just becoming
47 /*virtual*/ void Line::Draw(Painter * painter)
49 painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
51 if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
52 painter->DrawHandle(position);
54 if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
55 painter->DrawHandle(endpoint);
57 if ((state == OSInactive) && !hitLine)
58 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
60 if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
62 Vector point1 = (draggingHandle1 ? endpoint : position);
63 Vector point2 = (draggingHandle1 ? position : endpoint);
65 Vector current(point2 - point1);
66 Vector v = current.Unit() * length;
67 Vector v2 = point1 + v;
68 // painter->DrawLine((int)point1.x, (int)point1.y, (int)v2.x, (int)v2.y);
69 painter->DrawLine(point1, v2);
71 if (current.Magnitude() > length)
73 painter->SetPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
74 // painter->DrawLine((int)v2.x, (int)v2.y, (int)point2.x, (int)point2.y);
75 painter->DrawLine(v2, point2);
78 // Problem: when drawing at large zoom levels, this throws away precision thus
79 // causing the line to rendered too short. !!! FIX !!! [DONE]
81 // painter->DrawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
82 painter->DrawLine(position, endpoint);
85 /*virtual*/ Vector Line::Center(void)
87 // Technically, this is the midpoint but who are we to quibble? :-)
88 Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
92 /*virtual*/ bool Line::Collided(Vector point)
94 // We can assume this, since this is a mouse down event here.
95 objectWasDragged = false;
99 There's a small problem here with the implementation: You can have a dimension tied
100 to only one point while at the same time you can have a dimension sitting on this line.
101 Since there's only *one* dimPoint for each point, this can be problematic...
103 We solve this by allowing only *one* Dimension object to be attached to the Line,
104 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
106 Problem still arises when we delete this object; The attached Dimension object will
107 then have bad pointers! What it *should* do is delete the object if and only if this
108 line is not attached to any other object. If it is, then one of those attachment
109 points should be sent to the dimension object (done for position & endpoint).
111 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
114 Also: It would be nice to have a preview of the dimension being drawn, with a modifier
115 key to make it draw/show on the other side...
117 TODO: Make Dimension preview with modifier keys for showing on other side
119 // Is the dimension tool active? Let's use it:
122 // User clicked on the line itself (endpoint checks should preceed this one):
123 // (Priorities are taken care of in HitTest()...)
126 if (attachedDimension == NULL)
128 // How to get this object into the top level container???
130 The real question is do we care. I think so, because if this isn't in the top
131 level container, it won't get drawn...
132 But we can fix that by making this object call any attached object's (like
133 a dimension only) Draw() function... :-/
135 attachedDimension = new Dimension(&position, &endpoint, DTLinear, this);
138 parent->Add(attachedDimension);
142 // If there's one already there, tell it to flip sides...
143 attachedDimension->FlipSides();
151 if (state == OSInactive)
153 //printf("Line: pp = %lf, length = %lf, distance = %lf\n", parameterizedPoint, lineSegment.Magnitude(), distance);
154 //printf(" v1.Magnitude = %lf, v2.Magnitude = %lf\n", v1.Magnitude(), v2.Magnitude());
155 //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);
157 //How to translate this into pixels from Document space???
158 //Maybe we need to pass a scaling factor in here from the caller? That would make sense, as
159 //the caller knows about the zoom factor and all that good kinda crap
160 //I think what's needed is an Object class variable/method that can be changed by the TLC and
161 //called in derived classes to properly scale the location to the current zoom level. That *should* work.
163 // ALSO: Need to code a global (read: Object class) variable that tells use whether a modifier
164 // key was pressed in addition to the mouse click, so we can do stuff like, say, hold
165 // down CTRL and be able to do multiple selecting of objects (in that case, we would
166 // keep the Object state from changing).
171 oldPoint = position; //maybe "position"?
172 draggingHandle1 = true;
179 oldPoint = endpoint; //maybe "position"?
180 draggingHandle2 = true;
192 else if (state == OSSelected)
194 // Here we test for collision with handles as well! (SOON!) [I think it works...NOPE]
197 if (v1.Magnitude() < 2.0) // Handle #1
198 else if (v2.Magnitude() < 2.0) // Handle #2
203 // state = OSInactive;
210 // If we got here, we clicked on nothing, so set the object to inactive.
211 // (Once we can read key modifiers, we can override this to allow multiple selection.)
216 /*virtual*/ void Line::PointerMoved(Vector point)
218 // Hit test tells us what we hit (if anything) through boolean variables. It
219 // also tells us whether or not the state changed.
220 needUpdate = HitTest(point);
222 objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
224 if (objectWasDragged)
226 Vector delta = point - oldPoint;
228 if (draggingHandle1 || draggingLine)
231 if (draggingHandle2 || draggingLine)
239 We can't count on any coupling between the dimension object and us, so how do we do this???
240 Also, there may be more than one Dimension object connected to a single endpoint!
243 - Keep track of the state of the connected dimension
244 - Pass the Dimension the point that's being changed and the delta
247 - Pass the point in a notification function (how?)
248 - Pass the point as a reference to the class instance object (&endpoint). This way, the line
249 doesn't have to care about keeping track of Dimensions connected to it. But still have to
250 care about other connected entities (other Lines, Circles, Arcs, Splines, Texts, etc). I
251 think I'd be OK with this.
252 Since the Dimension has a pointer to our object, all we have to do is update our coordinates
253 and the Dimension object will adjust itself on the next repaint. Problem solved, and we don't
254 have to know anything about how many Dimensions are connected to us, or where! \o/
255 The question then becomes, how do we do this kind of coupling???
257 We need to know about connected entities so that we can have them either move in expected ways
258 or constrain the movement of this Line object. This is how we will be a cut above all other CAD
259 software currently out there: the GUI will try to do the right thing, most of the time. :-)
263 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
264 Vector point1 = (draggingHandle1 ? endpoint : position);
265 Vector point2 = (draggingHandle1 ? position : endpoint);
268 Vector current(point2, point1);
269 Vector v = current.Unit() * length;
270 Vector v2 = point1 + v;
273 if (!Object::fixedLength)
277 if (Object::fixedAngle)
279 // Here we calculate the component of the current vector along the fixed angle.
280 // A_compB = (A . Bu) * Bu
281 double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
283 Actually, this isn't quite right. What we want to do is look for the intersection along either
284 the horizontal line or vertical line that intersects from the current mouse position.
288 position = endpoint + (angle * magnitudeAlongB);
291 endpoint = position + (angle * magnitudeAlongB);
296 //If we tell the dimension to flip sides, this is no longer a valid
297 //assumption. !!! FIX !!!
298 //Ideally, we should just send the point that's changing to the Dimension object
299 //and have it figure out which point needs to move... Or is it???
300 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
301 // so that we don't have to wait until the dragging is done to correct the position of the
302 // point in question, but we'd need another variable tho.
305 dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
308 dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
313 /*virtual*/ void Line::PointerReleased(void)
315 if (draggingHandle1 || draggingHandle2)
317 // Set the length (in case the global state was set to fixed (or not))
318 if (Object::fixedLength)
320 if (draggingHandle1) // startpoint
322 Vector v = Vector(position - endpoint).Unit() * length;
323 position = endpoint + v;
327 // Vector v1 = endpoint - position;
328 Vector v = Vector(endpoint - position).Unit() * length;
329 endpoint = position + v;
334 // Otherwise, we calculate the new length, just in case on the next move
335 // it turns out to have a fixed length. :-)
336 length = Vector(endpoint - position).Magnitude();
339 if (!Object::fixedAngle)
341 // Calculate the new angle, just in case on the next move it turns out to
343 angle = Vector(endpoint - position).Unit();
347 draggingLine = false;
348 draggingHandle1 = false;
349 draggingHandle2 = false;
351 // hitPoint1 = hitPoint2 = hitLine = false;
353 // Here we check for just a click: If object was clicked and dragged, then
354 // revert to the old state (OSInactive). Otherwise, keep the new state that
356 /*Maybe it would be better to just check for "object was dragged" state and not have to worry
357 about keeping track of old states...
359 if (objectWasDragged)
363 // Check to see if the point passed in coincides with any we have. If so, return a
364 // pointer to it; otherwise, return NULL.
365 /*virtual*/ Vector * Line::GetPointAt(Vector v)
369 else if (v == endpoint)
376 void Line::SetDimensionOnPoint1(Dimension * dimension)
378 dimPoint1 = dimension;
381 dimension->SetPoint1(position);
384 void Line::SetDimensionOnPoint2(Dimension * dimension)
386 dimPoint2 = dimension;
389 dimension->SetPoint2(endpoint);
392 void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
394 // If they don't pass one in, create it for the caller.
395 if (dimension == NULL)
397 dimension = new Dimension(&position, &endpoint, DTLinear, this);
400 parent->Add(dimension);
403 attachedDimension = dimension;
405 // After we set the points here, we don't have to care about them anymore.
408 dimension->SetPoint1(&position);
409 dimension->SetPoint2(&endpoint);
414 bool Line::HitTest(Point point)
418 hitPoint1 = hitPoint2 = hitLine = false;
419 Vector lineSegment = endpoint - position;
420 Vector v1 = point - position;
421 Vector v2 = point - endpoint;
422 double parameterizedPoint = lineSegment.Dot(v1) / lineSegment.Magnitude(), distance;
424 // Geometric interpretation:
425 // The parameterized point on the vector lineSegment is where the perpendicular
426 // intersects lineSegment. If pp < 0, then the perpendicular lies beyond the 1st
427 // endpoint. If pp > length of ls, then the perpendicular lies beyond the 2nd endpoint.
429 if (parameterizedPoint < 0.0)
430 distance = v1.Magnitude();
431 else if (parameterizedPoint > lineSegment.Magnitude())
432 distance = v2.Magnitude();
434 // distance = ?Det?(ls, v1) / |ls|
435 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y) / lineSegment.Magnitude());
437 // Geometric interpretation of the above:
438 // If the segment endpoints are s and e, and the point is p, then the test
439 // for the perpendicular intercepting the segment is equivalent to insisting
440 // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
441 // Perpendicular distance from the point to the segment is computed by first
442 // computing the area of the triangle the three points form, then dividing by
443 // the length of the segment. Distances are done just by the Pythagorean
444 // theorem. Twice the area of the triangle formed by three points is the
445 // determinant of the following matrix:
447 // sx sy 1 0 0 1 0 0 0
448 // ex ey 1 ==> ex ey 1 ==> ex ey 0
449 // px py 1 px py 1 px py 0
451 // By translating the start point to the origin, and subtracting row 1 from
452 // all other rows, we end up with the matrix on the right which greatly
453 // simplifies the calculation of the determinant.
455 //How do we determine distance here? Especially if zoomed in or out???
456 //#warning "!!! Distances tested for may not be valid if zoomed in or out !!!"
458 if ((v1.Magnitude() * Painter::zoom) < 8.0)
460 else if ((v2.Magnitude() * Painter::zoom) < 8.0)
462 else if ((distance * Painter::zoom) < 5.0)
465 return StateChanged();
468 void Line::SaveState(void)
470 oldHitPoint1 = hitPoint1;
471 oldHitPoint2 = hitPoint2;
472 oldHitLine = hitLine;
475 bool Line::StateChanged(void)
477 if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
484 Intersection of two lines:
486 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
488 When they intersect, we can set the equations equal to one another:
490 i + j + t (3i - j) = -i + s (j)
492 Equating coefficients:
493 1 + 3t = -1 and 1 - t = s
494 So t = -2/3 and s = 5/3
496 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 .
499 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
500 and v2(p2x, p2y), v3(p3x, p3y) for l2.
502 d1 = v1 - v0, d2 = v3 - v2
504 Our parametric equations for the line then are:
509 Set r1 = r2, thus we have:
511 v0 + t(d1) = v2 + s(d2)
513 Taking coefficients, we have:
515 p0x + t(d1x) = p2x + s(d2x)
516 p0y + t(d1y) = p2y + s(d2y)
520 t(d1x) - s(d2x) = p2x - p0x
521 t(d1y) - s(d2y) = p2y - p0y
523 Determinant D is ad - bc where the matrix looks like:
528 so D = (d1x)(d2y) - (d2x)(d1y)
529 if D = 0, the lines are parallel.
530 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
531 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
534 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
536 ---------------------------------------------------------------------------------------------------
538 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:
544 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
551 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
552 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
555 crossing vector = v1 * ratio;
559 -----------------------------------
561 So... to code this, let's say we have two Lines: l1 & l2.
563 Vector v1 = l1.endpoint - l1.position;
564 Vector v2 = l2.endpoint - l2.position;
567 Vector normal1(-v1.y, v1.x);
568 Vector normal3(-v3.y, v3.x);
570 double dotProduct1 = v2.Dot(normal1);
571 double dotProduct2 = v2.Dot(normal3);
573 if (dotProduct2 == 0)
574 return ParallelLines;
577 // I think we'd still have to add the intersection to the position point to get the intersection...
578 Point intersection = v1 * (dotProduct1 / dotProduct2);