]> Shamusworld >> Repos - architektonas/blobdiff - src/line.cpp
Fixed loading code, added "Base Unit" dialog.
[architektonas] / src / line.cpp
index 60e5ab0c1095ff67856aa5b333bf72a0d46afbcf..a3d745aa3da32908a53b3f45c47d6076af07ef24 100644 (file)
@@ -4,7 +4,7 @@
 // (C) 2011 Underground Software
 // See the README and GPLv3 files for licensing and warranty information
 //
-// JLH = James L. Hammons <jlhamm@acm.org>
+// JLH = James Hammons <jlhamm@acm.org>
 //
 // WHO  WHEN        WHAT
 // ---  ----------  ------------------------------------------------------------
 #include "line.h"
 
 #include <QtGui>
+#include "container.h"
 #include "dimension.h"
+#include "mathconstants.h"
+#include "painter.h"
 
-Line::Line(Vector p1, Vector p2, Object * p/*= NULL*/): Object(p1, p), endpoint(p2),
+
+Line::Line(Vector p1, Vector p2, Object * p/*= NULL*/): Object(p1, p),
+       /*type(OTLine),*/ endpoint(p2),
        draggingLine(false), draggingHandle1(false), draggingHandle2(false), //needUpdate(false),
-       length(Vector::Magnitude(p2, p1)), hitPoint1(false), hitPoint2(false), hitLine(false)
+       length(Vector::Magnitude(p2, p1)), angle(Vector(endpoint - position).Unit()),
+       hitPoint1(false), hitPoint2(false), hitLine(false)
 {
+       type = OTLine;
 }
 
+
 Line::~Line()
 {
+// Taking care of connections should be done by the Container, as we don't know
+// anything about any other object connected to this one.
+#if 0
        // If there are any attached Dimensions, we must set the attachment points
        // to NULL since they will no longer be valid.
        if (attachedDimension)
@@ -39,20 +50,22 @@ Line::~Line()
        // IT WOULD BE NICE to have any object points attached to this line automagically
        // connect to this dimension object at this point, instead of just becoming
        // detached.
+#endif
 }
 
-/*virtual*/ void Line::Draw(QPainter * painter)
+
+/*virtual*/ void Line::Draw(Painter * painter)
 {
-       painter->setPen(QPen(Qt::red, 2.0, Qt::DotLine));
+       painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
 
        if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
-               painter->drawEllipse(QPointF(position.x, position.y), 4.0, 4.0);
+               painter->DrawHandle(position);
 
        if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
-               painter->drawEllipse(QPointF(endpoint.x, endpoint.y), 4.0, 4.0);
+               painter->DrawHandle(endpoint);
 
        if ((state == OSInactive) && !hitLine)
-               painter->setPen(QPen(Qt::black, 1.0, Qt::SolidLine));
+               painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
 
        if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
        {
@@ -62,16 +75,50 @@ Line::~Line()
                Vector current(point2 - point1);
                Vector v = current.Unit() * length;
                Vector v2 = point1 + v;
-               painter->drawLine((int)point1.x, (int)point1.y, (int)v2.x, (int)v2.y);
+//             painter->DrawLine((int)point1.x, (int)point1.y, (int)v2.x, (int)v2.y);
+               painter->DrawLine(point1, v2);
 
                if (current.Magnitude() > length)
                {
-                       painter->setPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
-                       painter->drawLine((int)v2.x, (int)v2.y, (int)point2.x, (int)point2.y);
+                       painter->SetPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
+//                     painter->DrawLine((int)v2.x, (int)v2.y, (int)point2.x, (int)point2.y);
+                       painter->DrawLine(v2, point2);
                }
        }
+// Problem: when drawing at large zoom levels, this throws away precision thus
+//          causing the line to rendered too short. !!! FIX !!! [DONE]
        else
-               painter->drawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
+//             painter->DrawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
+               painter->DrawLine(position, endpoint);
+
+       // If we're rotating or setting the span, draw an information panel
+       // showing both absolute and relative angles being set.
+       if (draggingHandle1 || draggingHandle2)
+       {
+               double absAngle = (Vector(endpoint - position).Angle()) * RADIANS_TO_DEGREES;
+//             double relAngle = (startAngle >= oldAngle ? startAngle - oldAngle :
+//                     startAngle - oldAngle + (2.0 * PI)) * RADIANS_TO_DEGREES;
+               double absLength = Vector(position - endpoint).Magnitude();
+
+               QString text;
+
+               text = QObject::tr("Length: %1 in.\n") + QChar(0x2221) + QObject::tr(": %2");
+               text = text.arg(absLength).arg(absAngle);
+
+               QPen pen = QPen(QColor(0x00, 0xFF, 0x00), 1.0, Qt::SolidLine);
+               painter->SetPen(pen);
+               painter->SetBrush(QBrush(QColor(0x40, 0xFF, 0x40, 0x9F)));
+               QRectF textRect(10.0, 10.0, 270.0, 70.0);       // x, y, w, h
+               painter->DrawRoundedRect(textRect, 7.0, 7.0);
+
+               textRect.setLeft(textRect.left() + 14);
+               painter->SetFont(*Object::font);
+//                     pen = QPen(QColor(0xDF, 0x5F, 0x00), 1.0, Qt::SolidLine);
+               pen = QPen(QColor(0x00, 0x5F, 0xDF));
+               painter->SetPen(pen);
+               painter->DrawText(textRect, Qt::AlignVCenter, text);
+//             painter->SetPen(QPen(QColor(0xDF, 0x5F, 0x00)));
+       }
 }
 
 /*virtual*/ Vector Line::Center(void)
@@ -83,10 +130,20 @@ Line::~Line()
 
 /*virtual*/ bool Line::Collided(Vector point)
 {
-// Can't assume this!
-// Actually, we can, since this is a mouse down event here.
+       // We can assume this, since this is a mouse down event here.
        objectWasDragged = false;
+       SaveState();
        HitTest(point);
+//     return StateChanged();
+
+// this is shite. this should be checked for in the Container, not here!
+       // If we're part of a non-top-level container, send this signal to it
+       if (parent->type == OTContainer && !((Container *)parent)->isTopLevelContainer
+               && (hitLine || hitPoint1 || hitPoint2))
+       {
+               parent->state = OSSelected;
+               return true;
+       }
 
 /*
 There's a small problem here with the implementation: You can have a dimension tied
@@ -97,7 +154,7 @@ We solve this by allowing only *one* Dimension object to be attached to the Line
 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
 
 Problem still arises when we delete this object; The attached Dimension object will
-then have bad pointers! What is *should* do is delete the object if and only if this
+then have bad pointers! What it *should* do is delete the object if and only if this
 line is not attached to any other object. If it is, then one of those attachment
 points should be sent to the dimension object (done for position & endpoint).
 
@@ -116,6 +173,7 @@ TODO: Make Dimension preview with modifier keys for showing on other side
                // (Priorities are taken care of in HitTest()...)
                if (hitLine)
                {
+#if 0
                        if (attachedDimension == NULL)
                        {
                                // How to get this object into the top level container???
@@ -125,7 +183,7 @@ level container, it won't get drawn...
 But we can fix that by making this object call any attached object's (like
 a dimension only) Draw() function... :-/
 */
-                               attachedDimension = new Dimension(&position, &endpoint, this);
+                               attachedDimension = new Dimension(&position, &endpoint, DTLinear, this);
 
                                if (parent != NULL)
                                        parent->Add(attachedDimension);
@@ -135,6 +193,26 @@ a dimension only) Draw() function... :-/
                                // If there's one already there, tell it to flip sides...
                                attachedDimension->FlipSides();
                        }
+#else
+                       // New approach here: We look for connected objects.
+                       Object * attachedDimension = FindAttachedDimension();
+
+                       if (attachedDimension)
+                       {
+                               // If there's an attached Dimension, tell it to switch sides...
+                               ((Dimension *)attachedDimension)->FlipSides();
+                       }
+                       else
+                       {
+                               // Otherwise, we make a new one and attach it here.
+                               attachedDimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
+                               connected.push_back(Connection(attachedDimension, 0));
+                               connected.push_back(Connection(attachedDimension, 1.0));
+
+                               if (parent != NULL)
+                                       parent->Add(attachedDimension);
+                       }
+#endif
 
                        return true;
                }
@@ -196,21 +274,52 @@ Like so:
 //                     state = OSInactive;
                        oldPoint = point;
                        draggingLine = true;
+
+                       // Toggle selected state if CTRL held
+                       if (qApp->keyboardModifiers() == Qt::ControlModifier)
+                               state = OSInactive;
+
                        return true;
                }
        }
 
+       // If CTRL is held, then we bypass the "turn off" code. Still didn't hit
+       // *this* object though. :-)
+       if (qApp->keyboardModifiers() == Qt::ControlModifier)
+               return false;
+
        // If we got here, we clicked on nothing, so set the object to inactive.
        // (Once we can read key modifiers, we can override this to allow multiple selection.)
        state = OSInactive;
        return false;
 }
 
+
 /*virtual*/ void Line::PointerMoved(Vector point)
 {
-       // Hit test tells us what we hit (if anything) through boolean variables. It
-       // also tells us whether or not the state changed.
-       needUpdate = HitTest(point);
+       if (selectionInProgress)
+       {
+               // Check for whether or not the rect contains this line
+#if 0
+               if (selection.normalized().contains(Extents()))
+#else
+//             if (selection.normalized().contains(position.x, position.y)
+//                     && selection.normalized().contains(endpoint.x, endpoint.y))
+               if (selection.contains(position.x, position.y)
+                       && selection.contains(endpoint.x, endpoint.y))
+#endif
+                       state = OSSelected;
+               else
+                       state = OSInactive;
+
+               return;
+       }
+
+       // Hit test tells us what we hit (if anything) through boolean variables. (It
+       // also tells us whether or not the state changed. --not any more)
+       SaveState();
+       HitTest(point);
+       needUpdate = StateChanged();
 
        objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
 
@@ -226,6 +335,8 @@ Like so:
 
                oldPoint = point;
                needUpdate = true;
+
+//doesn't work         QMainWindow::statusBar()->setText("You are manipulating a line");
        }
 
 /*
@@ -238,18 +349,20 @@ Ugly ways to do it:
 
 More elegant ways:
  - Pass the point in a notification function (how?)
- - Pass the point as a reference to the class instance object (&endpoint). This way, the line
-   doesn't have to care about keeping track of Dimensions connected to it. But still have to
-   care about other connected entities (other Lines, Circles, Arcs, Splines, Texts, etc). I
-   think I'd be OK with this.
-   Since the Dimension has a pointer to our object, all we have to do is update our coordinates
-   and the Dimension object will adjust itself on the next repaint. Problem solved, and we don't
-   have to know anything about how many Dimensions are connected to us, or where! \o/
+ - Pass the point as a reference to the class instance object (&endpoint). This
+   way, the line doesn't have to care about keeping track of Dimensions
+   connected to it. But still have to care about other connected entities
+   (other Lines, Circles, Arcs, Splines, Texts, etc). I think I'd be OK with
+   this. Since the Dimension has a pointer to our object, all we have to do is
+   update our coordinates and the Dimension object will adjust itself on the
+   next repaint. Problem solved, and we don't have to know anything about how
+   many Dimensions are connected to us, or where! \o/
    The question then becomes, how do we do this kind of coupling???
 
-We need to know about connected entities so that we can have them either move in expected ways
-or constrain the movement of this Line object. This is how we will be a cut above all other CAD
-software currently out there: the GUI will try to do the right thing, most of the time. :-)
+We need to know about connected entities so that we can have them either move
+in expected ways or constrain the movement of this Line object. This is how we
+will be a cut above all other CAD software currently out there: the GUI will
+try to do the right thing, most of the time. :-)
 */
        if (needUpdate)
        {
@@ -257,13 +370,24 @@ software currently out there: the GUI will try to do the right thing, most of th
                Vector point1 = (draggingHandle1 ? endpoint : position);
                Vector point2 = (draggingHandle1 ? position : endpoint);
 
-               Vector current(point2, point1);
-               Vector v = current.Unit() * length;
-               Vector v2 = point1 + v;
+               if (Object::fixedAngle)
+               {
+                       // Here we calculate the component of the current vector along the fixed angle.
+                       // A_compB = (A . Bu) * Bu
+                       double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
+/*
+Actually, this isn't quite right. What we want to do is look for the intersection along either
+the horizontal line or vertical line that intersects from the current mouse position.
+*/
 
-               //bleh
-               if (!Object::fixedLength)
-                       v2 = point2;
+                       if (draggingHandle1)
+                               position = endpoint + (angle * magnitudeAlongB);
+
+                       if (draggingHandle2)
+                               endpoint = position + (angle * magnitudeAlongB);
+               }
+//             else
+//                     v2 = point2;
 
 //If we tell the dimension to flip sides, this is no longer a valid
 //assumption. !!! FIX !!!
@@ -282,6 +406,7 @@ software currently out there: the GUI will try to do the right thing, most of th
        }
 }
 
+
 /*virtual*/ void Line::PointerReleased(void)
 {
        if (draggingHandle1 || draggingHandle2)
@@ -296,98 +421,53 @@ software currently out there: the GUI will try to do the right thing, most of th
                        }
                        else                                    // endpoint
                        {
-//                             Vector v1 = endpoint - position;
                                Vector v = Vector(endpoint - position).Unit() * length;
                                endpoint = position + v;
                        }
                }
                else
                {
-                       // Otherwise, we calculate the new length, just in case on the next move
-                       // it turns out to have a fixed length. :-)
+                       // Otherwise, we calculate the new length, just in case on the next
+                       // move it turns out to have a fixed length. :-)
                        length = Vector(endpoint - position).Magnitude();
                }
+
+               if (!Object::fixedAngle)
+               {
+                       // Calculate the new angle, just in case on the next move it turns
+                       // out to be fixed. :-)
+                       angle = Vector(endpoint - position).Unit();
+               }
        }
 
        draggingLine = false;
        draggingHandle1 = false;
        draggingHandle2 = false;
 
-//     hitPoint1 = hitPoint2 = hitLine = false;
-
-       // Here we check for just a click: If object was clicked and dragged, then
-       // revert to the old state (OSInactive). Otherwise, keep the new state that
-       // we set.
-/*Maybe it would be better to just check for "object was dragged" state and not have to worry
-about keeping track of old states...
-*/
        if (objectWasDragged)
                state = oldState;
 }
 
-#if 0
-void Line::SetDimensionOnPoint1(Dimension * dimension)
-{
-       dimPoint1 = dimension;
-
-       if (dimension)
-               dimension->SetPoint1(position);
-}
-
-void Line::SetDimensionOnPoint2(Dimension * dimension)
-{
-       dimPoint2 = dimension;
-
-       if (dimension)
-               dimension->SetPoint2(endpoint);
-}
-#else
-void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
-{
-       // If they don't pass one in, create it for the caller.
-       if (dimension == NULL)
-       {
-               dimension = new Dimension(&position, &endpoint, this);
-
-               if (parent)
-                       parent->Add(dimension);
-       }
-
-       attachedDimension = dimension;
-
-       // After we set the points here, we don't have to care about them anymore.
-       if (dimension)
-       {
-               dimension->SetPoint1(&position);
-               dimension->SetPoint2(&endpoint);
-       }
-}
-#endif
 
-bool Line::HitTest(Point point)
+/*virtual*/ bool Line::HitTest(Point point)
 {
-       SaveState();
+//     SaveState();
 
        hitPoint1 = hitPoint2 = hitLine = false;
        Vector lineSegment = endpoint - position;
        Vector v1 = point - position;
        Vector v2 = point - endpoint;
-       double parameterizedPoint = lineSegment.Dot(v1) / lineSegment.Magnitude(), distance;
+       double t = Vector::Parameter(position, endpoint, point);
+       double distance;
 
        // Geometric interpretation:
-       // The parameterized point on the vector lineSegment is where the perpendicular
-       // intersects lineSegment. If pp < 0, then the perpendicular lies beyond the 1st
-       // endpoint. If pp > length of ls, then the perpendicular lies beyond the 2nd endpoint.
-
-       if (parameterizedPoint < 0.0)
-               distance = v1.Magnitude();
-       else if (parameterizedPoint > lineSegment.Magnitude())
-               distance = v2.Magnitude();
-       else
-               // distance = ?Det?(ls, v1) / |ls|
-               distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y) / lineSegment.Magnitude());
+       // The parameter "t" on the vector lineSegment is where the normal of
+       // lineSegment coincides with point. If t < 0, the normal lies beyond the
+       // 1st endpoint. If t > 1, then the normal lies beyond the 2nd endpoint. We
+       // only calculate the length of the normal between the point and the
+       // lineSegment when the parameter is between 0 and 1.
 
-       // Geometric interpretation of the above:
+       // Geometric interpretation of "distance = ?Det?(ls, v1) / |ls|":
        // If the segment endpoints are s and e, and the point is p, then the test
        // for the perpendicular intercepting the segment is equivalent to insisting
        // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
@@ -405,18 +485,161 @@ bool Line::HitTest(Point point)
        // all other rows, we end up with the matrix on the right which greatly
        // simplifies the calculation of the determinant.
 
-//How do we determine distance here? Especially if zoomed in or out???
-#warning "!!! Distances tested for may not be valid if zoomed in or out !!!"
-       if (v1.Magnitude() < 8.0)
+       if (t < 0.0)
+               distance = v1.Magnitude();
+       else if (t > 1.0)
+               distance = v2.Magnitude();
+       else
+               // distance = ?Det?(ls, v1) / |ls|
+               distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y)
+                       / lineSegment.Magnitude());
+
+       if ((v1.Magnitude() * Painter::zoom) < 8.0)
                hitPoint1 = true;
-       else if (v2.Magnitude() < 8.0)
+       else if ((v2.Magnitude() * Painter::zoom) < 8.0)
                hitPoint2 = true;
-       else if (distance < 5.0)
+       else if ((distance * Painter::zoom) < 5.0)
                hitLine = true;
 
-       return StateChanged();
+       return (hitPoint1 | hitPoint2 | hitLine ? true : false);
+//     return StateChanged();
+}
+
+
+// Check to see if the point passed in coincides with any we have. If so, return a
+// pointer to it; otherwise, return NULL.
+/*virtual*/ Vector * Line::GetPointAt(Vector v)
+{
+       if (v == position)
+               return &position;
+       else if (v == endpoint)
+               return &endpoint;
+
+       return 0;
 }
 
+
+/*virtual*/ void Line::Enumerate(FILE * file)
+{
+       fprintf(file, "LINE (%lf,%lf) (%lf,%lf)\n", position.x, position.y, endpoint.x, endpoint.y);
+}
+
+
+/*virtual*/ Object * Line::Copy(void)
+{
+#warning "!!! This doesn't take care of attached Dimensions !!!"
+/*
+This is a real problem. While having a pointer in the Dimension to this line's points
+is fast & easy, it creates a huge problem when trying to replicate an object like this.
+
+Maybe a way to fix that then, is to have reference numbers instead of pointers. That
+way, if you copy them, ... you might still have problems. Because you can't be sure if
+a copy will be persistant or not, you then *definitely* do not want them to have the
+same reference number.
+*/
+       return new Line(position, endpoint, parent);
+}
+
+
+/*virtual*/ Vector Line::GetPointAtParameter(double parameter)
+{
+       if (parameter <= 0)
+               return position;
+       else if (parameter >= 1.0)
+               return endpoint;
+
+       // Our parameter lies between zero and one, so calculate it!
+       Vector v(endpoint, position);
+       double length = v.Magnitude();
+       // We scale the magnitude of v so that it lies between 0 and 1...
+       // By multiplying the parameter by the magnitude, we obtain the point we
+       // want. No scaling necessary as it's inherent in the approach!
+       double spotOnLength = length * parameter;
+
+       // To get our point, we use the initial point of the line and add in our
+       // scaled point.
+       Vector result = position + (v * spotOnLength);
+       return result;
+}
+
+
+/*virtual*/ QRectF Line::Extents(void)
+{
+       QRectF rect(QPointF(position.x, position.y), QPointF(endpoint.x, endpoint.y));
+       return rect.normalized();
+}
+
+
+/*virtual*/ void Line::Translate(Vector amount)
+{
+       position += amount;
+       endpoint += amount;
+}
+
+
+/*virtual*/ void Line::Rotate(Vector point, double angle)
+{
+}
+
+
+/*virtual*/ void Line::Scale(Vector point, double amount)
+{
+}
+
+
+void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
+{
+       // If they don't pass one in, create it for the caller.
+       if (dimension == NULL)
+       {
+//printf("Line::SetDimensionOnLine(): Creating new dimension...\n");
+//             dimension = new Dimension(position, endpoint, DTLinear, this);
+               dimension = new Dimension(Connection(this, 0), Connection(this, 1.0), DTLinear, this);
+
+               if (parent)
+//{
+//printf("Line::SetDimensionOnLine(): Adding to parent...\n");
+                       parent->Add(dimension);
+//}
+       }
+       else
+       {
+               dimension->Connect(this, 0);
+               dimension->Connect(this, 1.0);
+       }
+
+       // Make sure the Dimension is connected to us...
+       Connect(dimension, 0);
+       Connect(dimension, 1.0);
+}
+
+
+Object * Line::FindAttachedDimension(void)
+{
+       // Is there anything connected to this line? If not, return NULL
+       if (connected.size() < 2)
+               return NULL;
+
+       // Otherwise, we have to search our objects to see if there's a likely
+       // candidate. In this case, we're looking for a pointer to the same object
+       // with a parameter of 0 and 1 respectively. This is O((n^2)/2).
+       for(uint i=0; i<connected.size(); i++)
+       {
+               for(uint j=i+1; j<connected.size(); j++)
+               {
+//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);
+                       if ((connected[i].object == connected[j].object)
+                               && ((connected[i].t == 0 && connected[j].t == 1.0)
+                               || (connected[i].t == 1.0 && connected[j].t == 0)))
+                               return connected[i].object;
+               }
+       }
+
+       // Didn't find anything, so return NULL
+       return NULL;
+}
+
+
 void Line::SaveState(void)
 {
        oldHitPoint1 = hitPoint1;
@@ -424,6 +647,7 @@ void Line::SaveState(void)
        oldHitLine = hitLine;
 }
 
+
 bool Line::StateChanged(void)
 {
        if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
@@ -432,6 +656,7 @@ bool Line::StateChanged(void)
        return false;
 }
 
+
 /*
 Intersection of two lines:
 
@@ -472,7 +697,7 @@ rearranging we get:
 t(d1x) - s(d2x) = p2x - p0x
 t(d1y) - s(d2y) = p2y - p0y
 
-Determinant D is ad - bc where the matrix look like:
+Determinant D is ad - bc where the matrix looks like:
 
 a b
 c d