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