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)
36 // Taking care of connections should be done by the Container, as we don't know
37 // anything about any other object connected to this one.
39 // If there are any attached Dimensions, we must set the attachment points
40 // to NULL since they will no longer be valid.
41 if (attachedDimension)
43 attachedDimension->SetPoint1(NULL);
44 attachedDimension->SetPoint2(NULL);
46 // IT WOULD BE NICE to have any object points attached to this line automagically
47 // connect to this dimension object at this point, instead of just becoming
53 /*virtual*/ void Line::Draw(Painter * painter)
55 painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
57 if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
58 painter->DrawHandle(position);
60 if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
61 painter->DrawHandle(endpoint);
63 if ((state == OSInactive) && !hitLine)
64 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
66 if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
68 Vector point1 = (draggingHandle1 ? endpoint : position);
69 Vector point2 = (draggingHandle1 ? position : endpoint);
71 Vector current(point2 - point1);
72 Vector v = current.Unit() * length;
73 Vector v2 = point1 + v;
74 // painter->DrawLine((int)point1.x, (int)point1.y, (int)v2.x, (int)v2.y);
75 painter->DrawLine(point1, v2);
77 if (current.Magnitude() > length)
79 painter->SetPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
80 // painter->DrawLine((int)v2.x, (int)v2.y, (int)point2.x, (int)point2.y);
81 painter->DrawLine(v2, point2);
84 // Problem: when drawing at large zoom levels, this throws away precision thus
85 // causing the line to rendered too short. !!! FIX !!! [DONE]
87 // painter->DrawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
88 painter->DrawLine(position, endpoint);
91 /*virtual*/ Vector Line::Center(void)
93 // Technically, this is the midpoint but who are we to quibble? :-)
94 Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
98 /*virtual*/ bool Line::Collided(Vector point)
100 // We can assume this, since this is a mouse down event here.
101 objectWasDragged = false;
105 There's a small problem here with the implementation: You can have a dimension tied
106 to only one point while at the same time you can have a dimension sitting on this line.
107 Since there's only *one* dimPoint for each point, this can be problematic...
109 We solve this by allowing only *one* Dimension object to be attached to the Line,
110 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
112 Problem still arises when we delete this object; The attached Dimension object will
113 then have bad pointers! What it *should* do is delete the object if and only if this
114 line is not attached to any other object. If it is, then one of those attachment
115 points should be sent to the dimension object (done for position & endpoint).
117 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
120 Also: It would be nice to have a preview of the dimension being drawn, with a modifier
121 key to make it draw/show on the other side...
123 TODO: Make Dimension preview with modifier keys for showing on other side
125 // Is the dimension tool active? Let's use it:
128 // User clicked on the line itself (endpoint checks should preceed this one):
129 // (Priorities are taken care of in HitTest()...)
133 if (attachedDimension == NULL)
135 // How to get this object into the top level container???
137 The real question is do we care. I think so, because if this isn't in the top
138 level container, it won't get drawn...
139 But we can fix that by making this object call any attached object's (like
140 a dimension only) Draw() function... :-/
142 attachedDimension = new Dimension(&position, &endpoint, DTLinear, this);
145 parent->Add(attachedDimension);
149 // If there's one already there, tell it to flip sides...
150 attachedDimension->FlipSides();
153 // New approach here: We look for connected objects.
154 Object * attachedDimension = FindAttachedDimension();
156 if (attachedDimension)
158 // If there's an attached Dimension, tell it to switch sides...
159 ((Dimension *)attachedDimension)->FlipSides();
163 // Otherwise, we make a new one and attach it here.
164 attachedDimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
165 connected.push_back(Connection(attachedDimension, 0));
166 connected.push_back(Connection(attachedDimension, 1.0));
169 parent->Add(attachedDimension);
178 if (state == OSInactive)
180 //printf("Line: pp = %lf, length = %lf, distance = %lf\n", parameterizedPoint, lineSegment.Magnitude(), distance);
181 //printf(" v1.Magnitude = %lf, v2.Magnitude = %lf\n", v1.Magnitude(), v2.Magnitude());
182 //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);
184 //How to translate this into pixels from Document space???
185 //Maybe we need to pass a scaling factor in here from the caller? That would make sense, as
186 //the caller knows about the zoom factor and all that good kinda crap
187 //I think what's needed is an Object class variable/method that can be changed by the TLC and
188 //called in derived classes to properly scale the location to the current zoom level. That *should* work.
190 // ALSO: Need to code a global (read: Object class) variable that tells use whether a modifier
191 // key was pressed in addition to the mouse click, so we can do stuff like, say, hold
192 // down CTRL and be able to do multiple selecting of objects (in that case, we would
193 // keep the Object state from changing).
198 oldPoint = position; //maybe "position"?
199 draggingHandle1 = true;
206 oldPoint = endpoint; //maybe "position"?
207 draggingHandle2 = true;
219 else if (state == OSSelected)
221 // Here we test for collision with handles as well! (SOON!) [I think it works...NOPE]
224 if (v1.Magnitude() < 2.0) // Handle #1
225 else if (v2.Magnitude() < 2.0) // Handle #2
230 // state = OSInactive;
237 // If we got here, we clicked on nothing, so set the object to inactive.
238 // (Once we can read key modifiers, we can override this to allow multiple selection.)
244 /*virtual*/ void Line::PointerMoved(Vector point)
246 // Hit test tells us what we hit (if anything) through boolean variables. It
247 // also tells us whether or not the state changed.
248 needUpdate = HitTest(point);
250 objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
252 if (objectWasDragged)
254 Vector delta = point - oldPoint;
256 if (draggingHandle1 || draggingLine)
259 if (draggingHandle2 || draggingLine)
267 We can't count on any coupling between the dimension object and us, so how do we do this???
268 Also, there may be more than one Dimension object connected to a single endpoint!
271 - Keep track of the state of the connected dimension
272 - Pass the Dimension the point that's being changed and the delta
275 - Pass the point in a notification function (how?)
276 - Pass the point as a reference to the class instance object (&endpoint). This way, the line
277 doesn't have to care about keeping track of Dimensions connected to it. But still have to
278 care about other connected entities (other Lines, Circles, Arcs, Splines, Texts, etc). I
279 think I'd be OK with this.
280 Since the Dimension has a pointer to our object, all we have to do is update our coordinates
281 and the Dimension object will adjust itself on the next repaint. Problem solved, and we don't
282 have to know anything about how many Dimensions are connected to us, or where! \o/
283 The question then becomes, how do we do this kind of coupling???
285 We need to know about connected entities so that we can have them either move in expected ways
286 or constrain the movement of this Line object. This is how we will be a cut above all other CAD
287 software currently out there: the GUI will try to do the right thing, most of the time. :-)
291 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
292 Vector point1 = (draggingHandle1 ? endpoint : position);
293 Vector point2 = (draggingHandle1 ? position : endpoint);
296 Vector current(point2, point1);
297 Vector v = current.Unit() * length;
298 Vector v2 = point1 + v;
301 if (!Object::fixedLength)
305 if (Object::fixedAngle)
307 // Here we calculate the component of the current vector along the fixed angle.
308 // A_compB = (A . Bu) * Bu
309 double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
311 Actually, this isn't quite right. What we want to do is look for the intersection along either
312 the horizontal line or vertical line that intersects from the current mouse position.
316 position = endpoint + (angle * magnitudeAlongB);
319 endpoint = position + (angle * magnitudeAlongB);
324 //If we tell the dimension to flip sides, this is no longer a valid
325 //assumption. !!! FIX !!!
326 //Ideally, we should just send the point that's changing to the Dimension object
327 //and have it figure out which point needs to move... Or is it???
328 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
329 // so that we don't have to wait until the dragging is done to correct the position of the
330 // point in question, but we'd need another variable tho.
333 dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
336 dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
342 /*virtual*/ void Line::PointerReleased(void)
344 if (draggingHandle1 || draggingHandle2)
346 // Set the length (in case the global state was set to fixed (or not))
347 if (Object::fixedLength)
349 if (draggingHandle1) // startpoint
351 Vector v = Vector(position - endpoint).Unit() * length;
352 position = endpoint + v;
356 // Vector v1 = endpoint - position;
357 Vector v = Vector(endpoint - position).Unit() * length;
358 endpoint = position + v;
363 // Otherwise, we calculate the new length, just in case on the next move
364 // it turns out to have a fixed length. :-)
365 length = Vector(endpoint - position).Magnitude();
368 if (!Object::fixedAngle)
370 // Calculate the new angle, just in case on the next move it turns out to
372 angle = Vector(endpoint - position).Unit();
376 draggingLine = false;
377 draggingHandle1 = false;
378 draggingHandle2 = false;
380 // hitPoint1 = hitPoint2 = hitLine = false;
382 // Here we check for just a click: If object was clicked and dragged, then
383 // revert to the old state (OSInactive). Otherwise, keep the new state that
385 /*Maybe it would be better to just check for "object was dragged" state and not have to worry
386 about keeping track of old states...
388 if (objectWasDragged)
393 // Check to see if the point passed in coincides with any we have. If so, return a
394 // pointer to it; otherwise, return NULL.
395 /*virtual*/ Vector * Line::GetPointAt(Vector v)
399 else if (v == endpoint)
406 /*virtual*/ void Line::Enumerate(FILE * file)
408 fprintf(file, "LINE (%lf,%lf) (%lf,%lf)\n", position.x, position.y, endpoint.x, endpoint.y);
412 /*virtual*/ Object * Line::Copy(void)
414 #warning "!!! This doesn't take care of attached Dimensions !!!"
416 This is a real problem. While having a pointer in the Dimension to this line's points is fast & easy,
417 it creates a huge problem when trying to replicate an object like this.
419 Maybe a way to fix that then, is to have reference numbers instead of pointers. That way, if you copy
420 them, ... you might still have problems. Because you can't be sure if a copy will be persistant or not,
421 you then *definitely* do not want them to have the same reference number.
423 return new Line(position, endpoint, parent);
427 /*virtual*/ Vector Line::GetPointAtParameter(double parameter)
431 else if (parameter >= 1.0)
434 // Our parameter lies between zero and one, so calculate it!
435 Vector v(endpoint, position);
436 double length = v.Magnitude();
437 // We scale the magnitude of v so that it lies between 0 and 1...
438 // By multiplying the parameter by the magnitude, we obtain the point we
439 // want. No scaling necessary as it's inherent in the approach!
440 double spotOnLength = length * parameter;
442 // To get our point, we use the initial point of the line and add in our
444 Vector result = position + (v * spotOnLength);
449 void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
451 // If they don't pass one in, create it for the caller.
452 if (dimension == NULL)
454 //printf("Line::SetDimensionOnLine(): Creating new dimension...\n");
455 // dimension = new Dimension(position, endpoint, DTLinear, this);
456 dimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
460 //printf("Line::SetDimensionOnLine(): Adding to parent...\n");
461 parent->Add(dimension);
466 dimension->Connect(this, 0);
467 dimension->Connect(this, 1.0);
470 // Make sure the Dimension is connected to us...
471 Connect(dimension, 0);
472 Connect(dimension, 1.0);
476 Object * Line::FindAttachedDimension(void)
478 // Is there anything connected to this line? If not, return NULL
479 if (connected.size() < 2)
482 // Otherwise, we have to search our objects to see if there's a likely
483 // candidate. In this case, we're looking for a pointer to the same object
484 // with a parameter of 0 and 1 respectively. This is O((n^2)/2).
485 for(uint i=0; i<connected.size(); i++)
487 for(uint j=i+1; j<connected.size(); j++)
489 //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);
490 if ((connected[i].object == connected[j].object)
491 && ((connected[i].t == 0 && connected[j].t == 1.0)
492 || (connected[i].t == 1.0 && connected[j].t == 0)))
493 return connected[i].object;
497 // Didn't find anything, so return NULL
502 bool Line::HitTest(Point point)
506 hitPoint1 = hitPoint2 = hitLine = false;
507 Vector lineSegment = endpoint - position;
508 Vector v1 = point - position;
509 Vector v2 = point - endpoint;
510 double parameterizedPoint = lineSegment.Dot(v1) / lineSegment.Magnitude(), distance;
512 // Geometric interpretation:
513 // The parameterized point on the vector lineSegment is where the perpendicular
514 // intersects lineSegment. If pp < 0, then the perpendicular lies beyond the 1st
515 // endpoint. If pp > length of ls, then the perpendicular lies beyond the 2nd endpoint.
517 if (parameterizedPoint < 0.0)
518 distance = v1.Magnitude();
519 else if (parameterizedPoint > lineSegment.Magnitude())
520 distance = v2.Magnitude();
522 // distance = ?Det?(ls, v1) / |ls|
523 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y) / lineSegment.Magnitude());
525 // Geometric interpretation of the above:
526 // If the segment endpoints are s and e, and the point is p, then the test
527 // for the perpendicular intercepting the segment is equivalent to insisting
528 // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
529 // Perpendicular distance from the point to the segment is computed by first
530 // computing the area of the triangle the three points form, then dividing by
531 // the length of the segment. Distances are done just by the Pythagorean
532 // theorem. Twice the area of the triangle formed by three points is the
533 // determinant of the following matrix:
535 // sx sy 1 0 0 1 0 0 0
536 // ex ey 1 ==> ex ey 1 ==> ex ey 0
537 // px py 1 px py 1 px py 0
539 // By translating the start point to the origin, and subtracting row 1 from
540 // all other rows, we end up with the matrix on the right which greatly
541 // simplifies the calculation of the determinant.
543 //How do we determine distance here? Especially if zoomed in or out???
544 //#warning "!!! Distances tested for may not be valid if zoomed in or out !!!"
546 if ((v1.Magnitude() * Painter::zoom) < 8.0)
548 else if ((v2.Magnitude() * Painter::zoom) < 8.0)
550 else if ((distance * Painter::zoom) < 5.0)
553 return StateChanged();
556 void Line::SaveState(void)
558 oldHitPoint1 = hitPoint1;
559 oldHitPoint2 = hitPoint2;
560 oldHitLine = hitLine;
563 bool Line::StateChanged(void)
565 if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
572 Intersection of two lines:
574 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
576 When they intersect, we can set the equations equal to one another:
578 i + j + t (3i - j) = -i + s (j)
580 Equating coefficients:
581 1 + 3t = -1 and 1 - t = s
582 So t = -2/3 and s = 5/3
584 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 .
587 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
588 and v2(p2x, p2y), v3(p3x, p3y) for l2.
590 d1 = v1 - v0, d2 = v3 - v2
592 Our parametric equations for the line then are:
597 Set r1 = r2, thus we have:
599 v0 + t(d1) = v2 + s(d2)
601 Taking coefficients, we have:
603 p0x + t(d1x) = p2x + s(d2x)
604 p0y + t(d1y) = p2y + s(d2y)
608 t(d1x) - s(d2x) = p2x - p0x
609 t(d1y) - s(d2y) = p2y - p0y
611 Determinant D is ad - bc where the matrix looks like:
616 so D = (d1x)(d2y) - (d2x)(d1y)
617 if D = 0, the lines are parallel.
618 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
619 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
622 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
624 ---------------------------------------------------------------------------------------------------
626 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:
632 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
639 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
640 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
643 crossing vector = v1 * ratio;
647 -----------------------------------
649 So... to code this, let's say we have two Lines: l1 & l2.
651 Vector v1 = l1.endpoint - l1.position;
652 Vector v2 = l2.endpoint - l2.position;
655 Vector normal1(-v1.y, v1.x);
656 Vector normal3(-v3.y, v3.x);
658 double dotProduct1 = v2.Dot(normal1);
659 double dotProduct2 = v2.Dot(normal3);
661 if (dotProduct2 == 0)
662 return ParallelLines;
665 // I think we'd still have to add the intersection to the position point to get the intersection...
666 Point intersection = v1 * (dotProduct1 / dotProduct2);