If you just want to see if the mouse is near a line segment, you don't need to know exactly where the pixels are - you just need to know if they are logically within a certain distance.
Here's a little class I knocked together. It just uses the normal formula for a line y = mx+c
to calculate if any particular point is within a certain distance (tolerance) of the line.
Given two points, p1
and p2
that are the coords of the endpoints of a line you want to hit-test, you would initialise it like this:
var hitTest = new LineIntersectionChecker(p1, p2);
Then check if another point, p
is on the line like this:
if (hitTest.IsOnLine(p))
...
The class implementation:
public sealed class LineIntersectionChecker
{
private readonly PointF _p1;
private readonly PointF _p2;
private readonly double _slope;
private readonly double _yIntersect;
private readonly double _tolerance;
private readonly double _x1;
private readonly double _x2;
private readonly double _y1;
private readonly double _y2;
private readonly bool _isHorizontal;
private readonly bool _isVertical;
public LineIntersectionChecker(PointF p1, PointF p2, double tolerance = 1.0)
{
_p1 = p1;
_p2 = p2;
_tolerance = tolerance;
_isVertical = (Math.Abs(p1.X - p2.X) < 0.01);
_isHorizontal = (Math.Abs(p1.Y - p2.Y) < 0.01);
if (_isVertical)
{
_slope = double.NaN;
_yIntersect = double.NaN;
}
else // Useable.
{
_slope = (p1.Y - p2.Y)/(double) (p1.X - p2.X);
_yIntersect = p1.Y - _slope * p1.X ;
}
if (_p1.X < _p2.X)
{
_x1 = _p1.X - _tolerance;
_x2 = _p2.X + _tolerance;
}
else
{
_x1 = _p2.X - _tolerance;
_x2 = _p1.X + _tolerance;
}
if (_p1.Y < _p2.Y)
{
_y1 = _p1.Y - _tolerance;
_y2 = _p2.Y + _tolerance;
}
else
{
_y1 = _p2.Y - _tolerance;
_y2 = _p1.Y + _tolerance;
}
}
public bool IsOnLine(PointF p)
{
if (!inRangeX(p.X) || !inRangeY(p.Y))
return false;
if (_isHorizontal)
return inRangeY(p.Y);
if (_isVertical)
return inRangeX(p.X);
double expectedY = p.X*_slope + _yIntersect;
return (Math.Abs(expectedY - p.Y) <= _tolerance);
}
private bool inRangeX(double x)
{
return (_x1 <= x) && (x <= _x2);
}
private bool inRangeY(double y)
{
return (_y1 <= y) && (y <= _y2);
}
}
You use it by instantiating it with the points at either end of the line that you want to hit-test, and then call IsOnLine(p)
for each point you want to check against the line.
You would get the points to check from MouseMove or MouseDown messages.
Note that you can set a different tolerance in the constructor. I defaulted it to 1 because "within 1 pixel" seems a reasonable default.
Here's the code I tested it with:
double m = 0.5;
double c = 1.5;
Func<double, float> f = x => (float)(m*x + c);
Random rng = new Random();
PointF p1 = new PointF(-1000, f(-1000));
PointF p2 = new PointF(1000, f(1000));
var intersector = new LineIntersectionChecker(p1, p2, 0.1);
Debug.Assert(intersector.IsOnLine(new PointF(0f, 1.5f)));
for (int i = 0; i < 1000; ++i)
{
float x = rng.Next((int)p1.X+2, (int)p2.X-2);
PointF p = new PointF(x, f(x));
Debug.Assert(intersector.IsOnLine(p));
}