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