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.
Last active
February 15, 2019 06:12
-
-
Save nitaku/477578476ee02fe20f637f3f9c7b008e to your computer and use it in GitHub Desktop.
Closest point on segment
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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