"There are better ways to implement lineAtPoint:"


#1

For fun, here’s one “better” way to implement lineAtPoint: (as mentioned in the section “Detecting Taps with UITapGestureRecognizer”).

The math comes from paulbourke.net/geometry/pointlineplane. (Note the stackoverflow answer contains a small typo in the first function, which it erroneously calls sqrtf.)

In TouchDrawView.m:

static CGFloat BNRLineAtPointMaxDistance = 20.0;

- (Line *)lineAtPoint:(CGPoint)p
{
  Line *closestLine = nil;
  CGFloat closestDistance;
  for (Line *line in _completeLines) {
    CGFloat distance = BNR_distanceToSegment(p, [line begin], [line end]);
    if (!closestLine || distance < closestDistance) {
      closestLine = line;
      closestDistance = distance;
    }
  }
  if (closestLine && closestDistance <= BNRLineAtPointMaxDistance) {
    return closestLine;
  } else {
    return nil;
  }
}

CGFloat BNR_sqr(CGFloat x) {
  return x * x;
}

CGFloat BNR_dist2(CGPoint v, CGPoint w)
{
  return BNR_sqr(v.x - w.x) + BNR_sqr(v.y - w.y);
}

CGFloat BNR_distanceToSegment(CGPoint p, CGPoint v, CGPoint w)
{
  CGFloat l2 = BNR_dist2(v, w);
  if (l2 == 0.0) {
    return sqrtf(BNR_dist2(p, v));
  }
  
  CGFloat t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
  if (t < 0.0) {
    return sqrtf(BNR_dist2(p, v));
  }
  if (t > 1.0) {
    return sqrtf(BNR_dist2(p, w));
  }
  return sqrtf(BNR_dist2(p, CGPointMake(v.x + t * (w.x - v.x), v.y + t * (w.y - v.y))));
}

#2

I didn’t really test this.

- (CGPoint)projectPoint:(CGPoint)p
{
      float dx = [self begin].x - [self end].x;
      float dy = [self begin].y - [self end].y;
      float len = dx * dx + dy * dy;
      float dotProd = dx * (p.x - [self end].x) + dy * (p.y - [self end].y);
      
      return CGPointMake([self end].x + dx * (dotProd / len), [self end].y + dy * (dotProd / len));
}

- (float)distanceFromPoint:(CGPoint)p
{
     CGPoint proj = [self projectPoint:p];
     return sqrtf(pow(proj.x - p.x, 2) + pow(proj.y - p.y, 2));
}

#3

[quote=“JoeConway”]I didn’t really test this.
[/quote]

Sweet! Needs bounds checking, though, since we’re dealing with line segments rather than lines. Otherwise you end up with a situation like this, where the projection of the point is close, but the line segment itself is not close:

(The menu shows where I tapped.)

Hence the ugly factorization of the code I stole from stackoverflow.

I totally stole your use of pow(…, 2), though. Forgot about that, and it makes the code slightly less ugly.


#4

Makes sense, you can clamp dotProd/len to 0…1.

#define CLAMP(min, val, max) (val < min ? min : (val > max ? max : val))

 - (CGPoint)projectPoint:(CGPoint)p
{
      float dx = [self begin].x - [self end].x;
      float dy = [self begin].y - [self end].y;
      float len = dx * dx + dy * dy;
      float dotProd = dx * (p.x - [self end].x) + dy * (p.y - [self end].y);
      
      float t = CLAMP(0, dotProd / len, 1);
      return CGPointMake([self end].x + t *dx, [self end].y + t * dy);
}

#5

It is all a matter of taste, but I think it might be more elegant to move the distance calculation to the “Line”-class. The “TouchDrawView”-class is already complicated enough.

[code]- (CGFloat)getDistanceFromPoint:(CGPoint)def
{
// length components of line
CGFloat lengthX = end.x - begin.x;
CGFloat lengthY = end.y - begin.y;

// calculate lambda
CGFloat lambda = lengthX * (def.x - begin.x) + lengthY * (def.y - begin.y);
lambda /= lengthX * lengthX + lengthY * lengthY;

// if lambda < 0 or if lambda > 1 we are beyond the ends of the line
CGFloat clampedLambda = lambda;
if(clampedLambda < 0.0) clampedLambda = 0.0;
if(clampedLambda > 1.0) clampedLambda = 1.0;

// calculate closest point on the line
closest.x = begin.x + clampedLambda * lengthX;
closest.y = begin.y + clampedLambda * lengthY;

// finally calculate closest distance between point and line
return hypot(closest.x - def.x, closest.y - def.y);

}
[/code]