Scurry.Polygon (Scurry v3.0.0)
View SourceFunctions related to polygons and lines relevant for 2D map pathfinding.
This provides functions for;
- line of sight between two points
- classify polygon vertices as concave or vertex
- intersections of lines and polygons
- checking if points are inside/outside polygons
- finding nearest point on a polygon and distances
Polygons are
- A list of vertices,
[{x1, y1}, {x2,y2}, ...]
defined as alist(vector)
. - They must not be closed, ie. last vertex should not be equal to the first.
- They must be in clockwise order in screen coordinates, otherwise convex/concave classification will be inversed as it traverses the egdes.
Order of vertices
Polygons must be defined in clockwise order in screen coordinates. This is necessary for the convex/concave classification. It counter clock, it will be inversed as it traverses the egdes.
Here's a crude drawing of an example of the M shaped polygon used for many tests/docs.
polygon = [{0, 0}, {10, 0}, {20, 0}, {20, 20}, {10, 10}, {0, 20}]
Summary
Types
A vector is represented as a tuple {x, y}
, it's x and y components, both number/0
. These are used for vector math in Scurry.Vector
and as 2D points in Scurry.Polygon
and Scurry.PolygonMap
.
Functions
Check if a specific vertex is concave, convex or neither.
Split a polygon into concave and convex vertices.
Get first intersection of a line with a polygon.
Get all intersections of a line with a polygon including their type.
Checks if a line intersects a polygon.
Checks if a line intersects a polygon.
Check if the polygon is clockwise within screen coordinates.
Check if a specific vertex in a polygon is concave
Check if a specific vertex in a polygon is convex
Check if a point is inside a polygon or not.
The opposite of is_inside?/3
, provided for code readability.
Get last intersection of a line with a polygon.
Find the edge of a polygon nearest a given point.
Find the point on the edge of a polygon nearest a given point.
Types
A line is represented by two vertices of type vector/0
@type polygon() :: [vector()]
A polygon is represented by a list of vector/0
, each being a tuple of {x, y}
coordinates, both number/0
. To be properly defined, the vertices of the polygon must be non-closed and clockwise.
A vector is represented as a tuple {x, y}
, it's x and y components, both number/0
. These are used for vector math in Scurry.Vector
and as 2D points in Scurry.Polygon
and Scurry.PolygonMap
.
Functions
Check if a specific vertex is concave, convex or neither.
Whehter a vertex is concave or convex is defined by it pointing out - it's inner angle is less than 180 means convex and more than 180 means concave.
When testing a vertex, keep this in mind and negate appropriately depending on whether it's the boundary polygon or a hole polygon being tested.
Params
polygon
(polygon/0
) the polygon in which to check a vertex.at
(integer/0
), which vertex in thepolygon
to check.
Returns
:convex
for a convex vertice.:concave
for a concave vertice.:neither
for a vertice that's a straight edge, ie. 180 degrees.
Examples
# A vaguely M shaped polygon
iex> m = [{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}]
[{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}]
iex> Polygon.classify_vertex(m, 0)
:convex
iex> Polygon.classify_vertex(m, 1)
:neither
iex> Polygon.classify_vertex(m, 4)
:concave
Split a polygon into concave and convex vertices.
When doing pathfinding, there will typically be a outer polygon bounding the "world" and multiple inner polygons describing "holes/obstacles".
The path can only be within the outer polygon and has to "walk around" the holes.
Classifying the polygons into concave and convex is used to determine the walkable graph.
- The outer polygon's concave (pointing into the world) vertices are used. used.
- The holes' convex (point out of the hole, into the world) vertices used used.
In code, this looks like
# Get the concave vectices of the bounding polygon
{concave, _convex} = Polygon.classify_vertices(world)
# Get al lthe convex vertices of all the holes
convex = Enum.reduce(holes, [], fn points, acc ->
{_, convex} = Polygon.classify_vertices(points)
acc ++ convex
end)
# The initial walk map is the combined set
vertices = concave ++ convex
Params
polygon
(polygon/0
) the polygon to classify.
Returns
{list of concave vertices, list of convex}
.
- A tuple of two lists of
vector/0
. - The first list are all the concave vertices or
[]
if none - The second list are all the convex vertices or
[]
if none
Note
Three points that fall on the same line ([{0, 0}, {1, 0}, {2, 0}]
) does not
match neither the concave/convex definition (angle greater-than / less-than
180 degrees) this function will discard these via classify_vertex/2
.
Examples
# A vaguely M shaped polygon
iex> Polygon.classify_vertices([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}])
{[{1, 0.5}], [{0, 0}, {2, 0}, {2, 1}, {0, 1}]}
# A square around 0,0
iex> Polygon.classify_vertices([{-1, -1}, {1, -1}, {1, 1}, {-1, 1}])
{[], [{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]}
# A flat line
iex> Polygon.classify_vertices([{0, 0}, {1, 0}, {2, 0}])
{[], []}
Get first intersection of a line with a polygon.
The "opposite" of last_intersection/2
.
Params
polygon
(polygon/0
) a polygon to check for an intersection withline
.line
(line/0
). The line to check for intersections withpolygon
.
A line/0
is a tuple of two vector/0
and the first is considered the head
of the line and "first" in this context means nearest to that point.
Returns
- A
vector/0
indicating whereline
first intersectspolygon
. nil
if there's no intersection.
Examples
# A square around 0,0
iex> square = [{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
[{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
iex> Polygon.first_intersection(square, {{0, -2}, {0, 2}})
{0.0, -1.0}
iex> Polygon.first_intersection(square, {{0, 2}, {0, -2}})
{0.0, 1.0}
iex> Polygon.first_intersection(square, {{2, -2}, {2, 2}})
nil
Get all intersections of a line with a polygon including their type.
This function basically calls Geo.line_segment_intersection/2
for every segment
of the polygon
against the line
and filters the results to only include
the list of intersection points.
Params
polygon
(polygon/0
), the polygon to check.line
(line/0
), the line check for intersections againstpolygon
.
Options
:allow_points
(defaultfalse
) whether aon_point
intersection should be considered an intersection or not. This varies from use cases. Eg. when building a polygon, points will be connected and thus intersect iftrue
. This may not be the desired result, sofalse
won't consider points intersections.
Returns
- a list of
vector/0
indicating where the line intersects []
if there's no intersections.
Examples
iex> polygon = [{0, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}]
[{0, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}]
iex> line = {{1, -1}, {1, 3}}
{{1, -1}, {1, 3}}
iex> Polygon.intersections(polygon, line)
[{1.0, 0.0}]
iex> Polygon.intersections(polygon, line, allow_points: true)
[{1.0, 0.0}, {1.0, 0.5}]
@spec intersects(polygon(), line()) :: :nointersection | :on_segment | {:intersection, vector()} | {:poiont_intersection, vector()}
Checks if a line intersects a polygon.
Find an intersection (if any) between polygon
and line
. For most cases
using intersections/3
will be a better choice to get intersections.
Params
polygon
(polygon/0
), the polygon to check.line
(line/0
), the line to check if it intersectspolygon
.
Returns
This relates to Scurry.Geo.line_segment_intersection/2
.
:nointersection
,line
does not intersectpolygon
{:point_intersection, vector}
{;intersection, line}
:on_segment
Examples
# A vaguely M shaped polygon
iex> Polygon.intersects([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}], {{1, -0.5}, {1, 0.5}})
{:point_intersection, {1.0, 0.0}}
iex> Polygon.intersects([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}], {{0.5, -0.5}, {0.5, 0.5}})
{:intersection, {0.5, 0.0}}
iex> Polygon.intersects([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}], {{0.5, -1}, {0.5, 0}})
{:point_intersection, {0.5, 0.0}}
iex> Polygon.intersects([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}], {{0.5, -2}, {0.5, -1}})
:nointersection
iex> Polygon.intersects([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}], {{2, 0.1}, {2, 0.9}})
:on_segment
Checks if a line intersects a polygon.
This is a boolean version of intersects/2
. For most cases using
intersections/3
will be a better choice to get intersections.
Params
polygon
(polygon/0
), the polygon to check.line
(line/0
), the line to check if it intersectspolygon
.
Returns
true
or false
wether the line intersects the polygon or not.
Examples
# A vaguely M shaped polygon
iex> Polygon.intersects?([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}], {{1, -0.5}, {1, 0.5}})
true
iex> Polygon.intersects?([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}], {{1, -1}, {1, 0}})
true
iex> Polygon.intersects?([{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}], {{1, -2}, {1, -1}})
false
Check if the polygon is clockwise within screen coordinates.
This is provided as a utility to check polygons eg. if loading them from data files. The library internally does not use this to validate polygons to avoid a runtime penalty.
Params
polygon
(polygon/0
) the polygon to check to check.
Returrns
true
if the points in the polygon are defined in a clockwise orientation.
Examples
iex> Polygon.is_clockwise?([{0, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}])
true
iex> Polygon.is_clockwise?([{0, 0}, {0, 1}, {1, 0.5}, {2, 1}, {2, 0}])
false
Check if a specific vertex in a polygon is concave
Params
polygon
(polygon/0
) the polygon in which to check a vertex.at
(integer/0
), which vertex in thepolygon
to check.
Returns
true
if the vertex is concave, otherwise false
See also classify_vertex/2
.
Examples
# A vaguely M shaped polygon
iex> m = [{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}]
[{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}]
iex> Polygon.is_concave?(m, 0)
false
iex> Polygon.is_concave?(m, 1)
false
iex> Polygon.is_concave?(m, 2)
false
iex> Polygon.is_concave?(m, 3)
false
iex> Polygon.is_concave?(m, 4)
true
iex> Polygon.is_concave?(m, 5)
false
Check if a specific vertex in a polygon is convex
Params
polygon
(polygon/0
) the polygon in which to check a vertex.at
(integer/0
), which vertex in thepolygon
to check.
Returns
true
if the vertex is convex, otherwise false
See also classify_vertex/2
.
Examples
# A vaguely M shaped polygon
iex> m = [{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}]
[{0, 0}, {1, 0}, {2, 0}, {2, 1}, {1, 0.5}, {0, 1}]
iex> Polygon.is_convex?(m, 0)
true
iex> Polygon.is_convex?(m, 1)
false
iex> Polygon.is_convex?(m, 2)
true
iex> Polygon.is_convex?(m, 3)
true
iex> Polygon.is_convex?(m, 4)
false
iex> Polygon.is_convex?(m, 5)
true
Check if a point is inside a polygon or not.
Params
polygon
(polygon/0
) the polygon to check against.point
(vector/0
) the point to check if is inside (or on)polygon
.
Options
allow_border
, defaults totrue
and allowspoint
to be thepolygon
edges.
Returns
true
is thepoint
is inside thepolygon
true
is thepoint
is onpolygon
edges ifallow_border
istrue
.
Examples
# A square around 0,0
iex> square = [{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
[{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
iex> Polygon.is_inside?(square, {0, 0})
true
iex> Polygon.is_inside?(square, {-1, 0})
true
iex> Polygon.is_inside?(square, {-1, 0}, [allow_border: false])
false
iex> Polygon.is_inside?(square, {-2, 0})
false
The opposite of is_inside?/3
, provided for code readability.
See is_inside?/3
for behaviour and imagine the opposite.
Get last intersection of a line with a polygon.
The "opposite" of first_intersection/2
.
Params
polygon
(polygon/0
) a polygon to check for an intersection withline
.line
(line/0
). The line to check for intersections withpolygon
.
A line/0
is a tuple of two vector/0
and the last is considered the tail
of the line and "last" in this context means nearest to that point.
Returns
- A
vector/0
indicating whereline
last intersectspolygon
nil
if there's no intersection.
Examples
# A square around 0,0
iex> square = [{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
[{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
iex> Polygon.last_intersection(square, {{0, -2}, {0, 2}})
{0.0, 1.0}
iex> Polygon.last_intersection(square, {{0, 2}, {0, -2}})
{0.0, -1.0}
iex> Polygon.last_intersection(square, {{2, -2}, {2, 2}})
nil
Find the edge of a polygon nearest a given point.
Given a point
that's inside or outside a given polygon
, this checks each
segment of the polygon, and returns the nearest one.
If multiple lines are "nearest", the first in the polygon is picked.
Params
polygon
(polygon/0
) the polygion to find the nearest edge on.point
(vector/0
) the point to find the nearest edge to.
Returns
A line/0
that is a segment of polygon
that is closest to point
.
Examples
# A square around 0,0
iex> square = [{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
[{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
iex> Polygon.nearest_edge(square, {0, 0})
{{-1, -1}, {1, -1}}
iex> Polygon.nearest_edge(square, {0.9, 0})
{{1, -1}, {1, 1}}
iex> Polygon.nearest_edge(square, {1.1, 0})
{{1, -1}, {1, 1}}
Find the point on the edge of a polygon nearest a given point.
Given a point
that's inside or outside a given polygon
, this uses
nearest_edge/2
to find the closest edge and then computes the point on the
edge nearest the given point
.
Params
polygon
(polygon/0
) the polygion to find the nearest point on.point
(vector/0
) the point to find the nearest point to.
Returns
A vector/0
that is a point on polygon
that is closest to point
.
Examples
# A square around 0,0
iex> square = [{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
[{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]
iex> Polygon.nearest_point_on_edge(square, {0, 0})
{0.0, -1.0}
iex> Polygon.nearest_point_on_edge(square, {0.9, 0})
{1.0, 0.0}
iex> Polygon.nearest_point_on_edge(square, {1.1, 0})
{1.0, 0.0}