1

For my A-Level project I am creating a dijktras algorithm program. I have created nodes, textboxes, buttons etc. all with correct hover detection (with the mouse), however, I tried to create a hover detection for the arcs/lines/connectors using arctan; by getting the difference in y over the difference in x (opposite over adjacent as we did) of the two connecting locations and then from one location (point a) to the mouse position and comparing. However, the difference always seems to be larger the closer you get to point a so that with the limits I set up it easily detects hovering closer to point a than point b (especially when the difference in y over the difference in x is very small. This creates some errors as I want the program to detect it evenly over the entire line. Are there any better ways which I could approach the line hover check?

Kevin
  • 74,910
  • 12
  • 133
  • 166
Jack
  • 65
  • 1
  • 10
  • I don't think I follow. You're using arctan to find the angle between the two endpoints of a line segment. Ok, but how does that tell you anything about the proximity of that line segment to the mouse position, which doesn't seem to be part of your formula? – Kevin Dec 14 '15 at 12:58
  • I use arctan a second time to get the angle between the mouse location and one of the points and compare them. I do other checks to make sure that the mouse is between the two points but I've checked and that all works. – Jack Dec 14 '15 at 13:00

3 Answers3

3

Calculate the distance from the line.

For a straight line (from wikipedia https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line) distance of point (x0,y0) from line (x1,1)->(x2,y2) is:

formula for distance of point (x0,y0) from line (x1,1)->(x2,y2)

There are other formulae for calculating distance of point from straight line, but this one is convenient and no special cases for horizontal/vertical lines.

For an arc, use ( (distance from the mouse to center of the circle) - (the radius of the circle) ).

These will both give you measures which are consistent and independent of where the mouse is, so you can define the threshold of mouse hover as some relatively small value. You should handle the special cases where mouse is within range of more than one line/arc - nothing worse than a UI which randomly chooses one in these cases, because the user will always want to choose one that isn't randomly selected.

Update: a little more searching and I found this Shortest distance between a point and a line segment - woolie's python answer seems nice and concise. This calculates the intersection of the line passing through point v and w, v->w with the perpendicular which goes through the point p in terms of a value t which varies between 0 and 1 along the line from v to w - so it is easy to decide that the mouse cursor at p is on the line segment v->w because in this case 0.0<=t<=1.0 or beyond either end of the segment v->w because in this case t < 0.0 or t > 1.0

HTH Barny

Community
  • 1
  • 1
  • This is a good solution iff, when OP says "line", he means "line" and not "line segment". If he does mean "line segment", then just using "distance to line" may not be appropriate. Ex. For a line segment with end points (0,0) and (0,1), and mouse position (1, 100), its "distance to line" is 1, but its "distance to line segment" is 99.005. – Kevin Dec 14 '15 at 13:43
  • I'd assumed that when the OP said in one of his comments on the question 'I do other checks to make sure that the mouse is between the two points ' that these would still be used. – DisappointedByUnaccountableMod Dec 14 '15 at 14:50
  • Hmm, that is true... Then he could do `if is_between(a,b,p): return distance(a,b,p); else: return min(distance(a,p), distance(b,p))`. – Kevin Dec 14 '15 at 14:57
  • I'm not sure what would cause this situation not to work. I have programmed it into the program and it seems to work perfectly. Unless I can find a way it doesn't work I think this is my preferred solution – Jack Dec 14 '15 at 17:57
  • I have found an issue where it is not within the bounds. I then applied the limitation code it works. I'm not sure what the else section of your code does Kevin? – Jack Dec 14 '15 at 18:09
1

A different way is to make a bounding box from the line. Simply offset the line by some fixed perpendicular distance. Perhaps you already have logic to detect if the mouse is inside an arbitrary rectangle, but if not, see here: Finding whether a point lies inside a rectangle or not

Community
  • 1
  • 1
John Zwinck
  • 239,568
  • 38
  • 324
  • 436
1

I like barny's suggestion to use the line-point distance, but I assume when you say "line" you mean "line segment". In which case you can't just use a line distance formula, because it might project the mouse position onto a point that is collinear to your line segment, but does not actually lie between the end points.

Here is an alternative implementation that accounts for this.

import math

def magnitude(A):
    return math.sqrt(A.x**2 + A.y**2)

#https://en.wikipedia.org/wiki/Dot_product
def dot_product(A,B):
    return A.x*B.x + A.y*B.y

#the family of vectors parallel to vector B is defined by
#V(f) = B*f
#where f is a scalar value. when f is between 0 and 1, the vector's length is between 0 and B's magnitude.
#this returns the f value of the vector projection of A onto B.
#https://en.wikipedia.org/wiki/Vector_projection
def scalar_projection_ratio(A,B):
    length = dot_product(A,B) / magnitude(B)
    return length / magnitude(B)

#finds the vector with the same angle and magnitude as line segment ab.
def vectorize(a,b):
    return Vector(b.x - a.x, b.y - a.y)

def clamp(x, left, right):
    if x < left: return left
    if x > right: return right
    return x

#finds the point lying between `a` and `b` which is closest to `p`.
def closest_point_to_line_segment(a,b,p):
    B = vectorize(a,b)
    P = vectorize(a,p)

    f = scalar_projection_ratio(P, B)

    #if f is less than 0 or greater than 1, the vector projection of P onto AB does not lie between A and B.
    #so we must clamp it to that range.

    f = clamp(f, 0, 1)
    return Point(a.x + f*B.x, a.y + f*B.y)

def distance_to_line_segment(a,b,p):
    t = closest_point_to_line_segment(a,b,p)
    return magnitude(vectorize(t,p))

def is_over_line(a, b, p):
    return distance_to_line_segment(a,b,p) < 20 #or whatever tolerance is appropriate

This code assumes you have Vector and Point classes, which are just simple containers for x & y values. You could use tuples or whatever other data structure you like, though; it would only require some nominal changes.


Just to verify that I didn't mistype anything in the above code, I used it to make a quick heatmap that shows the distance to the red line segment for each pixel.

enter image description here

The darkest values represent the shortest distance, so this seems to confirm that this approach works.

Kevin
  • 74,910
  • 12
  • 133
  • 166