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