--[[ Copyright (c) 2011 Matthias Richter Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]]-- local math_abs, math_floor, math_min, math_max = math.abs, math.floor, math.min, math.max local math_sqrt, math_log, math_pi, math_huge = math.sqrt, math.log, math.pi, math.huge local function math_absmin(a,b) return math_abs(a) < math_abs(b) and a or b end module(..., package.seeall) local Class = require(_PACKAGE .. 'class') local vector = require(_PACKAGE .. 'vector') local Polygon = require(_PACKAGE .. 'polygon') _M.class = nil _M.vector = nil _M.polygon = nil local function test_axes(axes, shape_one, shape_two, sep, min_overlap) for _,axis in ipairs(axes) do local l1,r1 = shape_one:projectOn(axis) local l2,r2 = shape_two:projectOn(axis) -- do the intervals overlap? if r1 < l2 or r2 < l1 then return false end -- get the smallest absolute overlap local overlap = math_absmin(l2-r1, r2-l1) if math_abs(overlap) < min_overlap then sep, min_overlap = overlap * axis, math_abs(overlap) end end return true, sep, min_overlap end local function SAT(shape_one, axes_one, shape_two, axes_two) local collide, sep, overlap = false, vector(0,0), math_huge collide, sep, overlap = test_axes(axes_one, shape_one, shape_two, sep, overlap) if not collide then return false end collide, sep = test_axes(axes_two, shape_one, shape_two, sep, overlap) return collide, sep end local function outcircles_intersect(shape_one, shape_two) local x1,y1,r1 = shape_one:outcircle() local x2,y2,r2 = shape_two:outcircle() return (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) <= (r1+r2)*(r1+r2) end -- -- base class -- local Shape = Class{name = 'Shape', function(self, t) self._type = t self._rotation = 0 end} function Shape:moveTo(x,y) local cx,cy = self:center() self:move(x - cx, y - cy) end function Shape:rotation() return self._rotation end function Shape:rotate(angle) self._rotation = self._rotation + angle end function Shape:setRotation(angle, x,y) return self:rotate(angle - self._rotation, x,y) end -- supported shapes Shape.POLYGON = setmetatable({}, {__tostring = function() return 'POLYGON' end}) Shape.COMPOUND = setmetatable({}, {__tostring = function() return 'COMPOUND' end}) Shape.CIRCLE = setmetatable({}, {__tostring = function() return 'CIRCLE' end}) Shape.POINT = setmetatable({}, {__tostring = function() return 'POINT' end}) -- -- class definitions -- local ConvexPolygonShape = Class{name = 'ConvexPolygonShape', function(self, polygon) Shape.construct(self, Shape.POLYGON) assert(polygon:isConvex(), "Polygon is not convex.") self._polygon = polygon end} ConvexPolygonShape:inherit(Shape) local ConcavePolygonShape = Class{name = 'ConcavePolygonShape', function(self, poly) Shape.construct(self, Shape.COMPOUND) self._polygon = poly self._shapes = poly:splitConvex() for i,s in ipairs(self._shapes) do self._shapes[i] = ConvexPolygonShape(s) end end} ConcavePolygonShape:inherit(Shape) function PolygonShape(polygon, ...) -- create from coordinates if needed if type(polygon) == "number" then polygon = Polygon(polygon, ...) else polygon = polygon:clone() end if polygon:isConvex() then return ConvexPolygonShape(polygon) end return ConcavePolygonShape(polygon) end CircleShape = Class{name = 'CircleShape', function(self, cx,cy, radius) Shape.construct(self, Shape.CIRCLE) self._center = vector(cx,cy) self._radius = radius end} CircleShape:inherit(Shape) PointShape = Class{name = 'PointShape', function(self, x,y) Shape.construct(self, Shape.POINT) self._pos = vector(x,y) end} PointShape:inherit(Shape) -- -- collision functions -- function ConvexPolygonShape:getAxes() local axes = {} local vert = self._polygon.vertices for i = 1,#vert do axes[#axes+1] = (vert[i]-vert[(i%#vert)+1]):perpendicular():normalize_inplace() end return axes end function ConvexPolygonShape:projectOn(axis) local vertices = self._polygon.vertices local projection = {} for i = 1,#vertices do projection[i] = vertices[i] * axis -- same as vertices[i]:projectOn(axis) * axis end return math_min(unpack(projection)), math_max(unpack(projection)) end function CircleShape:projectOn(axis) -- v:projectOn(a) * a = v * a (see ConvexPolygonShape) -- therefore: (c +- a*r) * a = c*a +- |a|^2 * r local center = self._center * axis local shift = self._radius * axis:len2() return center - shift, center + shift end -- collision dispatching: -- let circle shape or compund shape handle the collision function ConvexPolygonShape:collidesWith(other) if other._type ~= Shape.POLYGON then local collide, sep = other:collidesWith(self) return collide, sep and -sep end -- else: type is POLYGON, use the SAT if not outcircles_intersect(self, other) then return false end return SAT(self, self:getAxes(), other, other:getAxes()) end function ConcavePolygonShape:collidesWith(other) if other._type == Shape.POINT then return other:collidesWith(self) end if not outcircles_intersect(self, other) then return false end local sep, collide, collisions = vector(0,0), false, 0 for _,s in ipairs(self._shapes) do local status, separating_vector = s:collidesWith(other) collide = collide or status if status then sep, collisions = sep + separating_vector, collisions + 1 end end return collide, sep / collisions end function CircleShape:collidesWith(other) if other._type == Shape.CIRCLE then local d = self._center:dist(other._center) local radii = self._radius + other._radius if d < radii then -- if circles overlap, push it out upwards if d == 0 then return true, radii * vector(0,1) end -- otherwise push out in best direction return true, (radii - d) * (self._center - other._center):normalize_inplace() end return false elseif other._type == Shape.COMPOUND then local collide, sep = other:collidesWith(self) return collide, sep and -sep elseif other._type == Shape.POINT then return other:collidesWith(self) end -- else: other._type == POLYGON if not outcircles_intersect(self, other) then return false end -- retrieve closest edge to center local points = other._polygon.vertices local closest, dist = points[1], (self._center - points[1]):len2() for i = 2,#points do local d = (self._center - points[i]):len2() if d < dist then closest, dist = points[i], d end end local axis = vector(0,1) if dist ~= 0 then axis = (closest - self._center):normalize_inplace() end return SAT(self, {axis}, other, other:getAxes()) end function PointShape:collidesWith(other) if other._type == Shape.POINT then return (self._pos == other._pos), vector(0,0) end return other:contains(self._pos.x, self._pos.y), vector(0,0) end -- -- point location/ray intersection -- function ConvexPolygonShape:contains(x,y) return self._polygon:contains(x,y) end function ConcavePolygonShape:contains(x,y) return self._polygon:contains(x,y) end function CircleShape:contains(x,y) return (vector(x,y) - self._center):len2() < self._radius * self._radius end function PointShape:contains(x,y) return x == self._pos.x and y == self._pos.y end function ConcavePolygonShape:intersectsRay(x,y, dx,dy) return self._polygon:intersectsRay(x,y, dx,dy) end function ConvexPolygonShape:intersectsRay(x,y, dx,dy) return self._polygon:intersectsRay(x,y, dx,dy) end -- circle intersection if distance of ray/center is smaller -- than radius function CircleShape:intersectsRay(x,y, dx,dy) local pc = vector(x,y) - self._center local d = vector(dx,dy) local a = d * d local b = 4 * d * pc local c = pc * pc - self._radius * self._radius local discriminant = b*b - 4*a*c if discriminant < 0 then return false end discriminant = math_sqrt(discriminant) return true, math_min(-b + discriminant, -b - discriminant) / (2*a) end -- point shape intersects ray if it lies on the ray function PointShape:intersectsRay(x,y,dx,dy) local p = self._pos - vector(x,y) local d = vector(dx,dy) local t = p * d / d:len2() return t >= 0, t end -- -- auxiliary -- function ConvexPolygonShape:center() return self._polygon.centroid:unpack() end function ConcavePolygonShape:center() return self._polygon.centroid:unpack() end function CircleShape:center() return self._center:unpack() end function PointShape:center() return self._pos:unpack() end function ConvexPolygonShape:outcircle() local cx,cy = self:center() return cx,cy, self._polygon._radius end function ConcavePolygonShape:outcircle() local cx,cy = self:center() return cx,cy, self._polygon._radius end function CircleShape:outcircle() local cx,cy = self:center() return cx,cy, self._radius end function PointShape:outcircle() return self._pos.x, self._pos.y, 0 end function ConvexPolygonShape:move(x,y) self._polygon:move(x,y) end function ConcavePolygonShape:move(x,y) self._polygon:move(x,y) for _,p in ipairs(self._shapes) do p:move(x,y) end end function CircleShape:move(x,y) self._center = self._center + vector(x,y) end function PointShape:move(x,y) self._pos.x = self._pos.x + x self._pos.y = self._pos.y + y end function ConcavePolygonShape:rotate(angle,cx,cy) Shape.rotate(self, angle) self._polygon:rotate(angle,cx) for _,p in ipairs(self._shapes) do p:rotate(angle, cx and vector(cx,cy) or self._polygon.centroid) end end function ConvexPolygonShape:rotate(angle, cx,cy) Shape.rotate(self, angle) self._polygon:rotate(angle, cx, cy) end function CircleShape:rotate(angle, cx,cy) Shape.rotate(self, angle) if not cx then return end local c = vector(cx,cy) self._center = (self._center - c):rotate_inplace(angle) + c end function PointShape:rotate(angle, cx,cy) Shape.rotate(self, angle) if not cx then return end local c = vector(cx,cy) self._pos = (self._pos - c):rotate_inplace(angle) + c end function ConvexPolygonShape:draw(mode) local mode = mode or 'line' love.graphics.polygon(mode, self._polygon:unpack()) end function ConcavePolygonShape:draw(mode) local mode = mode or 'line' if mode == 'line' then love.graphics.polygon('line', self._polygon:unpack()) else for _,p in ipairs(self._shapes) do love.graphics.polygon(mode, p._polygon:unpack()) end end end function CircleShape:draw(mode, segments) local segments = segments or math_max(3, math_floor(math_pi * math_log(self._radius))) love.graphics.circle(mode, self._center.x, self._center.y, self._radius, segments) end function PointShape:draw() love.graphics.point(self._pos.x, self._pos.y) end