]> Shamusworld >> Repos - architektonas/blob - src/line.cpp
Fix for missing ampersand in QApplication.
[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 Line::~Line()
34 {
35         // If there are any attached Dimensions, we must set the attachment points
36         // to NULL since they will no longer be valid.
37         if (attachedDimension)
38         {
39                 attachedDimension->SetPoint1(NULL);
40                 attachedDimension->SetPoint2(NULL);
41         }
42         // IT WOULD BE NICE to have any object points attached to this line automagically
43         // connect to this dimension object at this point, instead of just becoming
44         // detached.
45 }
46
47 /*virtual*/ void Line::Draw(Painter * painter)
48 {
49         painter->SetPen(QPen(Qt::red, 2.0, Qt::DotLine));
50
51         if ((state == OSSelected) || ((state == OSInactive) && hitPoint1))
52                 painter->DrawHandle(position);
53
54         if ((state == OSSelected) || ((state == OSInactive) && hitPoint2))
55                 painter->DrawHandle(endpoint);
56
57         if ((state == OSInactive) && !hitLine)
58                 painter->SetPen(QPen(Qt::black, 1.0, Qt::SolidLine));
59
60         if (Object::fixedLength && (draggingHandle1 || draggingHandle2))
61         {
62                 Vector point1 = (draggingHandle1 ? endpoint : position);
63                 Vector point2 = (draggingHandle1 ? position : endpoint);
64
65                 Vector current(point2 - point1);
66                 Vector v = current.Unit() * length;
67                 Vector v2 = point1 + v;
68 //              painter->DrawLine((int)point1.x, (int)point1.y, (int)v2.x, (int)v2.y);
69                 painter->DrawLine(point1, v2);
70
71                 if (current.Magnitude() > length)
72                 {
73                         painter->SetPen(QPen(QColor(128, 0, 0), 1.0, Qt::DashLine));
74 //                      painter->DrawLine((int)v2.x, (int)v2.y, (int)point2.x, (int)point2.y);
75                         painter->DrawLine(v2, point2);
76                 }
77         }
78 // Problem: when drawing at large zoom levels, this throws away precision thus
79 //          causing the line to rendered too short. !!! FIX !!! [DONE]
80         else
81 //              painter->DrawLine((int)position.x, (int)position.y, (int)endpoint.x, (int)endpoint.y);
82                 painter->DrawLine(position, endpoint);
83 }
84
85 /*virtual*/ Vector Line::Center(void)
86 {
87         // Technically, this is the midpoint but who are we to quibble? :-)
88         Vector v((position.x - endpoint.x) / 2.0, (position.y - endpoint.y) / 2.0);
89         return endpoint + v;
90 }
91
92 /*virtual*/ bool Line::Collided(Vector point)
93 {
94         // We can assume this, since this is a mouse down event here.
95         objectWasDragged = false;
96         HitTest(point);
97
98 /*
99 There's a small problem here with the implementation: You can have a dimension tied
100 to only one point while at the same time you can have a dimension sitting on this line.
101 Since there's only *one* dimPoint for each point, this can be problematic...
102
103 We solve this by allowing only *one* Dimension object to be attached to the Line,
104 Arc, etc. and by giving the Dimension object a pointer to our endpoints.
105
106 Problem still arises when we delete this object; The attached Dimension object will
107 then have bad pointers! What it *should* do is delete the object if and only if this
108 line is not attached to any other object. If it is, then one of those attachment
109 points should be sent to the dimension object (done for position & endpoint).
110
111 NOTE: The STL vector<T> *does not* take ownership of pointers, therefore is suitable
112       for our purposes
113
114 Also: It would be nice to have a preview of the dimension being drawn, with a modifier
115 key to make it draw/show on the other side...
116
117 TODO: Make Dimension preview with modifier keys for showing on other side
118 */
119         // Is the dimension tool active? Let's use it:
120         if (dimensionActive)
121         {
122                 // User clicked on the line itself (endpoint checks should preceed this one):
123                 // (Priorities are taken care of in HitTest()...)
124                 if (hitLine)
125                 {
126                         if (attachedDimension == NULL)
127                         {
128                                 // How to get this object into the top level container???
129 /*
130 The real question is do we care. I think so, because if this isn't in the top
131 level container, it won't get drawn...
132 But we can fix that by making this object call any attached object's (like
133 a dimension only) Draw() function... :-/
134 */
135                                 attachedDimension = new Dimension(&position, &endpoint, this);
136
137                                 if (parent != NULL)
138                                         parent->Add(attachedDimension);
139                         }
140                         else
141                         {
142                                 // If there's one already there, tell it to flip sides...
143                                 attachedDimension->FlipSides();
144                         }
145
146                         return true;
147                 }
148         }
149
150
151         if (state == OSInactive)
152         {
153 //printf("Line: pp = %lf, length = %lf, distance = %lf\n", parameterizedPoint, lineSegment.Magnitude(), distance);
154 //printf("      v1.Magnitude = %lf, v2.Magnitude = %lf\n", v1.Magnitude(), v2.Magnitude());
155 //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);
156 //printf("      \n", );
157 //How to translate this into pixels from Document space???
158 //Maybe we need to pass a scaling factor in here from the caller? That would make sense, as
159 //the caller knows about the zoom factor and all that good kinda crap
160 //I think what's needed is an Object class variable/method that can be changed by the TLC and
161 //called in derived classes to properly scale the location to the current zoom level. That *should* work.
162
163 // ALSO: Need to code a global (read: Object class) variable that tells use whether a modifier
164 //       key was pressed in addition to the mouse click, so we can do stuff like, say, hold
165 //       down CTRL and be able to do multiple selecting of objects (in that case, we would
166 //       keep the Object state from changing).
167                 if (hitPoint1)
168                 {
169                         oldState = state;
170                         state = OSSelected;
171                         oldPoint = position; //maybe "position"?
172                         draggingHandle1 = true;
173                         return true;
174                 }
175                 else if (hitPoint2)
176                 {
177                         oldState = state;
178                         state = OSSelected;
179                         oldPoint = endpoint; //maybe "position"?
180                         draggingHandle2 = true;
181                         return true;
182                 }
183                 else if (hitLine)
184                 {
185                         oldState = state;
186                         state = OSSelected;
187                         oldPoint = point;
188                         draggingLine = true;
189                         return true;
190                 }
191         }
192         else if (state == OSSelected)
193         {
194                 // Here we test for collision with handles as well! (SOON!) [I think it works...NOPE]
195 /*
196 Like so:
197                 if (v1.Magnitude() < 2.0) // Handle #1
198                 else if (v2.Magnitude() < 2.0) // Handle #2
199 */
200                 if (hitLine)
201                 {
202                         oldState = state;
203 //                      state = OSInactive;
204                         oldPoint = point;
205                         draggingLine = true;
206                         return true;
207                 }
208         }
209
210         // If we got here, we clicked on nothing, so set the object to inactive.
211         // (Once we can read key modifiers, we can override this to allow multiple selection.)
212         state = OSInactive;
213         return false;
214 }
215
216 /*virtual*/ void Line::PointerMoved(Vector point)
217 {
218         // Hit test tells us what we hit (if anything) through boolean variables. It
219         // also tells us whether or not the state changed.
220         needUpdate = HitTest(point);
221
222         objectWasDragged = (draggingLine | draggingHandle1 | draggingHandle2);
223
224         if (objectWasDragged)
225         {
226                 Vector delta = point - oldPoint;
227
228                 if (draggingHandle1 || draggingLine)
229                         position += delta;
230
231                 if (draggingHandle2 || draggingLine)
232                         endpoint += delta;
233
234                 oldPoint = point;
235                 needUpdate = true;
236         }
237
238 /*
239 We can't count on any coupling between the dimension object and us, so how do we do this???
240 Also, there may be more than one Dimension object connected to a single endpoint!
241
242 Ugly ways to do it:
243  - Keep track of the state of the connected dimension
244  - Pass the Dimension the point that's being changed and the delta
245
246 More elegant ways:
247  - Pass the point in a notification function (how?)
248  - Pass the point as a reference to the class instance object (&endpoint). This way, the line
249    doesn't have to care about keeping track of Dimensions connected to it. But still have to
250    care about other connected entities (other Lines, Circles, Arcs, Splines, Texts, etc). I
251    think I'd be OK with this.
252    Since the Dimension has a pointer to our object, all we have to do is update our coordinates
253    and the Dimension object will adjust itself on the next repaint. Problem solved, and we don't
254    have to know anything about how many Dimensions are connected to us, or where! \o/
255    The question then becomes, how do we do this kind of coupling???
256
257 We need to know about connected entities so that we can have them either move in expected ways
258 or constrain the movement of this Line object. This is how we will be a cut above all other CAD
259 software currently out there: the GUI will try to do the right thing, most of the time. :-)
260 */
261         if (needUpdate)
262         {
263 // should only do this if "Fixed Length" is set... !!! FIX !!! [DONE]
264                 Vector point1 = (draggingHandle1 ? endpoint : position);
265                 Vector point2 = (draggingHandle1 ? position : endpoint);
266
267 #if 0
268                 Vector current(point2, point1);
269                 Vector v = current.Unit() * length;
270                 Vector v2 = point1 + v;
271
272                 //bleh
273                 if (!Object::fixedLength)
274                         v2 = point2;
275 #endif
276
277                 if (Object::fixedAngle)
278                 {
279                         // Here we calculate the component of the current vector along the fixed angle.
280                         // A_compB = (A . Bu) * Bu
281                         double magnitudeAlongB = Vector::Dot(Vector(point2 - point1), angle);
282 /*
283 Actually, this isn't quite right. What we want to do is look for the intersection along either
284 the horizontal line or vertical line that intersects from the current mouse position.
285 */
286
287                         if (draggingHandle1)
288                                 position = endpoint + (angle * magnitudeAlongB);
289
290                         if (draggingHandle2)
291                                 endpoint = position + (angle * magnitudeAlongB);
292                 }
293 //              else
294 //                      v2 = point2;
295
296 //If we tell the dimension to flip sides, this is no longer a valid
297 //assumption. !!! FIX !!!
298 //Ideally, we should just send the point that's changing to the Dimension object
299 //and have it figure out which point needs to move... Or is it???
300 // Ideally, we shouldn't have to fuck around with this shit. We need to fix the rendering code
301 // so that we don't have to wait until the dragging is done to correct the position of the
302 // point in question, but we'd need another variable tho.
303 #if 0
304                 if (dimPoint1)
305                         dimPoint1->SetPoint1(draggingHandle1 ? v2 : position);
306                 
307                 if (dimPoint2)
308                         dimPoint2->SetPoint2(draggingHandle2 ? v2 : endpoint);
309 #endif
310         }
311 }
312
313 /*virtual*/ void Line::PointerReleased(void)
314 {
315         if (draggingHandle1 || draggingHandle2)
316         {
317                 // Set the length (in case the global state was set to fixed (or not))
318                 if (Object::fixedLength)
319                 {
320                         if (draggingHandle1)    // startpoint
321                         {
322                                 Vector v = Vector(position - endpoint).Unit() * length;
323                                 position = endpoint + v;
324                         }
325                         else                                    // endpoint
326                         {
327 //                              Vector v1 = endpoint - position;
328                                 Vector v = Vector(endpoint - position).Unit() * length;
329                                 endpoint = position + v;
330                         }
331                 }
332                 else
333                 {
334                         // Otherwise, we calculate the new length, just in case on the next move
335                         // it turns out to have a fixed length. :-)
336                         length = Vector(endpoint - position).Magnitude();
337                 }
338
339                 if (!Object::fixedAngle)
340                 {
341                         // Calculate the new angle, just in case on the next move it turns out to
342                         // be fixed. :-)
343                         angle = Vector(endpoint - position).Unit();
344                 }
345         }
346
347         draggingLine = false;
348         draggingHandle1 = false;
349         draggingHandle2 = false;
350
351 //      hitPoint1 = hitPoint2 = hitLine = false;
352
353         // Here we check for just a click: If object was clicked and dragged, then
354         // revert to the old state (OSInactive). Otherwise, keep the new state that
355         // we set.
356 /*Maybe it would be better to just check for "object was dragged" state and not have to worry
357 about keeping track of old states...
358 */
359         if (objectWasDragged)
360                 state = oldState;
361 }
362
363 // Check to see if the point passed in coincides with any we have. If so, return a
364 // pointer to it; otherwise, return NULL.
365 /*virtual*/ Vector * Line::GetPointAt(Vector v)
366 {
367         if (v == position)
368                 return &position;
369         else if (v == endpoint)
370                 return &endpoint;
371
372         return 0;
373 }
374
375 #if 0
376 void Line::SetDimensionOnPoint1(Dimension * dimension)
377 {
378         dimPoint1 = dimension;
379
380         if (dimension)
381                 dimension->SetPoint1(position);
382 }
383
384 void Line::SetDimensionOnPoint2(Dimension * dimension)
385 {
386         dimPoint2 = dimension;
387
388         if (dimension)
389                 dimension->SetPoint2(endpoint);
390 }
391 #else
392 void Line::SetDimensionOnLine(Dimension * dimension/*=NULL*/)
393 {
394         // If they don't pass one in, create it for the caller.
395         if (dimension == NULL)
396         {
397                 dimension = new Dimension(&position, &endpoint, this);
398
399                 if (parent)
400                         parent->Add(dimension);
401         }
402
403         attachedDimension = dimension;
404
405         // After we set the points here, we don't have to care about them anymore.
406         if (dimension)
407         {
408                 dimension->SetPoint1(&position);
409                 dimension->SetPoint2(&endpoint);
410         }
411 }
412 #endif
413
414 bool Line::HitTest(Point point)
415 {
416         SaveState();
417
418         hitPoint1 = hitPoint2 = hitLine = false;
419         Vector lineSegment = endpoint - position;
420         Vector v1 = point - position;
421         Vector v2 = point - endpoint;
422         double parameterizedPoint = lineSegment.Dot(v1) / lineSegment.Magnitude(), distance;
423
424         // Geometric interpretation:
425         // The parameterized point on the vector lineSegment is where the perpendicular
426         // intersects lineSegment. If pp < 0, then the perpendicular lies beyond the 1st
427         // endpoint. If pp > length of ls, then the perpendicular lies beyond the 2nd endpoint.
428
429         if (parameterizedPoint < 0.0)
430                 distance = v1.Magnitude();
431         else if (parameterizedPoint > lineSegment.Magnitude())
432                 distance = v2.Magnitude();
433         else
434                 // distance = ?Det?(ls, v1) / |ls|
435                 distance = fabs((lineSegment.x * v1.y - v1.x * lineSegment.y) / lineSegment.Magnitude());
436
437         // Geometric interpretation of the above:
438         // If the segment endpoints are s and e, and the point is p, then the test
439         // for the perpendicular intercepting the segment is equivalent to insisting
440         // that the two dot products {s-e}.{s-p} and {e-s}.{e-p} are both non-negative.
441         // Perpendicular distance from the point to the segment is computed by first
442         // computing the area of the triangle the three points form, then dividing by
443         // the length of the segment.  Distances are done just by the Pythagorean
444         // theorem. Twice the area of the triangle formed by three points is the
445         // determinant of the following matrix:
446         //
447         // sx sy 1       0  0  1       0  0  0
448         // ex ey 1  ==>  ex ey 1  ==>  ex ey 0
449         // px py 1       px py 1       px py 0
450         //
451         // By translating the start point to the origin, and subtracting row 1 from
452         // all other rows, we end up with the matrix on the right which greatly
453         // simplifies the calculation of the determinant.
454
455 //How do we determine distance here? Especially if zoomed in or out???
456 //#warning "!!! Distances tested for may not be valid if zoomed in or out !!!"
457 // [FIXED]
458         if ((v1.Magnitude() * Painter::zoom) < 8.0)
459                 hitPoint1 = true;
460         else if ((v2.Magnitude() * Painter::zoom) < 8.0)
461                 hitPoint2 = true;
462         else if ((distance * Painter::zoom) < 5.0)
463                 hitLine = true;
464
465         return StateChanged();
466 }
467
468 void Line::SaveState(void)
469 {
470         oldHitPoint1 = hitPoint1;
471         oldHitPoint2 = hitPoint2;
472         oldHitLine = hitLine;
473 }
474
475 bool Line::StateChanged(void)
476 {
477         if ((hitPoint1 != oldHitPoint1) || (hitPoint2 != oldHitPoint2) || (hitLine != oldHitLine))
478                 return true;
479
480         return false;
481 }
482
483 /*
484 Intersection of two lines:
485
486 Find where the lines with equations r = i + j + t (3i - j) and r = -i + s (j) intersect.
487
488 When they intersect, we can set the equations equal to one another:
489
490 i + j + t (3i - j) = -i + s (j)
491
492 Equating coefficients:
493 1 + 3t = -1 and 1 - t = s
494 So t = -2/3 and s = 5/3
495
496 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 .
497
498
499 so, let's say we have two lines, l1 and l2. Points are v0(p0x, p0y), v1(p1x, p1y) for l1
500 and v2(p2x, p2y), v3(p3x, p3y) for l2.
501
502 d1 = v1 - v0, d2 = v3 - v2
503
504 Our parametric equations for the line then are:
505
506 r1 = v0 + t(d1)
507 r2 = v2 + s(d2)
508
509 Set r1 = r2, thus we have:
510
511 v0 + t(d1) = v2 + s(d2)
512
513 Taking coefficients, we have:
514
515 p0x + t(d1x) = p2x + s(d2x)
516 p0y + t(d1y) = p2y + s(d2y)
517
518 rearranging we get:
519
520 t(d1x) - s(d2x) = p2x - p0x
521 t(d1y) - s(d2y) = p2y - p0y
522
523 Determinant D is ad - bc where the matrix looks like:
524
525 a b
526 c d
527
528 so D = (d1x)(d2y) - (d2x)(d1y)
529 if D = 0, the lines are parallel.
530 Dx = (p2x - p0x)(d2y) - (d2x)(p2y - p0y)
531 Dy = (d1x)(p2y - p0y) - (p2x - p0x)(d1y)
532 t = Dx/D, s = Dy/D
533
534 We only need to calculate t, as we can then multiply it by d1 to get the intersection point.
535
536 ---------------------------------------------------------------------------------------------------
537
538 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:
539
540 v1 ( bx1 , by1 );
541 v2 ( bx2 , by2 );
542 v3 ( bx3 , by3 );
543
544 Perp product is equal with dot product of normal of first vector and the second vector, so we need normals:
545
546 n1 ( -by1 , bx1 );
547 n3 ( -by3 , bx3 );
548
549 Dot products:
550
551 dp1 = n3 . v2 = -by3 * bx2 + bx3 * by2;
552 dp2 = n1 . v2 = -by1 * bx2 + bx1 * by2;
553
554 ratio = dp1 / dp2;
555 crossing vector = v1 * ratio;
556
557 And that's it.
558
559 -----------------------------------
560
561 So... to code this, let's say we have two Lines: l1 & l2.
562
563 Vector v1 = l1.endpoint - l1.position;
564 Vector v2 = l2.endpoint - l2.position;
565 Vector v3 = v2 - v1;
566
567 Vector normal1(-v1.y, v1.x);
568 Vector normal3(-v3.y, v3.x);
569
570 double dotProduct1 = v2.Dot(normal1);
571 double dotProduct2 = v2.Dot(normal3);
572
573 if (dotProduct2 == 0)
574         return ParallelLines;
575 else
576 {
577         // I think we'd still have to add the intersection to the position point to get the intersection...
578         Point intersection = v1 * (dotProduct1 / dotProduct2);
579         return intersection;
580 }
581 */
582