Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active February 15, 2019 06:12
Show Gist options
  • Save nitaku/477578476ee02fe20f637f3f9c7b008e to your computer and use it in GitHub Desktop.
Save nitaku/477578476ee02fe20f637f3f9c7b008e to your computer and use it in GitHub Desktop.
Closest point on segment

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.

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()
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;
}
<!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="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg height="500" width="960"></svg>
<script src="index.js"></script>
</body>
</html>
// 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment