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