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);
110 QPen pen = QPen(QColor(0x00, 0xFF, 0x00), 1.0, Qt::SolidLine);
111 painter->SetPen(pen);
112 painter->SetBrush(QBrush(QColor(0x40, 0xFF, 0x40, 0x9F)));
113 QRectF textRect(10.0, 10.0, 270.0, 70.0); // x, y, w, h (in Qt coords)
114 painter->DrawRoundedRect(textRect, 7.0, 7.0);
116 textRect.setLeft(textRect.left() + 14);
117 painter->SetFont(*Object::font);
118 pen = QPen(QColor(0x00, 0x5F, 0xDF));
119 painter->SetPen(pen);
120 painter->DrawText(textRect, Qt::AlignVCenter, text);
122 painter->DrawInformativeText(text);
127 /*virtual*/ Vector Line::Center(void)
129 // Technically, this is the midpoint but who are we to quibble? :-)
130 Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
134 /*virtual*/ bool Line::Collided(Vector point)
137 what we can do here is set ignoreClicks to true to keep other objects that are
138 selected from deselecting themselves. Will that fuck up something else? Not sure
141 // Someone told us to fuck off, so we'll fuck off. :-)
145 // We can assume this, since this is a mouse down event here.
146 objectWasDragged = false;
149 // Now that we've done our hit testing on the non-snapped point, snap it if
152 point = SnapPointToGrid(point);
154 // this is shite. this should be checked for in the Container, not here!
155 #warning "!!! This should be checked for in Container, not here !!!"
156 // If we're part of a non-top-level container, send this signal to it
157 if (parent->type == OTContainer && !((Container *)parent)->isTopLevelContainer
158 && (hitLine || hitPoint1 || hitPoint2))
160 parent->state = OSSelected;
165 There's a small problem here with the implementation: You can have a dimension tied
166 to only one point while at the same time you can have a dimension sitting on this line.
167 Since there's only *one* dimPoint for each point, this can be problematic...
169 We solve this by allowing only *one* Dimension object to be attached to the Line,
170 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
172 Problem still arises when we delete this object; The attached Dimension object will
173 then have bad pointers! What it *should* do is delete the object if and only if this
174 line is not attached to any other object. If it is, then one of those attachment
175 points should be sent to the dimension object (done for position & endpoint).
177 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
180 Also: It would be nice to have a preview of the dimension being drawn, with a modifier
181 key to make it draw/show on the other side...
183 TODO: Make Dimension preview with modifier keys for showing on other side
187 N.B.: This no longer works, as the DrawDimension object takes precedence over this code.
188 THIS DOES NOTHING ANYMORE!!!
192 // Is the dimension tool active? Let's use it:
195 // User clicked on the line itself (endpoint checks should preceed this one):
196 // (Priorities are taken care of in HitTest()...)
200 if (attachedDimension == NULL)
202 // How to get this object into the top level container???
204 The real question is do we care. I think so, because if this isn't in the top
205 level container, it won't get drawn...
206 But we can fix that by making this object call any attached object's (like
207 a dimension only) Draw() function... :-/
209 attachedDimension = new Dimension(&position, &endpoint, DTLinear, this);
212 parent->Add(attachedDimension);
216 // If there's one already there, tell it to flip sides...
217 attachedDimension->FlipSides();
220 // New approach here: We look for connected objects.
221 Object * attachedDimension = FindAttachedDimension();
223 if (attachedDimension)
225 // If there's an attached Dimension, tell it to switch sides...
226 ((Dimension *)attachedDimension)->FlipSides();
230 // Otherwise, we make a new one and attach it here.
231 attachedDimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
232 connected.push_back(Connection(attachedDimension, 0));
233 connected.push_back(Connection(attachedDimension, 1.0));
236 parent->Add(attachedDimension);
245 if (state == OSInactive)
247 //How to translate this into pixels from Document space???
248 //Maybe we need to pass a scaling factor in here from the caller? That would
249 //make sense, as the caller knows about the zoom factor and all that good kinda
251 //I think what's needed is an Object class variable/method that can be changed
252 //by the TLC and called in derived classes to properly scale the location to
253 //the current zoom level. That *should* work.
255 // ALSO: Need to code a global (read: Object class) variable that tells use
256 // whether a modifier key was pressed in addition to the mouse click, so
257 // we can do stuff like, say, hold down CTRL and be able to do multiple
258 // selecting of objects (in that case, we would keep the Object state
265 draggingHandle1 = true;
273 draggingHandle2 = true;
285 else if (state == OSSelected)
290 // state = OSInactive;
294 // Toggle selected state if CTRL held
295 if (qApp->keyboardModifiers() == Qt::ControlModifier)
302 // If CTRL is held, then we bypass the "turn off" code. Still didn't hit
303 // *this* object though. :-)
304 if (qApp->keyboardModifiers() == Qt::ControlModifier)
307 // If we got here, we clicked on nothing, so set the object to inactive.
308 // (Once we can read key modifiers, we can override this to allow multiple selection.)
314 /*virtual*/ void Line::PointerMoved(Vector point)
316 if (selectionInProgress)
318 // Check for whether or not the rect contains this line
319 if (selection.contains(position.x, position.y)
320 && selection.contains(endpoint.x, endpoint.y))
328 // Hit test tells us what we hit (if anything) through boolean variables. (It
329 // also tells us whether or not the state changed. --not any more)
332 needUpdate = HitStateChanged();
334 objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
336 if (objectWasDragged)
338 Vector delta = point - oldPoint;
340 if (draggingHandle1 || draggingLine)
343 if (draggingHandle2 || draggingLine)
349 //doesn't work QMainWindow::statusBar()->setText("You are manipulating a line");
351 // Tell connected objects to move themselves...
354 std::vector<Connection>::iterator i;
356 for(i=connected.begin(); i!=connected.end(); i++)
358 if ((*i).object->type == OTLine)
359 ((Line *)((*i).object))->MovePointAtParameter((*i).t, delta);
360 else if ((*i).object->type == OTDimension)
361 ((Dimension *)((*i).object))->MovePointAtParameter((*i).t, delta);
367 We can't count on any coupling between the dimension object and us, so how do we do this???
368 Also, there may be more than one Dimension object connected to a single endpoint!
371 - Keep track of the state of the connected dimension
372 - Pass the Dimension the point that's being changed and the delta
375 - Pass the point in a notification function (how?)
376 - Pass the point as a reference to the class instance object (&endpoint). This
377 way, the line doesn't have to care about keeping track of Dimensions
378 connected to it. But still have to care about other connected entities
379 (other Lines, Circles, Arcs, Splines, Texts, etc). I think I'd be OK with
380 this. Since the Dimension has a pointer to our object, all we have to do is
381 update our coordinates and the Dimension object will adjust itself on the
382 next repaint. Problem solved, and we don't have to know anything about how
383 many Dimensions are connected to us, or where! \o/
384 The question then becomes, how do we do this kind of coupling???
386 We need to know about connected entities so that we can have them either move
387 in expected ways or constrain the movement of this Line object. This is how we
388 will be a cut above all other CAD software currently out there: the GUI will
389 try to do the right thing, most of the time. :-)
393 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
394 Vector point1 = (draggingHandle1 ? endpoint : position);
395 Vector point2 = (draggingHandle1 ? position : endpoint);
397 if (Object::fixedAngle)
399 // Here we calculate the component of the current vector along the fixed angle.
400 // A_compB = (A . Bu) * Bu
401 double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
403 Actually, this isn't quite right. What we want to do is look for the intersection along either
404 the horizontal line or vertical line that intersects from the current mouse position.
408 position = endpoint + (angle * magnitudeAlongB);
411 endpoint = position + (angle * magnitudeAlongB);
416 //If we tell the dimension to flip sides, this is no longer a valid
417 //assumption. !!! FIX !!!
418 //Ideally, we should just send the point that's changing to the Dimension object
419 //and have it figure out which point needs to move... Or is it???
420 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
421 // so that we don't have to wait until the dragging is done to correct the position of the
422 // point in question, but we'd need another variable tho.
425 dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
428 dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
434 /*virtual*/ void Line::PointerReleased(void)
436 if (draggingHandle1 || draggingHandle2)
438 // Set the length (in case the global state was set to fixed (or not))
439 if (Object::fixedLength)
441 if (draggingHandle1) // startpoint
443 Vector v = Vector(position - endpoint).Unit() * length;
444 position = endpoint + v;
448 Vector v = Vector(endpoint - position).Unit() * length;
449 endpoint = position + v;
454 // Otherwise, we calculate the new length, just in case on the next
455 // move it turns out to have a fixed length. :-)
456 length = Vector(endpoint - position).Magnitude();
459 if (!Object::fixedAngle)
461 // Calculate the new angle, just in case on the next move it turns
462 // out to be fixed. :-)
463 angle = Vector(endpoint - position).Unit();
467 draggingLine = false;
468 draggingHandle1 = false;
469 draggingHandle2 = false;
471 if (objectWasDragged)
476 /*virtual*/ bool Line::HitTest(Point point)
478 hitPoint1 = hitPoint2 = hitLine = false;
479 Vector lineSegment = endpoint - position;
480 Vector v1 = point - position;
481 Vector v2 = point - endpoint;
482 double t = Geometry::ParameterOfLineAndPoint(position, endpoint, point);
485 // Geometric interpretation of "distance = ?Det?(ls, v1) / |ls|":
486 // If the segment endpoints are s and e, and the point is p, then the test
487 // for the perpendicular intercepting the segment is equivalent to insisting
488 // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
489 // Perpendicular distance from the point to the segment is computed by first
490 // computing the area of the triangle the three points form, then dividing by
491 // the length of the segment. Distances are done just by the Pythagorean
492 // theorem. Twice the area of the triangle formed by three points is the
493 // determinant of the following matrix:
495 // sx sy 1 0 0 1 0 0 0
496 // ex ey 1 ==> ex ey 1 ==> ex ey 0
497 // px py 1 px py 1 px py 0
499 // By translating the start point to the origin, and subtracting row 1 from
500 // all other rows, we end up with the matrix on the right which greatly
501 // simplifies the calculation of the determinant.
504 distance = v1.Magnitude();
506 distance = v2.Magnitude();
508 // distance = ?Det?(ls, v1) / |ls|
509 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y)
510 / lineSegment.Magnitude());
512 if ((v1.Magnitude() * Painter::zoom) < 8.0)
514 else if ((v2.Magnitude() * Painter::zoom) < 8.0)
516 else if ((distance * Painter::zoom) < 5.0)
519 return (hitPoint1 || hitPoint2 || hitLine ? true : false);
523 // Check to see if the point passed in coincides with any we have. If so, return a
524 // pointer to it; otherwise, return NULL.
525 /*virtual*/ Vector * Line::GetPointAt(Vector v)
529 else if (v == endpoint)
536 /*virtual*/ void Line::Enumerate(FILE * file)
538 fprintf(file, "LINE %i (%lf,%lf) (%lf,%lf)\n", layer, position.x, position.y, endpoint.x, endpoint.y);
542 /*virtual*/ Object * Line::Copy(void)
544 #warning "!!! This doesn't take care of attached Dimensions !!!"
546 This is a real problem. While having a pointer in the Dimension to this line's points
547 is fast & easy, it creates a huge problem when trying to replicate an object like this.
549 Maybe a way to fix that then, is to have reference numbers instead of pointers. That
550 way, if you copy them, ... you might still have problems. Because you can't be sure if
551 a copy will be persistant or not, you then *definitely* do not want them to have the
552 same reference number.
554 return new Line(position, endpoint, parent);
558 /*virtual*/ Vector Line::GetPointAtParameter(double parameter)
560 // Is there any real reason to clamp this to the endpoints?
561 // (hey, whaddya know? this was masking a bug!)
565 else if (parameter >= 1.0)
569 // The parameter is a percentage of the length of the vector, so all we
570 // have to do is scale the vector by it to find the point.
571 return position + (Vector(position, endpoint) * parameter);
575 /*virtual*/ void Line::MovePointAtParameter(double parameter, Vector v)
579 else if (parameter == 1.0)
582 {} // Not sure how to handle this case :-P
586 /*virtual*/ QRectF Line::Extents(void)
588 QRectF rect(QPointF(position.x, position.y), QPointF(endpoint.x, endpoint.y));
589 return rect.normalized();
593 /*virtual*/ void Line::Translate(Vector amount)
600 /*virtual*/ void Line::Rotate(Point point, double angle)
602 Point l1 = Geometry::RotatePointAroundPoint(position, point, angle);
603 Point l2 = Geometry::RotatePointAroundPoint(endpoint, point, angle);
609 /*virtual*/ void Line::Scale(Point point, double amount)
614 /*virtual*/ void Line::Mirror(Point p1, Point p2)
616 Point l1 = Geometry::MirrorPointAroundLine(position, p1, p2);
617 Point l2 = Geometry::MirrorPointAroundLine(endpoint, p1, p2);
623 /*virtual*/ void Line::Save(void)
626 oldEndpoint = endpoint;
630 /*virtual*/ void Line::Restore(void)
633 endpoint = oldEndpoint;
637 void Line::SetDimensionOnLine(Dimension * dimension/*= NULL*/)
639 // If they don't pass one in, create it for the caller.
640 if (dimension == NULL)
642 //printf("Line::SetDimensionOnLine(): Creating new dimension...\n");
643 // dimension = new Dimension(position, endpoint, DTLinear, this);
644 dimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
648 //printf("Line::SetDimensionOnLine(): Adding to parent...\n");
649 parent->Add(dimension);
654 dimension->Connect(this, 0);
655 dimension->Connect(this, 1.0);
658 // Make sure the Dimension is connected to us...
659 Connect(dimension, 0);
660 Connect(dimension, 1.0);
662 dimension->position = position;
663 dimension->endpoint = endpoint;
667 Object * Line::FindAttachedDimension(void)
669 // Is there anything connected to this line? If not, return NULL
670 if (connected.size() < 2)
673 // Otherwise, we have to search our objects to see if there's a likely
674 // candidate. In this case, we're looking for a pointer to the same object
675 // with a parameter of 0 and 1 respectively. This is O((n^2)/2).
676 for(uint i=0; i<connected.size(); i++)
678 for(uint j=i+1; j<connected.size(); j++)
680 //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);
681 if ((connected[i].object == connected[j].object)
682 && ((connected[i].t == 0 && connected[j].t == 1.0)
683 || (connected[i].t == 1.0 && connected[j].t == 0)))
684 return connected[i].object;
688 // Didn't find anything, so return NULL
693 void Line::SaveHitState(void)
695 oldHitPoint1 = hitPoint1;
696 oldHitPoint2 = hitPoint2;
697 oldHitLine = hitLine;
701 bool Line::HitStateChanged(void)
703 if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
711 Intersection of two lines:
713 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
715 When they intersect, we can set the equations equal to one another:
717 i + j + t (3i - j) = -i + s (j)
719 Equating coefficients:
720 1 + 3t = -1 and 1 - t = s
721 So t = -2/3 and s = 5/3
723 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 .
726 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
727 and v2(p2x, p2y), v3(p3x, p3y) for l2.
729 d1 = v1 - v0, d2 = v3 - v2
731 Our parametric equations for the line then are:
736 Set r1 = r2, thus we have:
738 v0 + t(d1) = v2 + s(d2)
740 Taking coefficients, we have:
742 p0x + t(d1x) = p2x + s(d2x)
743 p0y + t(d1y) = p2y + s(d2y)
747 t(d1x) - s(d2x) = p2x - p0x
748 t(d1y) - s(d2y) = p2y - p0y
750 Determinant D is ad - bc where the matrix looks like:
755 so D = (d1x)(d2y) - (d2x)(d1y)
756 if D = 0, the lines are parallel.
757 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
758 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
761 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
763 ---------------------------------------------------------------------------------------------------
765 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:
771 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
778 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
779 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
782 crossing vector = v1 * ratio;
786 -----------------------------------
788 So... to code this, let's say we have two Lines: l1 & l2.
790 Vector v1 = l1.endpoint - l1.position;
791 Vector v2 = l2.endpoint - l2.position;
794 Vector normal1(-v1.y, v1.x);
795 Vector normal3(-v3.y, v3.x);
797 double dotProduct1 = v2.Dot(normal1);
798 double dotProduct2 = v2.Dot(normal3);
800 if (dotProduct2 == 0)
801 return ParallelLines;
804 // I think we'd still have to add the intersection to the position point to get the intersection...
805 Point intersection = v1 * (dotProduct1 / dotProduct2);