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"
25 #include "mathconstants.h"
29 Line::Line(Vector p1, Vector p2, Object * p/*= NULL*/): Object(p1, p),
30 /*type(OTLine),*/ endpoint(p2),
31 draggingLine(false), draggingHandle1(false), draggingHandle2(false), //needUpdate(false),
32 length(Vector::Magnitude(p2, p1)), angle(Vector(endpoint - position).Unit()),
33 hitPoint1(false), hitPoint2(false), hitLine(false)
41 // Taking care of connections should be done by the Container, as we don't know
42 // anything about any other object connected to this one.
44 // If there are any attached Dimensions, we must set the attachment points
45 // to NULL since they will no longer be valid.
46 if (attachedDimension)
48 attachedDimension->SetPoint1(NULL);
49 attachedDimension->SetPoint2(NULL);
51 // IT WOULD BE NICE to have any object points attached to this line automagically
52 // connect to this dimension object at this point, instead of just becoming
55 //actually not true, we know the object pointer and parameter!
56 //actuall, the Object base class does this for us...!
58 std::vector<Connection>::iterator i;
60 for(i=connected.begin(); i!=connected.end(); i++)
62 (*i).object->Disconnect(this, (*i).t);
68 /*virtual*/ void Line::Draw(Painter * painter)
70 painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
72 if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
73 painter->DrawHandle(position);
75 if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
76 painter->DrawHandle(endpoint);
78 if ((state == OSInactive) && !hitLine)
79 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
81 if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
83 Vector point1 = (draggingHandle1 ? endpoint : position);
84 Vector point2 = (draggingHandle1 ? position : endpoint);
86 Vector current(point2 - point1);
87 Vector v = current.Unit() * length;
88 Vector v2 = point1 + v;
89 painter->DrawLine(point1, v2);
91 if (current.Magnitude() > length)
93 painter->SetPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
94 painter->DrawLine(v2, point2);
98 painter->DrawLine(position, endpoint);
100 // If we're dragging an endpoint, draw an information panel showing both
101 // the length and angle being set.
102 if (draggingHandle1 || draggingHandle2)
104 double absAngle = (Vector(endpoint - position).Angle()) * RADIANS_TO_DEGREES;
105 double absLength = Vector(position - endpoint).Magnitude();
107 QString text = QObject::tr("Length: %1 in.\n") + QChar(0x2221) + QObject::tr(": %2");
108 text = text.arg(absLength).arg(absAngle);
109 painter->DrawInformativeText(text);
113 /*virtual*/ Vector Line::Center(void)
115 // Technically, this is the midpoint but who are we to quibble? :-)
116 Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
120 /*virtual*/ bool Line::Collided(Vector point)
123 what we can do here is set ignoreClicks to true to keep other objects that are
124 selected from deselecting themselves. Will that fuck up something else? Not sure
126 Actually, this is done here to keep tools from selecting stuff inadvertantly...
128 // We can assume this, since this is a mouse down event here.
129 objectWasDragged = false;
130 bool hit = HitTest(point);
132 // Someone told us to fuck off, so we'll fuck off. :-)
136 // Now that we've done our hit testing on the non-snapped point, snap it if
139 point = SnapPointToGrid(point);
141 if (snapPointIsValid)
144 // this is shite. this should be checked for in the Container, not here!
145 #warning "!!! This should be checked for in Container, not here !!!"
146 // If we're part of a non-top-level container, send this signal to it
147 if (parent->type == OTContainer && !((Container *)parent)->isTopLevelContainer
148 && (hitLine || hitPoint1 || hitPoint2))
150 parent->state = OSSelected;
154 if (state == OSInactive)
156 //How to translate this into pixels from Document space???
157 //Maybe we need to pass a scaling factor in here from the caller? That would
158 //make sense, as the caller knows about the zoom factor and all that good kinda
160 //I think what's needed is an Object class variable/method that can be changed
161 //by the TLC and called in derived classes to properly scale the location to
162 //the current zoom level. That *should* work.
164 // ALSO: Need to code a global (read: Object class) variable that tells use
165 // whether a modifier key was pressed in addition to the mouse click, so
166 // we can do stuff like, say, hold down CTRL and be able to do multiple
167 // selecting of objects (in that case, we would keep the Object state
174 draggingHandle1 = true;
182 draggingHandle2 = true;
194 else if (state == OSSelected)
199 // state = OSInactive;
203 // Toggle selected state if CTRL held
204 if (qApp->keyboardModifiers() == Qt::ControlModifier)
211 // If CTRL is held, then we bypass the "turn off" code. Still didn't hit
212 // *this* object though. :-)
213 if (qApp->keyboardModifiers() == Qt::ControlModifier)
216 // If we got here, we clicked on nothing, so set the object to inactive.
217 // (Once we can read key modifiers, we can override this to allow multiple selection.)
223 /*virtual*/ bool Line::PointerMoved(Vector point)
225 if (selectionInProgress)
227 // Check for whether or not the rect contains this line
228 if (selection.contains(position.x, position.y)
229 && selection.contains(endpoint.x, endpoint.y))
237 // Hit test tells us what we hit (if anything) through boolean variables.
239 bool hovered = HitTest(point);
240 needUpdate = HitStateChanged();
243 point = SnapPointToGrid(point);
245 if (snapPointIsValid)
248 objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
250 if (objectWasDragged)
252 Vector delta = point - oldPoint;
254 if (draggingHandle1 || draggingLine)
257 if (draggingHandle2 || draggingLine)
263 //doesn't work QMainWindow::statusBar()->setText("You are manipulating a line");
265 // Tell connected objects to move themselves...
268 std::vector<Connection>::iterator i;
270 for(i=connected.begin(); i!=connected.end(); i++)
272 if ((*i).object->type == OTLine)
273 ((Line *)((*i).object))->MovePointAtParameter((*i).t, delta);
274 else if ((*i).object->type == OTDimension)
275 ((Dimension *)((*i).object))->MovePointAtParameter((*i).t, delta);
281 We can't count on any coupling between the dimension object and us, so how do we do this???
282 Also, there may be more than one Dimension object connected to a single endpoint!
285 - Keep track of the state of the connected dimension
286 - Pass the Dimension the point that's being changed and the delta
289 - Pass the point in a notification function (how?)
290 - Pass the point as a reference to the class instance object (&endpoint). This
291 way, the line doesn't have to care about keeping track of Dimensions
292 connected to it. But still have to care about other connected entities
293 (other Lines, Circles, Arcs, Splines, Texts, etc). I think I'd be OK with
294 this. Since the Dimension has a pointer to our object, all we have to do is
295 update our coordinates and the Dimension object will adjust itself on the
296 next repaint. Problem solved, and we don't have to know anything about how
297 many Dimensions are connected to us, or where! \o/
298 The question then becomes, how do we do this kind of coupling???
300 We need to know about connected entities so that we can have them either move
301 in expected ways or constrain the movement of this Line object. This is how we
302 will be a cut above all other CAD software currently out there: the GUI will
303 try to do the right thing, most of the time. :-)
307 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
308 Vector point1 = (draggingHandle1 ? endpoint : position);
309 Vector point2 = (draggingHandle1 ? position : endpoint);
311 if (Object::fixedAngle)
313 // Here we calculate the component of the current vector along the fixed angle.
314 // A_compB = (A . Bu) * Bu
315 double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
317 Actually, this isn't quite right. What we want to do is look for the intersection along either
318 the horizontal line or vertical line that intersects from the current mouse position.
322 position = endpoint + (angle * magnitudeAlongB);
325 endpoint = position + (angle * magnitudeAlongB);
330 //If we tell the dimension to flip sides, this is no longer a valid
331 //assumption. !!! FIX !!!
332 //Ideally, we should just send the point that's changing to the Dimension object
333 //and have it figure out which point needs to move... Or is it???
334 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
335 // so that we don't have to wait until the dragging is done to correct the position of the
336 // point in question, but we'd need another variable tho.
339 dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
342 dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
350 /*virtual*/ void Line::PointerReleased(void)
352 if (draggingHandle1 || draggingHandle2)
354 // Set the length (in case the global state was set to fixed (or not))
355 if (Object::fixedLength)
357 if (draggingHandle1) // startpoint
359 Vector v = Vector(position - endpoint).Unit() * length;
360 position = endpoint + v;
364 Vector v = Vector(endpoint - position).Unit() * length;
365 endpoint = position + v;
370 // Otherwise, we calculate the new length, just in case on the next
371 // move it turns out to have a fixed length. :-)
372 length = Vector(endpoint - position).Magnitude();
375 if (!Object::fixedAngle)
377 // Calculate the new angle, just in case on the next move it turns
378 // out to be fixed. :-)
379 angle = Vector(endpoint - position).Unit();
383 draggingLine = false;
384 draggingHandle1 = false;
385 draggingHandle2 = false;
387 if (objectWasDragged)
392 /*virtual*/ bool Line::HitTest(Point point)
394 hitPoint1 = hitPoint2 = hitLine = false;
395 Vector lineSegment = endpoint - position;
396 Vector v1 = point - position;
397 Vector v2 = point - endpoint;
398 Vector v3 = point - Center(); // Midpoint, for snapping...
399 double t = Geometry::ParameterOfLineAndPoint(position, endpoint, point);
402 // Geometric interpretation of "distance = ?Det?(ls, v1) / |ls|":
403 // If the segment endpoints are s and e, and the point is p, then the test
404 // for the perpendicular intercepting the segment is equivalent to insisting
405 // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
406 // Perpendicular distance from the point to the segment is computed by first
407 // computing the area of the triangle the three points form, then dividing by
408 // the length of the segment. Distances are done just by the Pythagorean
409 // theorem. Twice the area of the triangle formed by three points is the
410 // determinant of the following matrix:
412 // sx sy 1 0 0 1 0 0 0
413 // ex ey 1 ==> ex ey 1 ==> ex ey 0
414 // px py 1 px py 1 px py 0
416 // By translating the start point to the origin, and subtracting row 1 from
417 // all other rows, we end up with the matrix on the right which greatly
418 // simplifies the calculation of the determinant.
421 distance = v1.Magnitude();
423 distance = v2.Magnitude();
425 // distance = ?Det?(ls, v1) / |ls|
426 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y)
427 / lineSegment.Magnitude());
429 if ((v1.Magnitude() * Painter::zoom) < 8.0)
432 snapPoint = position;
433 snapPointIsValid = true;
435 else if ((v2.Magnitude() * Painter::zoom) < 8.0)
438 snapPoint = endpoint;
439 snapPointIsValid = true;
441 else if ((distance * Painter::zoom) < 5.0)
444 // Not a manipulation point, but a snapping point:
445 if ((v3.Magnitude() * Painter::zoom) < 8.0)
447 snapPoint = Center();
448 snapPointIsValid = true;
451 return (hitPoint1 || hitPoint2 || hitLine ? true : false);
455 // Check to see if the point passed in coincides with any we have. If so, return a
456 // pointer to it; otherwise, return NULL.
457 /*virtual*/ Vector * Line::GetPointAt(Vector v)
461 else if (v == endpoint)
468 /*virtual*/ void Line::Enumerate(FILE * file)
470 fprintf(file, "LINE %i (%lf,%lf) (%lf,%lf)\n", layer, position.x, position.y, endpoint.x, endpoint.y);
474 /*virtual*/ Object * Line::Copy(void)
476 #warning "!!! This doesn't take care of attached Dimensions !!!"
478 This is a real problem. While having a pointer in the Dimension to this line's points
479 is fast & easy, it creates a huge problem when trying to replicate an object like this.
481 Maybe a way to fix that then, is to have reference numbers instead of pointers. That
482 way, if you copy them, ... you might still have problems. Because you can't be sure if
483 a copy will be persistant or not, you then *definitely* do not want them to have the
484 same reference number.
486 return new Line(position, endpoint, parent);
490 /*virtual*/ Vector Line::GetPointAtParameter(double parameter)
492 // Is there any real reason to clamp this to the endpoints?
493 // (hey, whaddya know? this was masking a bug!)
497 else if (parameter >= 1.0)
501 // The parameter is a percentage of the length of the vector, so all we
502 // have to do is scale the vector by it to find the point.
503 return position + (Vector(position, endpoint) * parameter);
507 /*virtual*/ void Line::MovePointAtParameter(double parameter, Vector v)
511 else if (parameter == 1.0)
514 {} // Not sure how to handle this case :-P
518 /*virtual*/ QRectF Line::Extents(void)
520 QRectF rect(QPointF(position.x, position.y), QPointF(endpoint.x, endpoint.y));
521 return rect.normalized();
525 /*virtual*/ void Line::Translate(Vector amount)
532 /*virtual*/ void Line::Rotate(Point point, double angle)
534 Point l1 = Geometry::RotatePointAroundPoint(position, point, angle);
535 Point l2 = Geometry::RotatePointAroundPoint(endpoint, point, angle);
541 /*virtual*/ void Line::Scale(Point point, double amount)
546 /*virtual*/ void Line::Mirror(Point p1, Point p2)
548 Point l1 = Geometry::MirrorPointAroundLine(position, p1, p2);
549 Point l2 = Geometry::MirrorPointAroundLine(endpoint, p1, p2);
555 /*virtual*/ void Line::Save(void)
558 oldEndpoint = endpoint;
562 /*virtual*/ void Line::Restore(void)
565 endpoint = oldEndpoint;
569 void Line::SetDimensionOnLine(Dimension * dimension/*= NULL*/)
571 // If they don't pass one in, create it for the caller.
572 // But ONLY if this line has a parent container!
573 // This is really bad to do here, it should be done in the parent container, always!
574 #warning "!!! Parent container should be creating Dimension object !!!"
575 if ((dimension == NULL) && (parent != NULL))
577 //printf("Line::SetDimensionOnLine(): Creating new dimension...\n");
578 dimension = new Dimension(position, endpoint, DTLinear, parent);
579 // dimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
581 // THIS IS SERIOUS!!! WITHOUT A PARENT, THIS OBJECT IS IN LIMBO!!!
584 //printf("Line::SetDimensionOnLine(): Adding to parent...\n");
585 parent->Add(dimension);
589 dimension->Connect(this, 0);
590 dimension->Connect(this, 1.0);
592 // Make sure the Dimension is connected to us...
593 Connect(dimension, 0);
594 Connect(dimension, 1.0);
596 dimension->position = position;
597 dimension->endpoint = endpoint;
601 Object * Line::FindAttachedDimension(void)
603 // Is there anything connected to this line? If not, return NULL
604 if (connected.size() < 2)
607 // Otherwise, we have to search our objects to see if there's a likely
608 // candidate. In this case, we're looking for a pointer to the same object
609 // with a parameter of 0 and 1 respectively. This is O((n^2)/2).
610 for(uint i=0; i<connected.size(); i++)
612 for(uint j=i+1; j<connected.size(); j++)
614 //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);
615 if ((connected[i].object == connected[j].object)
616 && ((connected[i].t == 0 && connected[j].t == 1.0)
617 || (connected[i].t == 1.0 && connected[j].t == 0)))
618 return connected[i].object;
622 // Didn't find anything, so return NULL
627 void Line::SaveHitState(void)
629 oldHitPoint1 = hitPoint1;
630 oldHitPoint2 = hitPoint2;
631 oldHitLine = hitLine;
635 bool Line::HitStateChanged(void)
637 if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
645 Intersection of two lines:
647 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
649 When they intersect, we can set the equations equal to one another:
651 i + j + t (3i - j) = -i + s (j)
653 Equating coefficients:
654 1 + 3t = -1 and 1 - t = s
655 So t = -2/3 and s = 5/3
657 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 .
660 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
661 and v2(p2x, p2y), v3(p3x, p3y) for l2.
663 d1 = v1 - v0, d2 = v3 - v2
665 Our parametric equations for the line then are:
670 Set r1 = r2, thus we have:
672 v0 + t(d1) = v2 + s(d2)
674 Taking coefficients, we have:
676 p0x + t(d1x) = p2x + s(d2x)
677 p0y + t(d1y) = p2y + s(d2y)
681 t(d1x) - s(d2x) = p2x - p0x
682 t(d1y) - s(d2y) = p2y - p0y
684 Determinant D is ad - bc where the matrix looks like:
689 so D = (d1x)(d2y) - (d2x)(d1y)
690 if D = 0, the lines are parallel.
691 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
692 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
695 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
697 ---------------------------------------------------------------------------------------------------
699 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:
705 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
712 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
713 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
716 crossing vector = v1 * ratio;
720 -----------------------------------
722 So... to code this, let's say we have two Lines: l1 & l2.
724 Vector v1 = l1.endpoint - l1.position;
725 Vector v2 = l2.endpoint - l2.position;
728 Vector normal1(-v1.y, v1.x);
729 Vector normal3(-v3.y, v3.x);
731 double dotProduct1 = v2.Dot(normal1);
732 double dotProduct2 = v2.Dot(normal3);
734 if (dotProduct2 == 0)
735 return ParallelLines;
738 // I think we'd still have to add the intersection to the position point to get the intersection...
739 Point intersection = v1 * (dotProduct1 / dotProduct2);