]> Shamusworld >> Repos - architektonas/blob - src/line.cpp
d37106e5a7846df0f9fe8fabed88aee129ea42c5
[architektonas] / src / line.cpp
1 // line.cpp: Line object
2 //
3 // Part of the Architektonas Project
4 // (C) 2011 Underground Software
5 // See the README and GPLv3 files for licensing and warranty information
6 //
7 // JLH = James Hammons <jlhamm@acm.org>
8 //
9 // WHO  WHEN        WHAT
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
17 //
18
19 #include "line.h"
20
21 #include <QtGui>
22 #include "dimension.h"
23 #include "painter.h"
24
25
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)
30 {
31 }
32
33
34 Line::~Line()
35 {
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.
38 #if 0
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)
42         {
43                 attachedDimension->SetPoint1(NULL);
44                 attachedDimension->SetPoint2(NULL);
45         }
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
48         // detached.
49 #endif
50 }
51
52
53 /*virtual*/ void Line::Draw(Painter * painter)
54 {
55         painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
56
57         if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
58                 painter->DrawHandle(position);
59
60         if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
61                 painter->DrawHandle(endpoint);
62
63         if ((state == OSInactive) && !hitLine)
64                 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
65
66         if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
67         {
68                 Vector point1 = (draggingHandle1 ? endpoint : position);
69                 Vector point2 = (draggingHandle1 ? position : endpoint);
70
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);
76
77                 if (current.Magnitude() > length)
78                 {
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);
82                 }
83         }
84 // Problem: when drawing at large zoom levels, this throws away precision thus
85 //          causing the line to rendered too short. !!! FIX !!! [DONE]
86         else
87 //              painter->DrawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
88                 painter->DrawLine(position, endpoint);
89 }
90
91 /*virtual*/ Vector Line::Center(void)
92 {
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);
95         return endpoint + v;
96 }
97
98 /*virtual*/ bool Line::Collided(Vector point)
99 {
100         // We can assume this, since this is a mouse down event here.
101         objectWasDragged = false;
102         HitTest(point);
103
104 /*
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...
108
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.
111
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).
116
117 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
118       for our purposes
119
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...
122
123 TODO: Make Dimension preview with modifier keys for showing on other side
124 */
125         // Is the dimension tool active? Let's use it:
126         if (dimensionActive)
127         {
128                 // User clicked on the line itself (endpoint checks should preceed this one):
129                 // (Priorities are taken care of in HitTest()...)
130                 if (hitLine)
131                 {
132 #if 0
133                         if (attachedDimension == NULL)
134                         {
135                                 // How to get this object into the top level container???
136 /*
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... :-/
141 */
142                                 attachedDimension = new Dimension(&position, &endpoint, DTLinear, this);
143
144                                 if (parent != NULL)
145                                         parent->Add(attachedDimension);
146                         }
147                         else
148                         {
149                                 // If there's one already there, tell it to flip sides...
150                                 attachedDimension->FlipSides();
151                         }
152 #else
153                         // New approach here: We look for connected objects.
154                         Object * attachedDimension = FindAttachedDimension();
155
156                         if (attachedDimension)
157                         {
158                                 // If there's an attached Dimension, tell it to switch sides...
159                                 ((Dimension *)attachedDimension)->FlipSides();
160                         }
161                         else
162                         {
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));
167
168                                 if (parent != NULL)
169                                         parent->Add(attachedDimension);
170                         }
171 #endif
172
173                         return true;
174                 }
175         }
176
177
178         if (state == OSInactive)
179         {
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);
183 //printf("      \n", );
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.
189
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).
194                 if (hitPoint1)
195                 {
196                         oldState = state;
197                         state = OSSelected;
198                         oldPoint = position; //maybe "position"?
199                         draggingHandle1 = true;
200                         return true;
201                 }
202                 else if (hitPoint2)
203                 {
204                         oldState = state;
205                         state = OSSelected;
206                         oldPoint = endpoint; //maybe "position"?
207                         draggingHandle2 = true;
208                         return true;
209                 }
210                 else if (hitLine)
211                 {
212                         oldState = state;
213                         state = OSSelected;
214                         oldPoint = point;
215                         draggingLine = true;
216                         return true;
217                 }
218         }
219         else if (state == OSSelected)
220         {
221                 // Here we test for collision with handles as well! (SOON!) [I think it works...NOPE]
222 /*
223 Like so:
224                 if (v1.Magnitude() < 2.0) // Handle #1
225                 else if (v2.Magnitude() < 2.0) // Handle #2
226 */
227                 if (hitLine)
228                 {
229                         oldState = state;
230 //                      state = OSInactive;
231                         oldPoint = point;
232                         draggingLine = true;
233                         return true;
234                 }
235         }
236
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.)
239         state = OSInactive;
240         return false;
241 }
242
243
244 /*virtual*/ void Line::PointerMoved(Vector point)
245 {
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);
249
250         objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
251
252         if (objectWasDragged)
253         {
254                 Vector delta = point - oldPoint;
255
256                 if (draggingHandle1 || draggingLine)
257                         position += delta;
258
259                 if (draggingHandle2 || draggingLine)
260                         endpoint += delta;
261
262                 oldPoint = point;
263                 needUpdate = true;
264         }
265
266 /*
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!
269
270 Ugly ways to do it:
271  - Keep track of the state of the connected dimension
272  - Pass the Dimension the point that's being changed and the delta
273
274 More elegant ways:
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???
284
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. :-)
288 */
289         if (needUpdate)
290         {
291 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
292                 Vector point1 = (draggingHandle1 ? endpoint : position);
293                 Vector point2 = (draggingHandle1 ? position : endpoint);
294
295 #if 0
296                 Vector current(point2, point1);
297                 Vector v = current.Unit() * length;
298                 Vector v2 = point1 + v;
299
300                 //bleh
301                 if (!Object::fixedLength)
302                         v2 = point2;
303 #endif
304
305                 if (Object::fixedAngle)
306                 {
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);
310 /*
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.
313 */
314
315                         if (draggingHandle1)
316                                 position = endpoint + (angle * magnitudeAlongB);
317
318                         if (draggingHandle2)
319                                 endpoint = position + (angle * magnitudeAlongB);
320                 }
321 //              else
322 //                      v2 = point2;
323
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.
331 #if 0
332                 if (dimPoint1)
333                         dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
334                 
335                 if (dimPoint2)
336                         dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
337 #endif
338         }
339 }
340
341
342 /*virtual*/ void Line::PointerReleased(void)
343 {
344         if (draggingHandle1 || draggingHandle2)
345         {
346                 // Set the length (in case the global state was set to fixed (or not))
347                 if (Object::fixedLength)
348                 {
349                         if (draggingHandle1)    // startpoint
350                         {
351                                 Vector v = Vector(position - endpoint).Unit() * length;
352                                 position = endpoint + v;
353                         }
354                         else                                    // endpoint
355                         {
356 //                              Vector v1 = endpoint - position;
357                                 Vector v = Vector(endpoint - position).Unit() * length;
358                                 endpoint = position + v;
359                         }
360                 }
361                 else
362                 {
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();
366                 }
367
368                 if (!Object::fixedAngle)
369                 {
370                         // Calculate the new angle, just in case on the next move it turns out to
371                         // be fixed. :-)
372                         angle = Vector(endpoint - position).Unit();
373                 }
374         }
375
376         draggingLine = false;
377         draggingHandle1 = false;
378         draggingHandle2 = false;
379
380 //      hitPoint1 = hitPoint2 = hitLine = false;
381
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
384         // we set.
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...
387 */
388         if (objectWasDragged)
389                 state = oldState;
390 }
391
392
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)
396 {
397         if (v == position)
398                 return &position;
399         else if (v == endpoint)
400                 return &endpoint;
401
402         return 0;
403 }
404
405
406 /*virtual*/ void Line::Enumerate(FILE * file)
407 {
408         fprintf(file, "LINE (%lf,%lf) (%lf,%lf)\n", position.x, position.y, endpoint.x, endpoint.y);
409 }
410
411
412 /*virtual*/ Object * Line::Copy(void)
413 {
414 #warning "!!! This doesn't take care of attached Dimensions !!!"
415 /*
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.
418
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.
422 */
423         return new Line(position, endpoint, parent);
424 }
425
426
427 /*virtual*/ Vector Line::GetPointAtParameter(double parameter)
428 {
429         if (parameter <= 0)
430                 return position;
431         else if (parameter >= 1.0)
432                 return endpoint;
433
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;
441
442         // To get our point, we use the initial point of the line and add in our
443         // scaled point.
444         Vector result = position + (v * spotOnLength);
445         return result;
446 }
447
448
449 void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
450 {
451         // If they don't pass one in, create it for the caller.
452         if (dimension == NULL)
453         {
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);
457
458                 if (parent)
459 //{
460 //printf("Line::SetDimensionOnLine(): Adding to parent...\n");
461                         parent->Add(dimension);
462 //}
463         }
464         else
465         {
466                 dimension->Connect(this, 0);
467                 dimension->Connect(this, 1.0);
468         }
469
470         // Make sure the Dimension is connected to us...
471         Connect(dimension, 0);
472         Connect(dimension, 1.0);
473 }
474
475
476 Object * Line::FindAttachedDimension(void)
477 {
478         // Is there anything connected to this line? If not, return NULL
479         if (connected.size() < 2)
480                 return NULL;
481
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++)
486         {
487                 for(uint j=i+1; j<connected.size(); j++)
488                 {
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;
494                 }
495         }
496
497         // Didn't find anything, so return NULL
498         return NULL;
499 }
500
501
502 bool Line::HitTest(Point point)
503 {
504         SaveState();
505
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;
511
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.
516
517         if (parameterizedPoint < 0.0)
518                 distance = v1.Magnitude();
519         else if (parameterizedPoint > lineSegment.Magnitude())
520                 distance = v2.Magnitude();
521         else
522                 // distance = ?Det?(ls, v1) / |ls|
523                 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y) / lineSegment.Magnitude());
524
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:
534         //
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
538         //
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.
542
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 !!!"
545 // [FIXED]
546         if ((v1.Magnitude() * Painter::zoom) < 8.0)
547                 hitPoint1 = true;
548         else if ((v2.Magnitude() * Painter::zoom) < 8.0)
549                 hitPoint2 = true;
550         else if ((distance * Painter::zoom) < 5.0)
551                 hitLine = true;
552
553         return StateChanged();
554 }
555
556 void Line::SaveState(void)
557 {
558         oldHitPoint1 = hitPoint1;
559         oldHitPoint2 = hitPoint2;
560         oldHitLine = hitLine;
561 }
562
563 bool Line::StateChanged(void)
564 {
565         if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
566                 return true;
567
568         return false;
569 }
570
571 /*
572 Intersection of two lines:
573
574 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
575
576 When they intersect, we can set the equations equal to one another:
577
578 i + j + t (3i - j) = -i + s (j)
579
580 Equating coefficients:
581 1 + 3t = -1 and 1 - t = s
582 So t = -2/3 and s = 5/3
583
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 .
585
586
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.
589
590 d1 = v1 - v0, d2 = v3 - v2
591
592 Our parametric equations for the line then are:
593
594 r1 = v0 + t(d1)
595 r2 = v2 + s(d2)
596
597 Set r1 = r2, thus we have:
598
599 v0 + t(d1) = v2 + s(d2)
600
601 Taking coefficients, we have:
602
603 p0x + t(d1x) = p2x + s(d2x)
604 p0y + t(d1y) = p2y + s(d2y)
605
606 rearranging we get:
607
608 t(d1x) - s(d2x) = p2x - p0x
609 t(d1y) - s(d2y) = p2y - p0y
610
611 Determinant D is ad - bc where the matrix looks like:
612
613 a b
614 c d
615
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)
620 t = Dx/D, s = Dy/D
621
622 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
623
624 ---------------------------------------------------------------------------------------------------
625
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:
627
628 v1 ( bx1 , by1 );
629 v2 ( bx2 , by2 );
630 v3 ( bx3 , by3 );
631
632 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
633
634 n1 ( -by1 , bx1 );
635 n3 ( -by3 , bx3 );
636
637 Dot products:
638
639 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
640 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
641
642 ratio = dp1 / dp2;
643 crossing vector = v1 * ratio;
644
645 And that's it.
646
647 -----------------------------------
648
649 So... to code this, let's say we have two Lines: l1 & l2.
650
651 Vector v1 = l1.endpoint - l1.position;
652 Vector v2 = l2.endpoint - l2.position;
653 Vector v3 = v2 - v1;
654
655 Vector normal1(-v1.y, v1.x);
656 Vector normal3(-v3.y, v3.x);
657
658 double dotProduct1 = v2.Dot(normal1);
659 double dotProduct2 = v2.Dot(normal3);
660
661 if (dotProduct2 == 0)
662         return ParallelLines;
663 else
664 {
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);
667         return intersection;
668 }
669 */
670