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