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
58 /*virtual*/ void Line::Draw(Painter * painter)
60 painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
62 if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
63 painter->DrawHandle(position);
65 if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
66 painter->DrawHandle(endpoint);
68 if ((state == OSInactive) && !hitLine)
69 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
71 if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
73 Vector point1 = (draggingHandle1 ? endpoint : position);
74 Vector point2 = (draggingHandle1 ? position : endpoint);
76 Vector current(point2 - point1);
77 Vector v = current.Unit() * length;
78 Vector v2 = point1 + v;
79 painter->DrawLine(point1, v2);
81 if (current.Magnitude() > length)
83 painter->SetPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
84 painter->DrawLine(v2, point2);
88 painter->DrawLine(position, endpoint);
90 // If we're dragging an endpoint, draw an information panel showing both
91 // the length and angle being set.
92 if (draggingHandle1 || draggingHandle2)
94 double absAngle = (Vector(endpoint - position).Angle()) * RADIANS_TO_DEGREES;
95 double absLength = Vector(position - endpoint).Magnitude();
97 QString text = QObject::tr("Length: %1 in.\n") + QChar(0x2221) + QObject::tr(": %2");
98 text = text.arg(absLength).arg(absAngle);
100 QPen pen = QPen(QColor(0x00, 0xFF, 0x00), 1.0, Qt::SolidLine);
101 painter->SetPen(pen);
102 painter->SetBrush(QBrush(QColor(0x40, 0xFF, 0x40, 0x9F)));
103 QRectF textRect(10.0, 10.0, 270.0, 70.0); // x, y, w, h (in Qt coords)
104 painter->DrawRoundedRect(textRect, 7.0, 7.0);
106 textRect.setLeft(textRect.left() + 14);
107 painter->SetFont(*Object::font);
108 pen = QPen(QColor(0x00, 0x5F, 0xDF));
109 painter->SetPen(pen);
110 painter->DrawText(textRect, Qt::AlignVCenter, text);
112 painter->DrawInformativeText(text);
117 /*virtual*/ Vector Line::Center(void)
119 // Technically, this is the midpoint but who are we to quibble? :-)
120 Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
124 /*virtual*/ bool Line::Collided(Vector point)
126 // Someone told us to fuck off, so we'll fuck off. :-)
130 // We can assume this, since this is a mouse down event here.
131 objectWasDragged = false;
134 // Now that we've done our hit testing on the non-snapped point, snap it if
137 point = SnapPointToGrid(point);
139 // this is shite. this should be checked for in the Container, not here!
140 #warning "!!! This should be checked for in Container, not here !!!"
141 // If we're part of a non-top-level container, send this signal to it
142 if (parent->type == OTContainer && !((Container *)parent)->isTopLevelContainer
143 && (hitLine || hitPoint1 || hitPoint2))
145 parent->state = OSSelected;
150 There's a small problem here with the implementation: You can have a dimension tied
151 to only one point while at the same time you can have a dimension sitting on this line.
152 Since there's only *one* dimPoint for each point, this can be problematic...
154 We solve this by allowing only *one* Dimension object to be attached to the Line,
155 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
157 Problem still arises when we delete this object; The attached Dimension object will
158 then have bad pointers! What it *should* do is delete the object if and only if this
159 line is not attached to any other object. If it is, then one of those attachment
160 points should be sent to the dimension object (done for position & endpoint).
162 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
165 Also: It would be nice to have a preview of the dimension being drawn, with a modifier
166 key to make it draw/show on the other side...
168 TODO: Make Dimension preview with modifier keys for showing on other side
170 // Is the dimension tool active? Let's use it:
173 // User clicked on the line itself (endpoint checks should preceed this one):
174 // (Priorities are taken care of in HitTest()...)
178 if (attachedDimension == NULL)
180 // How to get this object into the top level container???
182 The real question is do we care. I think so, because if this isn't in the top
183 level container, it won't get drawn...
184 But we can fix that by making this object call any attached object's (like
185 a dimension only) Draw() function... :-/
187 attachedDimension = new Dimension(&position, &endpoint, DTLinear, this);
190 parent->Add(attachedDimension);
194 // If there's one already there, tell it to flip sides...
195 attachedDimension->FlipSides();
198 // New approach here: We look for connected objects.
199 Object * attachedDimension = FindAttachedDimension();
201 if (attachedDimension)
203 // If there's an attached Dimension, tell it to switch sides...
204 ((Dimension *)attachedDimension)->FlipSides();
208 // Otherwise, we make a new one and attach it here.
209 attachedDimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
210 connected.push_back(Connection(attachedDimension, 0));
211 connected.push_back(Connection(attachedDimension, 1.0));
214 parent->Add(attachedDimension);
222 if (state == OSInactive)
224 //How to translate this into pixels from Document space???
225 //Maybe we need to pass a scaling factor in here from the caller? That would
226 //make sense, as the caller knows about the zoom factor and all that good kinda
228 //I think what's needed is an Object class variable/method that can be changed
229 //by the TLC and called in derived classes to properly scale the location to
230 //the current zoom level. That *should* work.
232 // ALSO: Need to code a global (read: Object class) variable that tells use
233 // whether a modifier key was pressed in addition to the mouse click, so
234 // we can do stuff like, say, hold down CTRL and be able to do multiple
235 // selecting of objects (in that case, we would keep the Object state
242 draggingHandle1 = true;
250 draggingHandle2 = true;
262 else if (state == OSSelected)
267 // state = OSInactive;
271 // Toggle selected state if CTRL held
272 if (qApp->keyboardModifiers() == Qt::ControlModifier)
279 // If CTRL is held, then we bypass the "turn off" code. Still didn't hit
280 // *this* object though. :-)
281 if (qApp->keyboardModifiers() == Qt::ControlModifier)
284 // If we got here, we clicked on nothing, so set the object to inactive.
285 // (Once we can read key modifiers, we can override this to allow multiple selection.)
291 /*virtual*/ void Line::PointerMoved(Vector point)
293 if (selectionInProgress)
295 // Check for whether or not the rect contains this line
297 if (selection.normalized().contains(Extents()))
299 // if (selection.normalized().contains(position.x, position.y)
300 // && selection.normalized().contains(endpoint.x, endpoint.y))
301 if (selection.contains(position.x, position.y)
302 && selection.contains(endpoint.x, endpoint.y))
311 // Hit test tells us what we hit (if anything) through boolean variables. (It
312 // also tells us whether or not the state changed. --not any more)
315 needUpdate = HitStateChanged();
317 objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
319 if (objectWasDragged)
321 Vector delta = point - oldPoint;
323 if (draggingHandle1 || draggingLine)
326 if (draggingHandle2 || draggingLine)
332 //doesn't work QMainWindow::statusBar()->setText("You are manipulating a line");
336 We can't count on any coupling between the dimension object and us, so how do we do this???
337 Also, there may be more than one Dimension object connected to a single endpoint!
340 - Keep track of the state of the connected dimension
341 - Pass the Dimension the point that's being changed and the delta
344 - Pass the point in a notification function (how?)
345 - Pass the point as a reference to the class instance object (&endpoint). This
346 way, the line doesn't have to care about keeping track of Dimensions
347 connected to it. But still have to care about other connected entities
348 (other Lines, Circles, Arcs, Splines, Texts, etc). I think I'd be OK with
349 this. Since the Dimension has a pointer to our object, all we have to do is
350 update our coordinates and the Dimension object will adjust itself on the
351 next repaint. Problem solved, and we don't have to know anything about how
352 many Dimensions are connected to us, or where! \o/
353 The question then becomes, how do we do this kind of coupling???
355 We need to know about connected entities so that we can have them either move
356 in expected ways or constrain the movement of this Line object. This is how we
357 will be a cut above all other CAD software currently out there: the GUI will
358 try to do the right thing, most of the time. :-)
362 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
363 Vector point1 = (draggingHandle1 ? endpoint : position);
364 Vector point2 = (draggingHandle1 ? position : endpoint);
366 if (Object::fixedAngle)
368 // Here we calculate the component of the current vector along the fixed angle.
369 // A_compB = (A . Bu) * Bu
370 double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
372 Actually, this isn't quite right. What we want to do is look for the intersection along either
373 the horizontal line or vertical line that intersects from the current mouse position.
377 position = endpoint + (angle * magnitudeAlongB);
380 endpoint = position + (angle * magnitudeAlongB);
385 //If we tell the dimension to flip sides, this is no longer a valid
386 //assumption. !!! FIX !!!
387 //Ideally, we should just send the point that's changing to the Dimension object
388 //and have it figure out which point needs to move... Or is it???
389 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
390 // so that we don't have to wait until the dragging is done to correct the position of the
391 // point in question, but we'd need another variable tho.
394 dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
397 dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
403 /*virtual*/ void Line::PointerReleased(void)
405 if (draggingHandle1 || draggingHandle2)
407 // Set the length (in case the global state was set to fixed (or not))
408 if (Object::fixedLength)
410 if (draggingHandle1) // startpoint
412 Vector v = Vector(position - endpoint).Unit() * length;
413 position = endpoint + v;
417 Vector v = Vector(endpoint - position).Unit() * length;
418 endpoint = position + v;
423 // Otherwise, we calculate the new length, just in case on the next
424 // move it turns out to have a fixed length. :-)
425 length = Vector(endpoint - position).Magnitude();
428 if (!Object::fixedAngle)
430 // Calculate the new angle, just in case on the next move it turns
431 // out to be fixed. :-)
432 angle = Vector(endpoint - position).Unit();
436 draggingLine = false;
437 draggingHandle1 = false;
438 draggingHandle2 = false;
440 if (objectWasDragged)
445 /*virtual*/ bool Line::HitTest(Point point)
449 hitPoint1 = hitPoint2 = hitLine = false;
450 Vector lineSegment = endpoint - position;
451 Vector v1 = point - position;
452 Vector v2 = point - endpoint;
453 // double t = Vector::Parameter(position, endpoint, point);
454 double t = Geometry::ParameterOfLineAndPoint(position, endpoint, point);
457 // Geometric interpretation:
458 // The parameter "t" on the vector lineSegment is where the normal of
459 // lineSegment coincides with point. If t < 0, the normal lies beyond the
460 // 1st endpoint. If t > 1, then the normal lies beyond the 2nd endpoint. We
461 // only calculate the length of the normal between the point and the
462 // lineSegment when the parameter is between 0 and 1.
464 // Geometric interpretation of "distance = ?Det?(ls, v1) / |ls|":
465 // If the segment endpoints are s and e, and the point is p, then the test
466 // for the perpendicular intercepting the segment is equivalent to insisting
467 // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
468 // Perpendicular distance from the point to the segment is computed by first
469 // computing the area of the triangle the three points form, then dividing by
470 // the length of the segment. Distances are done just by the Pythagorean
471 // theorem. Twice the area of the triangle formed by three points is the
472 // determinant of the following matrix:
474 // sx sy 1 0 0 1 0 0 0
475 // ex ey 1 ==> ex ey 1 ==> ex ey 0
476 // px py 1 px py 1 px py 0
478 // By translating the start point to the origin, and subtracting row 1 from
479 // all other rows, we end up with the matrix on the right which greatly
480 // simplifies the calculation of the determinant.
483 distance = v1.Magnitude();
485 distance = v2.Magnitude();
487 // distance = ?Det?(ls, v1) / |ls|
488 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y)
489 / lineSegment.Magnitude());
491 if ((v1.Magnitude() * Painter::zoom) < 8.0)
493 else if ((v2.Magnitude() * Painter::zoom) < 8.0)
495 else if ((distance * Painter::zoom) < 5.0)
498 return (hitPoint1 || hitPoint2 || hitLine ? true : false);
499 // return HitStateChanged();
503 // Check to see if the point passed in coincides with any we have. If so, return a
504 // pointer to it; otherwise, return NULL.
505 /*virtual*/ Vector * Line::GetPointAt(Vector v)
509 else if (v == endpoint)
516 /*virtual*/ void Line::Enumerate(FILE * file)
518 fprintf(file, "LINE %i (%lf,%lf) (%lf,%lf)\n", layer, position.x, position.y, endpoint.x, endpoint.y);
522 /*virtual*/ Object * Line::Copy(void)
524 #warning "!!! This doesn't take care of attached Dimensions !!!"
526 This is a real problem. While having a pointer in the Dimension to this line's points
527 is fast & easy, it creates a huge problem when trying to replicate an object like this.
529 Maybe a way to fix that then, is to have reference numbers instead of pointers. That
530 way, if you copy them, ... you might still have problems. Because you can't be sure if
531 a copy will be persistant or not, you then *definitely* do not want them to have the
532 same reference number.
534 return new Line(position, endpoint, parent);
538 /*virtual*/ Vector Line::GetPointAtParameter(double parameter)
542 else if (parameter >= 1.0)
545 // Our parameter lies between zero and one, so calculate it!
546 Vector v(endpoint, position);
547 double length = v.Magnitude();
548 // We scale the magnitude of v so that it lies between 0 and 1...
549 // By multiplying the parameter by the magnitude, we obtain the point we
550 // want. No scaling necessary as it's inherent in the approach!
551 double spotOnLength = length * parameter;
553 // To get our point, we use the initial point of the line and add in our
555 Vector result = position + (v * spotOnLength);
560 /*virtual*/ QRectF Line::Extents(void)
562 QRectF rect(QPointF(position.x, position.y), QPointF(endpoint.x, endpoint.y));
563 return rect.normalized();
567 /*virtual*/ void Line::Translate(Vector amount)
574 /*virtual*/ void Line::Rotate(Vector point, double angle)
579 /*virtual*/ void Line::Scale(Vector point, double amount)
584 /*virtual*/ void Line::Mirror(Point p1, Point p2)
586 Point l1 = Geometry::MirrorPointAroundLine(position, p1, p2);
587 Point l2 = Geometry::MirrorPointAroundLine(endpoint, p1, p2);
593 /*virtual*/ void Line::Save(void)
596 oldEndpoint = endpoint;
600 /*virtual*/ void Line::Restore(void)
603 endpoint = oldEndpoint;
607 void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
609 // If they don't pass one in, create it for the caller.
610 if (dimension == NULL)
612 //printf("Line::SetDimensionOnLine(): Creating new dimension...\n");
613 // dimension = new Dimension(position, endpoint, DTLinear, this);
614 dimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
618 //printf("Line::SetDimensionOnLine(): Adding to parent...\n");
619 parent->Add(dimension);
624 dimension->Connect(this, 0);
625 dimension->Connect(this, 1.0);
628 // Make sure the Dimension is connected to us...
629 Connect(dimension, 0);
630 Connect(dimension, 1.0);
634 Object * Line::FindAttachedDimension(void)
636 // Is there anything connected to this line? If not, return NULL
637 if (connected.size() < 2)
640 // Otherwise, we have to search our objects to see if there's a likely
641 // candidate. In this case, we're looking for a pointer to the same object
642 // with a parameter of 0 and 1 respectively. This is O((n^2)/2).
643 for(uint i=0; i<connected.size(); i++)
645 for(uint j=i+1; j<connected.size(); j++)
647 //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);
648 if ((connected[i].object == connected[j].object)
649 && ((connected[i].t == 0 && connected[j].t == 1.0)
650 || (connected[i].t == 1.0 && connected[j].t == 0)))
651 return connected[i].object;
655 // Didn't find anything, so return NULL
660 void Line::SaveHitState(void)
662 oldHitPoint1 = hitPoint1;
663 oldHitPoint2 = hitPoint2;
664 oldHitLine = hitLine;
668 bool Line::HitStateChanged(void)
670 if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
678 Intersection of two lines:
680 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
682 When they intersect, we can set the equations equal to one another:
684 i + j + t (3i - j) = -i + s (j)
686 Equating coefficients:
687 1 + 3t = -1 and 1 - t = s
688 So t = -2/3 and s = 5/3
690 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 .
693 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
694 and v2(p2x, p2y), v3(p3x, p3y) for l2.
696 d1 = v1 - v0, d2 = v3 - v2
698 Our parametric equations for the line then are:
703 Set r1 = r2, thus we have:
705 v0 + t(d1) = v2 + s(d2)
707 Taking coefficients, we have:
709 p0x + t(d1x) = p2x + s(d2x)
710 p0y + t(d1y) = p2y + s(d2y)
714 t(d1x) - s(d2x) = p2x - p0x
715 t(d1y) - s(d2y) = p2y - p0y
717 Determinant D is ad - bc where the matrix looks like:
722 so D = (d1x)(d2y) - (d2x)(d1y)
723 if D = 0, the lines are parallel.
724 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
725 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
728 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
730 ---------------------------------------------------------------------------------------------------
732 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:
738 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
745 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
746 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
749 crossing vector = v1 * ratio;
753 -----------------------------------
755 So... to code this, let's say we have two Lines: l1 & l2.
757 Vector v1 = l1.endpoint - l1.position;
758 Vector v2 = l2.endpoint - l2.position;
761 Vector normal1(-v1.y, v1.x);
762 Vector normal3(-v3.y, v3.x);
764 double dotProduct1 = v2.Dot(normal1);
765 double dotProduct2 = v2.Dot(normal3);
767 if (dotProduct2 == 0)
768 return ParallelLines;
771 // I think we'd still have to add the intersection to the position point to get the intersection...
772 Point intersection = v1 * (dotProduct1 / dotProduct2);