block by nitaku 477578476ee02fe20f637f3f9c7b008e

Closest point on segment

Full Screen

This example shows an algorithm to find the closest point on a segment, given another point. The code is almost completely taken from the answer by Justin L. to this stackoverflow post.

index.js

// Generated by CoffeeScript 1.10.0
(function() {
  var closest, drag, get_closest, height, lines_data, lines_layer, perpendicular, points_data, points_layer, redraw, result_layer, svg, width;

  get_closest = function(A, B, P) {
    var C, a_to_b, a_to_p, atb2, atp_dot_atb, maxx, maxy, minx, miny, ref, ref1, t;
    a_to_p = [P.x - A.x, P.y - A.y];
    a_to_b = [B.x - A.x, B.y - A.y];
    atb2 = a_to_b[0] * a_to_b[0] + a_to_b[1] * a_to_b[1];
    atp_dot_atb = a_to_p[0] * a_to_b[0] + a_to_p[1] * a_to_b[1];
    t = atp_dot_atb / atb2;
    C = {
      x: A.x + a_to_b[0] * t,
      y: A.y + a_to_b[1] * t
    };
    minx = Math.min(A.x, B.x);
    maxx = Math.max(A.x, B.x);
    miny = Math.min(A.y, B.y);
    maxy = Math.max(A.y, B.y);
    if ((minx <= (ref = C.x) && ref <= maxx) && (miny <= (ref1 = C.y) && ref1 <= maxy)) {
      return C;
    } else {
      return null;
    }
  };

  points_data = [
    {
      x: -200,
      y: 200
    }, {
      x: 200,
      y: -200
    }, {
      x: 200,
      y: 200
    }
  ];

  lines_data = [
    {
      start: points_data[0],
      end: points_data[1]
    }
  ];

  svg = d3.select('svg');

  width = svg.node().getBoundingClientRect().width;

  height = svg.node().getBoundingClientRect().height;

  svg.attr({
    viewBox: (-width / 2) + " " + (-height / 2) + " " + width + " " + height
  });

  drag = d3.behavior.drag().origin(function(d) {
    return d;
  });

  drag.on('drag', function(d) {
    d.x = d3.event.x;
    d.y = d3.event.y;
    return redraw();
  });

  lines_layer = svg.append('g');

  points_layer = svg.append('g');

  result_layer = svg.append('g');

  closest = result_layer.append('circle').attr({
    "class": 'closest',
    r: 4
  });

  perpendicular = lines_layer.append('line').attr({
    "class": 'perpendicular'
  });

  redraw = function() {
    var closest_data, lines, points;
    lines = lines_layer.selectAll('.line').data(lines_data);
    lines.enter().append('line').attr({
      "class": 'line'
    });
    lines.attr({
      x1: function(l) {
        return l.start.x;
      },
      y1: function(l) {
        return l.start.y;
      },
      x2: function(l) {
        return l.end.x;
      },
      y2: function(l) {
        return l.end.y;
      }
    });
    points = points_layer.selectAll('.point').data(points_data);
    points.enter().append('circle').call(drag).attr({
      "class": 'point',
      r: 8
    });
    points.attr({
      cx: function(p) {
        return p.x;
      },
      cy: function(p) {
        return p.y;
      }
    });
    closest_data = get_closest(points_data[0], points_data[1], points_data[2]);
    closest.attr({
      cx: closest_data != null ? closest_data.x : 0,
      cy: closest_data != null ? closest_data.y : 0,
      display: closest_data != null ? 'inline' : 'none'
    });
    return perpendicular.attr({
      x1: points_data[2].x,
      y1: points_data[2].y,
      x2: closest_data != null ? closest_data.x : 0,
      y2: closest_data != null ? closest_data.y : 0,
      display: closest_data != null ? 'inline' : 'none'
    });
  };

  redraw();

}).call(this);

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Closest point on segment</title>
    <link type="text/css" href="index.css" rel="stylesheet"/>
    <script src="//d3js.org/d3.v3.min.js"></script>
  </head>
  <body>
    <svg height="500" width="960"></svg>
    <script src="index.js"></script>
  </body>
</html>

index.coffee

get_closest = (A, B, P) ->
  a_to_p = [P.x - A.x, P.y - A.y]     # vector A->P
  a_to_b = [B.x - A.x, B.y - A.y]     # vector A->B

  atb2 = a_to_b[0]*a_to_b[0] + a_to_b[1]*a_to_b[1] # squared magnitude of a_to_b

  atp_dot_atb = a_to_p[0]*a_to_b[0] + a_to_p[1]*a_to_b[1] # The dot product of a_to_p and a_to_b

  t = atp_dot_atb / atb2              # The normalized "distance" from a to your closest point
  
  C = {x: A.x + a_to_b[0]*t, y: A.y + a_to_b[1]*t} # Add the distance to A, moving towards B
  
  # [optional] check if the point falls onto the segment
  minx = Math.min A.x, B.x
  maxx = Math.max A.x, B.x
  
  miny = Math.min A.y, B.y
  maxy = Math.max A.y, B.y
  
  if minx <= C.x <= maxx and miny <= C.y <= maxy
    return C
  else
    return null


points_data = [{x: -200, y: 200},{x: 200, y: -200},{x: 200, y: 200}]
lines_data = [{start: points_data[0], end: points_data[1]}]

svg = d3.select('svg')
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height

# translate the viewBox to have (0,0) at the center of the vis
svg
  .attr
    viewBox: "#{-width/2} #{-height/2} #{width} #{height}"
    
# define a drag behavior
drag = d3.behavior.drag()
  .origin((d) -> d)
  
drag.on 'drag', (d) ->
  # update the datum of the dragged node
  d.x = d3.event.x
  d.y = d3.event.y
  
  # update the representation
  redraw()
  
lines_layer = svg.append 'g'
points_layer = svg.append 'g'
result_layer = svg.append 'g'

closest = result_layer.append('circle')
  .attr
    class: 'closest'
    r: 4
    
perpendicular = lines_layer.append('line')
  .attr
    class: 'perpendicular'
  
redraw = () ->
  lines = lines_layer.selectAll('.line')
    .data(lines_data)
    
  lines.enter().append('line')
    .attr
      class: 'line'
  
  lines
    .attr
      x1: (l) -> l.start.x
      y1: (l) -> l.start.y
      x2: (l) -> l.end.x
      y2: (l) -> l.end.y
      
      
  points = points_layer.selectAll('.point')
    .data(points_data)
    
  points.enter().append('circle')
    .call(drag)
    .attr
      class: 'point'
      r: 8
      
  points
    .attr
      cx: (p) -> p.x
      cy: (p) -> p.y
      
  closest_data = get_closest(points_data[0], points_data[1], points_data[2])
  closest
    .attr
      cx: if closest_data? then closest_data.x else 0
      cy: if closest_data? then closest_data.y else 0
      display: if closest_data? then 'inline' else 'none'
        
  perpendicular
    .attr
      x1: points_data[2].x
      y1: points_data[2].y
      x2: if closest_data? then closest_data.x else 0
      y2: if closest_data? then closest_data.y else 0
      display: if closest_data? then 'inline' else 'none'
    
redraw()

index.css

svg {
  background: white;
}
.point {
  fill: #DDD;
  stroke: gray;
  stroke-width: 2;
}
.line, .perpendicular {
  stroke: #DDD;
  stroke-width: 2;
}
.closest {
  pointer-events: none;
}
.perpendicular {
  stroke-dasharray: 3 3;
}