-module(ow_vector). % @doc Vector math and other goodies % Particularly from: % https://github.com/JuantAldea/Separating-Axis-Theorem/blob/master/python/separation_axis_theorem.py -export([ add/2, rotate/2, length_squared/1, dot/2, normalize/1, orthogonal/1, edge_direction/2, vertices_to_edges/1, project/2, overlap/2, translate/2, is_collision/2, aabb/1, test/0 ]). -type vector() :: {scalar(), scalar()}. -type scalar() :: number(). -export_type([vector/0, scalar/0]). -spec add(vector(), vector()) -> vector(). add({X1, Y1}, {X2, Y2}) -> {X1 + X2, Y1 + Y2}. -spec rotate(vector(), scalar()) -> vector(). rotate({X, Y}, RotRad) -> CR = math:cos(RotRad), SR = math:sin(RotRad), {CR * X - SR * Y, SR * X + CR * Y}. -spec length_squared(vector()) -> float(). length_squared({X, Y}) -> math:pow(X, 2) + math:pow(Y, 2). -spec dot(vector(), vector()) -> scalar(). dot({X1, Y1}, {X2, Y2}) -> X1 * X2 + Y1 * Y2. -spec normalize(vector()) -> vector(). normalize({X1, Y1}) -> N = math:sqrt( math:pow(X1, 2) + math:pow(Y1, 2) ), {X1 / N, Y1 / N}. -spec orthogonal(vector()) -> vector(). orthogonal({X1, Y1}) -> % A vector orthogonal to the input vector {-Y1, X1}. -spec edge_direction(vector(), vector()) -> vector(). edge_direction({X1, Y1}, {X2, Y2}) -> % A vector pointing from V1 to V2 {X2 - X1, Y2 - Y1}. -spec vertices_to_edges([vector(), ...]) -> [vector(), ...]. vertices_to_edges(Vertices = [First | _Rest]) -> % A list of the edges of the vertices as vectors vertices_to_edges(Vertices, First, []). vertices_to_edges([Last], First, Acc) -> [edge_direction(Last, First) | Acc]; vertices_to_edges([V1, V2 | Rest], First, Acc) -> E = edge_direction(V1, V2), vertices_to_edges([V2 | Rest], First, [E | Acc]). -spec project([vector(), ...], vector()) -> [scalar(), ...]. project(Vertices, Axis) -> % A vector showing how much of the vertices lies along the axis Dots = [dot(Vertex, Axis) || Vertex <- Vertices], [lists:min(Dots), lists:max(Dots)]. -spec overlap([scalar(), ...], [scalar(), ...]) -> boolean(). overlap(Projection1, Projection2) -> Min1 = lists:min(Projection1), Min2 = lists:min(Projection2), Max1 = lists:max(Projection1), Max2 = lists:max(Projection2), (Min1 =< Max2) and (Min2 =< Max1). is_collision(Object1, Object2) -> Edges = vertices_to_edges(Object1) ++ vertices_to_edges(Object2), Axes = [normalize(orthogonal(Edge)) || Edge <- Edges], Overlaps = [detect_overlaps(Object1, Object2, Axis) || Axis <- Axes], lists:foldl(fun(Next, SoFar) -> Next and SoFar end, true, Overlaps). detect_overlaps(Object1, Object2, Axis) -> ProjA = project(Object1, Axis), ProjB = project(Object2, Axis), overlap(ProjA, ProjB). % Create an axis-aligned bounding box for the entity. This is NOT the minimum % bounding box, but is cheaper to calculate. It also must be recalculated for % every rotation of the object. aabb(Vertices) -> XList = [X || {X, _} <- Vertices], YList = [Y || {_, Y} <- Vertices], % Axis-aligned bounding box. [ {lists:min(XList), lists:min(YList)}, {lists:max(XList), lists:min(YList)}, {lists:min(XList), lists:max(YList)}, {lists:max(XList), lists:max(YList)} ]. translate(Object, {Xnew, Ynew}) -> [{X + Xnew, Y + Ynew} || {X, Y} <- Object]. test() -> A = [{0, 0}, {70, 0}, {0, 70}], B = [{70, 70}, {150, 70}, {70, 150}], C = [{30, 30}, {150, 70}, {70, 150}], [ is_collision(A, B), is_collision(A, C), is_collision(B, C) ].