HC/shapes.lua

394 lines
11 KiB
Lua
Raw Normal View History

2011-01-13 13:38:36 +00:00
--[[
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
2011-01-18 17:55:51 +00:00
module(..., package.seeall)
local Class = require(_PACKAGE .. 'class')
local vector = require(_PACKAGE .. 'vector')
local Polygon = require(_PACKAGE .. 'polygon')
2011-02-06 14:03:21 +00:00
_M.class = nil
_M.vector = nil
_M.polygon = nil
2011-01-13 13:38:36 +00:00
2011-01-14 22:25:43 +00:00
local function test_axes(axes, shape_one, shape_two, sep, min_overlap)
for _,axis in ipairs(axes) do
2011-01-13 13:38:36 +00:00
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
2011-01-13 13:38:36 +00:00
-- 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)
2011-01-13 13:38:36 +00:00
end
end
2011-01-14 22:25:43 +00:00
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
2011-01-14 22:25:43 +00:00
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)
2011-01-14 22:25:43 +00:00
return collide, sep
2011-01-13 13:38:36 +00:00
end
2011-02-06 16:40:06 +00:00
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
2011-01-13 13:38:36 +00:00
--
local Shape = Class{name = 'Shape', function(self, t)
2011-01-13 13:38:36 +00:00
self._type = t
end}
2011-02-01 22:41:28 +00:00
function Shape:moveTo(x,y)
2011-02-07 16:51:51 +00:00
local cx,cy = self:center()
self:move(x - cx, y - cy)
2011-02-01 22:41:28 +00:00
end
2011-01-13 13:38:36 +00:00
-- 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})
2011-02-26 16:27:04 +00:00
Shape.POINT = setmetatable({}, {__tostring = function() return 'POINT' end})
2011-01-13 13:38:36 +00:00
2011-02-06 16:40:06 +00:00
--
-- class definitions
2011-01-13 13:38:36 +00:00
--
local ConvexPolygonShape = Class{name = 'ConvexPolygonShape', function(self, polygon)
2011-01-13 13:38:36 +00:00
Shape.construct(self, Shape.POLYGON)
assert(polygon:isConvex(), "Polygon is not convex.")
self._polygon = polygon
end}
ConvexPolygonShape:inherit(Shape)
2011-01-13 13:38:36 +00:00
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, ...)
2011-01-16 16:18:47 +00:00
else
polygon = polygon:clone()
end
if polygon:isConvex() then
return ConvexPolygonShape(polygon)
end
return ConcavePolygonShape(polygon)
end
2011-02-06 16:40:06 +00:00
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)
2011-02-26 16:27:04 +00:00
PointShape = Class{name = 'PointShape', function(self, x,y)
Shape.construct(self, Shape.POINT)
self._pos = vector(x,y)
end}
PointShape:inherit(Shape)
2011-02-06 16:40:06 +00:00
--
-- collision functions
--
function ConvexPolygonShape:getAxes()
2011-01-13 13:38:36 +00:00
local axes = {}
local vert = self._polygon.vertices
for i = 1,#vert do
axes[#axes+1] = (vert[i]-vert[(i%#vert)+1]):perpendicular():normalize_inplace()
2011-01-13 13:38:36 +00:00
end
return axes
end
function ConvexPolygonShape:projectOn(axis)
2011-01-13 13:38:36 +00:00
local vertices = self._polygon.vertices
2011-01-14 22:25:43 +00:00
local projection = {}
2011-01-13 13:38:36 +00:00
for i = 1,#vertices do
2011-01-14 22:25:43 +00:00
projection[i] = vertices[i] * axis -- same as vertices[i]:projectOn(axis) * axis
2011-01-13 13:38:36 +00:00
end
return math_min(unpack(projection)), math_max(unpack(projection))
2011-01-13 13:38:36 +00:00
end
2011-02-06 16:40:06 +00:00
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)
2011-01-13 13:38:36 +00:00
if other._type ~= Shape.POLYGON then
local collide, sep = other:collidesWith(self)
return collide, sep and -sep
2011-01-13 13:38:36 +00:00
end
-- else: type is POLYGON, use the SAT
2011-02-06 16:40:06 +00:00
if not outcircles_intersect(self, other) then return false end
2011-01-14 22:25:43 +00:00
return SAT(self, self:getAxes(), other, other:getAxes())
2011-01-13 13:38:36 +00:00
end
function ConcavePolygonShape:collidesWith(other)
2011-02-26 16:27:04 +00:00
if other._type == Shape.POINT then
return other:collidesWith(self)
end
2011-02-06 16:40:06 +00:00
if not outcircles_intersect(self, other) then return false end
2011-01-14 22:25:43 +00:00
local sep, collide, collisions = vector(0,0), false, 0
2011-01-13 13:38:36 +00:00
for _,s in ipairs(self._shapes) do
local status, separating_vector = s:collidesWith(other)
collide = collide or status
if status then
2011-01-14 22:25:43 +00:00
sep, collisions = sep + separating_vector, collisions + 1
2011-01-13 13:38:36 +00:00
end
end
2011-01-14 22:25:43 +00:00
return collide, sep / collisions
2011-01-13 13:38:36 +00:00
end
function CircleShape:collidesWith(other)
if other._type == Shape.CIRCLE then
2011-01-14 22:25:43 +00:00
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()
2011-01-14 22:25:43 +00:00
end
return false
2011-01-13 13:38:36 +00:00
elseif other._type == Shape.COMPOUND then
local collide, sep = other:collidesWith(self)
return collide, sep and -sep
2011-02-26 16:27:04 +00:00
elseif other._type == Shape.POINT then
return other:collidesWith(self)
2011-01-13 13:38:36 +00:00
end
2011-02-06 16:40:06 +00:00
2011-01-13 13:38:36 +00:00
-- else: other._type == POLYGON
2011-02-06 16:40:06 +00:00
if not outcircles_intersect(self, other) then return false end
2011-01-13 13:38:36 +00:00
-- retrieve closest edge to center
2011-01-14 22:25:43 +00:00
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
2011-01-13 13:38:36 +00:00
end
local axis = vector(0,1)
if dist ~= 0 then axis = (closest - self._center):normalize_inplace() end
return SAT(self, {axis}, other, other:getAxes())
2011-01-13 13:38:36 +00:00
end
2011-02-26 16:27:04 +00:00
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
2011-02-06 16:40:06 +00:00
--
-- 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)
2011-02-07 16:42:48 +00:00
return (vector(x,y) - self._center):len2() < self._radius * self._radius
2011-02-06 16:40:06 +00:00
end
2011-02-26 16:27:04 +00:00
function PointShape:contains(x,y)
return x == self._pos.x and y == self._pos.y
end
2011-02-06 23:08:02 +00:00
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)
2011-02-07 16:42:48 +00:00
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)
2011-02-06 23:08:02 +00:00
end
2011-02-26 16:27:04 +00:00
-- 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
2011-02-26 16:27:04 +00:00
end
2011-02-06 16:40:06 +00:00
--
-- auxiliary
--
function ConvexPolygonShape:center()
return self._polygon.centroid:unpack()
end
function ConcavePolygonShape:center()
return self._polygon.centroid:unpack()
2011-01-13 13:38:36 +00:00
end
function CircleShape:center()
2011-01-13 13:38:36 +00:00
return self._center:unpack()
end
2011-02-26 16:27:04 +00:00
function PointShape:center()
return self._pos:unpack()
end
2011-02-06 16:40:06 +00:00
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
2011-02-26 16:27:04 +00:00
function PointShape:outcircle()
return self._pos.x, self._pos.y, 0
end
2011-02-06 16:40:06 +00:00
function ConvexPolygonShape:move(x,y)
2011-02-07 16:42:48 +00:00
self._polygon:move(x,y)
2011-02-06 16:40:06 +00:00
end
function ConcavePolygonShape:move(x,y)
2011-02-07 16:42:48 +00:00
self._polygon:move(x,y)
2011-02-06 16:40:06 +00:00
for _,p in ipairs(self._shapes) do
2011-02-07 16:42:48 +00:00
p:move(x,y)
2011-02-06 16:40:06 +00:00
end
end
2011-01-13 13:38:36 +00:00
function CircleShape:move(x,y)
2011-02-07 16:42:48 +00:00
self._center = self._center + vector(x,y)
2011-01-13 13:38:36 +00:00
end
2011-02-26 16:27:04 +00:00
function PointShape:move(x,y)
self._pos.x = self._pos.x + x
self._pos.y = self._pos.y + y
end
2011-02-06 16:40:06 +00:00
function ConcavePolygonShape:rotate(angle,cx,cy)
self._polygon:rotate(angle,cx)
for _,p in ipairs(self._shapes) do
2011-02-07 16:42:48 +00:00
p:rotate(angle, cx and vector(cx,cy) or self._polygon.centroid)
2011-02-06 16:40:06 +00:00
end
end
function ConvexPolygonShape:rotate(angle, cx,cy)
2011-02-07 16:42:48 +00:00
self._polygon:rotate(angle, cx, cy)
2011-02-06 16:40:06 +00:00
end
2011-01-14 22:25:43 +00:00
function CircleShape:rotate(angle, cx,cy)
if not cx then return end
2011-02-07 16:42:48 +00:00
local c = vector(cx,cy)
self._center = (self._center - c):rotate_inplace(angle) + c
2011-01-13 13:38:36 +00:00
end
2011-02-26 16:27:04 +00:00
function PointShape:rotate(angle, cx,cy)
if not cx then return end
local c = vector(cx,cy)
self._pos = (self._pos - c):rotate_inplace(angle) + c
end
2011-02-06 16:40:06 +00:00
function ConvexPolygonShape:draw(mode)
local mode = mode or 'line'
love.graphics.polygon(mode, self._polygon:unpack())
2011-01-13 13:38:36 +00:00
end
2011-02-06 16:40:06 +00:00
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)))
2011-02-06 16:40:06 +00:00
love.graphics.circle(mode, self._center.x, self._center.y, self._radius, segments)
2011-01-13 13:38:36 +00:00
end
2011-02-06 16:40:06 +00:00
2011-02-26 16:27:04 +00:00
function PointShape:draw()
love.graphics.point(self._pos.x, self._pos.y)
end