commit 3ad02421953747d372a2f6eb9662493445612d17 Author: Matthias Richter Date: Thu Jan 13 14:38:36 2011 +0100 Initial commit diff --git a/README b/README new file mode 100644 index 0000000..8e13225 --- /dev/null +++ b/README @@ -0,0 +1 @@ +coming soon... diff --git a/class.lua b/class.lua new file mode 100644 index 0000000..b7a54e7 --- /dev/null +++ b/class.lua @@ -0,0 +1,71 @@ +--[[ +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. +]]-- + +module(..., package.seeall) +local function __NULL__() end +function new(constructor) + -- check name and constructor + local name = '' + if type(constructor) == "table" then + if constructor.name then name = constructor.name end + constructor = constructor[1] + end + assert(not constructor or type(constructor) == "function", + string.format('%s: constructor has to be nil or a function', name)) + + -- build class + local c = {} + c.__index = c + c.__tostring = function() return string.format("", name) end + c.construct = constructor or __NULL__ + c.Construct = constructor or __NULL__ + c.inherit = Inherit + c.Inherit = Inherit + + local meta = { + __call = function(self, ...) + local obj = {} + self.construct(obj, ...) + return setmetatable(obj, self) + end, + __tostring = function() return tostring(name) end + } + + return setmetatable(c, meta) +end + +function Inherit(class, interface, ...) + if not interface then return end + + -- __index and construct are not overwritten as for them class[name] is defined + for name, func in pairs(interface) do + if not class[name] and type(func) == "function" then + class[name] = func + end + end + + Inherit(class, ...) +end diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..363156d --- /dev/null +++ b/init.lua @@ -0,0 +1,189 @@ +--[[ +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 _PATH = (...):gsub("(%.init$)?", "").."." +local vector = require(_PATH .. 'vector') + +module(..., package.seeall) +require(_PATH .. 'shape') +require(_PATH .. 'polygon') +require(_PATH .. 'spatialhash') +vector = vector.new + +local is_initialized = false +local hash = nil + +local shapes = {} +local shape_ids = {} + +local function __NOT_INIT() error("Not yet initialized") end +local function __NULL() end +local cb_start, cb_persist, cb_stop = __NOT_INIT, __NOT_INIT, __NOT_INIT + +function init(cell_size, callback_start, callback_persist, callback_stop) + cb_start = callback_start or __NULL + cb_persist = callback_persist or __NULL + cb_stop = callback_stop or __NULL + hash = Spatialhash(cell_size) + is_initialized = true +end + +function setCallbacks(start,persist,stop) + local tbl = start + if type(start) == "function" then + tbl = {start = start, persist = persist, stop = stop} + end + if tbl.start then cb_start = tbl.start end + if tbl.persist then cb_persist = tbl.persist end + if tbl.stop then cb_stop = tbl.stop end +end + +local function newShape(shape, ul,lr) + shapes[#shapes+1] = shape + shape_ids[shape] = #shapes + hash:insert(shape, ul,lr) + return shape +end + +-- create polygon shape and add it to internal structures +function newPolygonShape(...) + assert(is_initialized, "Not properly initialized!") + local poly = Polygon(...) + local shape + if not poly:isConvex() then + shape = CompoundShape( poly, poly:splitConvex() ) + else + shape = PolygonShape( poly ) + end + + -- replace shape member function with a function that also updates + -- the hash + local function hash_aware_member(oldfunc) + return function(self, ...) + local x1,y1, x2,y2 = self._polygon:getBBox() + oldfunc(self, ...) + local x3,y3, x4,y4 = self._polygon:getBBox() + hash:update(self, vector(x1,y1), vector(x2,y2), vector(x3,y3), vector(x4,y4)) + end + end + shape.move = hash_aware_member(shape.move) + shape.rotate = hash_aware_member(shape.rotate) + + local x1,y1, x2,y2 = poly:getBBox() + return newShape(shape, vector(x1,y1), vector(x2,y2)) +end + +-- create new polygon approximation of a circle +function newCircleShape(cx, cy, radius) + assert(is_initialized, "Not properly initialized!") + local shape = CircleShape(cx,cy, radius) + local oldmove = shape.move + function shape:move(x,y) + local r = vector(self._radius, self._radius) + local c1 = self._center + oldmove(self,x,y) + local c2 = self._center + hash:update(self, c1-r, c1+r, c2-r, c2+r) + end + + local c,r = shape._center, vector(radius,radius) + return newShape(shape, c-r, c+r) +end + +-- get unique indentifier for an unordered pair of shapes, i.e.: +-- collision_id(s,t) = collision_id(t,s) +local function collision_id(s,t) + local i,k = shape_ids[s], shape_ids[t] + if i < k then i,k = k,i end + return string.format("%d,%d", i,k) +end + +-- check for collisions +local colliding_last_frame = {} +function update(dt) + -- collect colliding shapes + local tested, colliding = {}, {} + for _,s in pairs(shapes) do + local neighbors + if s._type == Shape.CIRCLE then + local c,r = s._center, vector(s._radius, s._radius) + neighbors = hash:getNeighbors(s, c-r, c+r) + else + local x1,y1, x2,y2 = s._polygon:getBBox() + neighbors = hash:getNeighbors(s, vector(x1,y2), vector(x2,y2)) + end + + for _,t in ipairs(neighbors) do + -- check if shapes have already been tested for collision + local id = collision_id(s,t) + if not tested[id] then + local collide, sep = s:collidesWith(t) + if collide then + colliding[id] = {s, t, sep.x, sep.y} + end + tested[id] = true + end + end + end + + -- call colliding callbacks on colliding shapes + for id,info in pairs(colliding) do + if colliding_last_frame[id] then + colliding_last_frame[id] = nil + cb_persist( dt, unpack(info) ) + else + cb_start( dt, unpack(info) ) + end + end + + -- call stop callback on shapes that do not collide + -- anymore + for _,info in pairs(colliding_last_frame) do + cb_stop( dt, unpack(info) ) + end + + colliding_last_frame = colliding +end + +-- remove shape from internal tables and the hash +function removeShape(shape) + local id = shape_ids[shape] + shapes[id] = nil + shape_ids[shape] = nil + if shape.type == Shape.CIRCLE then + local c,r = shape._center, vector(shape._radius, shape._radius) + hash:remove(shape, c-r, c+r) + else + local x1,y1, x2,y2 = poly:getBBox() + hash:remove(shape, vector(x1,y1), vector(x2,y2)) + end + + for id,info in pairs(colliding_last_frame) do + if info[1] == shape or info[2] == shape then + colliding_last_frame[id] = nil + end + end +end diff --git a/polygon.lua b/polygon.lua new file mode 100644 index 0000000..72b96f0 --- /dev/null +++ b/polygon.lua @@ -0,0 +1,316 @@ +--[[ +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 _PATH = (...):gsub("polygon$", "") +local Class = require(_PATH .. 'class') +local vector = require(_PATH .. 'vector') +Class = Class.new +vector = vector.new + +---------------------------- +-- Private helper functions +-- +-- create vertex list of coordinate pairs +local function toVertexList(vertices, x,y, ...) + if not x or not y then return vertices end -- no more arguments + + vertices[#vertices + 1] = vector(x, y) -- set vertex + return toVertexList(vertices, ...) -- recurse +end + +-- returns true if three points lie on a line +local function areCollinear(p,q,r) + return (q - p):cross(r - p) == 0 +end +-- remove vertices that lie on a line +local function removeCollinear(vertices) + local ret = {} + for k=1,#vertices do + local i = k > 1 and k - 1 or #vertices + local l = k < #vertices and k + 1 or 1 + if not areCollinear(vertices[i], vertices[k], vertices[l]) then + ret[#ret+1] = vertices[k] + end + end + return ret +end + +-- get index of rightmost vertex (for testing orientation) +local function getIndexOfleftmost(vertices) + local idx = 1 + for i = 2,#vertices do + if vertices[i].x < vertices[idx].x then + idx = i + end + end + return idx +end + +-- returns true if three points make a counter clockwise turn +local function ccw(p, q, r) + return (q - p):cross(r - p) >= 0 +end + +-- unpack vertex coordinates, i.e. {x=p, y=q}, ... -> p,q, ... +local function unpackHelper(v, ...) + if not v then return end + return v.x,v.y,unpackHelper(...) +end + +-- test if a point lies inside of a triangle using cramers rule +local function pointInTriangle(q, p1,p2,p3) + local v1,v2 = p2 - p1, p3 - p1 + local qp = q - p1 + local dv = v1:cross(v2) + local l = qp:cross(v2) / dv + if l <= 0 then return false end + local m = v1:cross(qp) / dv + if m <= 0 then return false end + return l+m < 1 +end + +-- returns starting indices of shared edge, i.e. if p and q share the +-- edge with indices p1,p2 of p and q1,q2 of q, the return value is p1,q1 +local function getSharedEdge(p,q) + local vertices = {} + for i,v in ipairs(q) do vertices[ tostring(v) ] = i end + for i,v in ipairs(p) do + local w = (i == #p) and p[1] or p[i+1] + if vertices[ tostring(v) ] and vertices[ tostring(w) ] then + return i, vertices[ tostring(v) ] + end + end +end + +----------------- +-- Polygon class +-- +Polygon = Class{name = "Polygon", function(self, ...) + local vertices = removeCollinear( toVertexList({}, ...) ) + assert(#vertices >= 3, "Need at least 3 non collinear points to build polygon (got "..#vertices..")") + + -- assert polygon is oriented counter clockwise + local r = getIndexOfleftmost(vertices) + local q = r > 1 and r - 1 or #vertices + local s = r < #vertices and r + 1 or 1 + if not ccw(vertices[q], vertices[r], vertices[s]) then -- reverse order if polygon is not ccw + local tmp = {} + for i=#vertices,1,-1 do + tmp[#tmp + 1] = vertices[i] + end + vertices = tmp + end + self.vertices = vertices + -- make vertices immutable + setmetatable(self.vertices, {__newindex = function() error("Thou shall not change a polygons vertices!") end}) + + -- compute polygon area and centroid + self.area = vertices[#vertices]:cross(vertices[1]) + for i = 1,#vertices-1 do + self.area = self.area + vertices[i]:cross(vertices[i+1]) + end + self.area = self.area / 2 + + local p,q = vertices[#vertices], vertices[1] + local det = p:cross(q) + self.centroid = vector((p.x+q.x) * det, (p.y+q.y) * det) + for i = 1,#vertices-1 do + p,q = vertices[i], vertices[i+1] + det = p:cross(q) + self.centroid.x = self.centroid.x + (p.x+q.x) * det + self.centroid.y = self.centroid.y + (p.y+q.y) * det + end + self.centroid = self.centroid / (6 * self.area) +end} + +-- return vertices as x1,y1,x2,y2, ..., xn,yn +function Polygon:unpack() + return unpackHelper( unpack(self.vertices) ) +end + +-- deep copy of the polygon +function Polygon:clone() + return Polygon( self:unpack() ) +end + +-- get bounding box +function Polygon:getBBox() + local ul = self.vertices[1]:clone() + local lr = ul:clone() + for i=2,#self.vertices do + local p = self.vertices[i] + if ul.x > p.x then ul.x = p.x end + if ul.y > p.y then ul.y = p.y end + + if lr.x < p.x then lr.x = p.x end + if lr.y < p.y then lr.y = p.y end + end + + return ul.x,ul.y, lr.x,lr.y +end + +-- a polygon is convex if all edges are oriented ccw +function Polygon:isConvex() + local function isConvex() + local v = self.vertices + if #v == 3 then return true end + + if not ccw(v[#v], v[1], v[2]) then + return false + end + for i = 2,#v-1 do + if not ccw(v[i-1], v[i], v[i+1]) then + return false + end + end + if not ccw(v[#v-1], v[#v], v[1]) then + return false + end + return true + end + + -- replace function so that this will only be computed once + local status = isConvex() + self.isConvex = function() return status end + return status +end + +function Polygon:move(direction) + for i,v in ipairs(self.vertices) do + self.vertices[i] = self.vertices[i] + direction + end + self.centroid = self.centroid + direction +end + +function Polygon:rotate(angle, center) + local center = center or self.centroid + for i,v in ipairs(self.vertices) do + self.vertices[i] = (self.vertices[i] - center):rotate_inplace(angle) + center + end +end + +-- triangulation by the method of kong +function Polygon:triangulate() + if #self.vertices == 3 then return {self:clone()} end + local triangles = {} -- list of triangles to be returned + local concave = {} -- list of concave edges + local adj = {} -- vertex adjacencies + local vertices = self.vertices + + -- retrieve adjacencies as the rest will be easier to implement + for i,p in ipairs(vertices) do + local l = (i == 1) and vertices[#vertices] or vertices[i-1] + local r = (i == #vertices) and vertices[1] or vertices[i+1] + adj[p] = {p = p, l = l, r = r} -- point, left and right neighbor + -- test if vertex is a concave edge + if not ccw(l,p,r) then concave[p] = p end + end + + -- and ear is an edge of the polygon that contains no other + -- vertex of the polygon + local function isEar(p1,p2,p3) + if not ccw(p1,p2,p3) then return false end + for q,_ in pairs(concave) do + if pointInTriangle(q, p1,p2,p3) then return false end + end + return true + end + + -- main loop + local nPoints, skipped = #vertices, 0 + local p = adj[ vertices[2] ] + while nPoints > 3 do + if not concave[p.p] and isEar(p.l, p.p, p.r) then + triangles[#triangles+1] = Polygon( unpackHelper(p.l, p.p, p.r) ) + if concave[p.l] and ccw(adj[p.l].l, p.l, p.r) then + concave[p.l] = nil + end + if concave[p.r] and ccw(p.l, p.r, adj[p.r].r) then + concave[p.r] = nil + end + -- remove point from list + adj[p.p] = nil + adj[p.l].r = p.r + adj[p.r].l = p.l + nPoints = nPoints - 1 + skipped = 0 + p = adj[p.l] + else + p = adj[p.r] + skipped = skipped + 1 + assert(skipped <= nPoints, "Cannot triangulate polygon (is the polygon intersecting itself?)") + end + end + triangles[#triangles+1] = Polygon( unpackHelper(p.l, p.p, p.r) ) + + return triangles +end + +-- return merged polygon if possible or nil otherwise +function Polygon:mergedWith(other) + local p,q = getSharedEdge(self.vertices, other.vertices) + if not (p and q) then return nil end + + local ret = {} + for i = 1, p do ret[#ret+1] = self.vertices[i] end + for i = 2, #other.vertices-1 do + local k = i + q - 1 + if k > #other.vertices then k = k - #other.vertices end + ret[#ret+1] = other.vertices[k] + end + for i = p+1,#self.vertices do ret[#ret+1] = self.vertices[i] end + return Polygon( unpackHelper( unpack(ret) ) ) +end + +-- split polygon into convex polygons. +-- note that this won't be the optimal split in most cases, as +-- finding the optimal split is a NP hard problem. +-- the method is to first triangulate and then greedily merge +-- the triangles. +function Polygon:splitConvex() + -- edge case: polygon is a triangle or already convex + if #self.vertices <= 3 or self:isConvex() then return {self:clone()} end + + local convex = self:triangulate() + local i = 1 + repeat + local p = convex[i] + local k = i + 1 + while k <= #convex do + local _, merged = pcall(function() return p:mergedWith(convex[k]) end) + if merged and merged:isConvex() then + convex[i] = merged + p = convex[i] + table.remove(convex, k) + else + k = k + 1 + end + end + i = i + 1 + until i >= #convex + + return convex +end diff --git a/shape.lua b/shape.lua new file mode 100644 index 0000000..bfeb4b6 --- /dev/null +++ b/shape.lua @@ -0,0 +1,257 @@ +--[[ +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 _PATH = (...):gsub("shape$", "") +local Class = require(_PATH .. 'class') +local vector = require(_PATH .. 'vector') +Class = Class.new +vector = vector.new + +local function combine_axes(a, b) + local in_a = {} + for i = 1,#a do in_a[ tostring(a[i]) ] = true end + for i = 1,#b do + if not in_a[ tostring(b[i]) ] then + a[#a+1] = b[i] + end + end + return a +end + +local function SAT(axis_table, shape_one, shape_two) + local sep,min_overlap = vector(0,0),math.huge + for _,axis in ipairs(axis_table) do + local l1,r1 = shape_one:projectOn(axis) + local l2,r2 = shape_two:projectOn(axis) + + local a,b = math.max(l1,l2), math.min(r1,r2) + if b < a then + return false + end + + local overlap = b-a + if overlap < min_overlap then + sep, min_overlap = axis * -overlap, overlap + end + end + return true, sep +end + +--------------- +-- Base class +-- +Shape = Class{name = 'Shape', function(self, t) + self._type = t +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}) + +------------------- +-- Convex polygon +-- +PolygonShape = Class{name = 'PolygonShape', function(self, polygon) + Shape.construct(self, Shape.POLYGON) + assert(polygon:isConvex(), "Polygon is not convex.") + self._polygon = polygon +end} +PolygonShape:inherit(Shape) + +function PolygonShape:getAxes() + local axes = {} + local vert = self._polygon.vertices + for i = 1,#vert-1 do + axes[#axes+1] = (vert[i+1]-vert[i]):perpendicular():normalize_inplace() + end + axes[#axes+1] = (vert[1]-vert[#vert]):perpendicular():normalize_inplace() + return axes +end + +function PolygonShape:projectOn(axis) + local vertices = self._polygon.vertices + local left, right = math.huge, -math.huge + for i = 1,#vertices do + local projection = vertices[i] * axis -- same as vertices[i]:projectOn(axis) * axis + if projection < left then + left = projection + end + if projection > right then + right = projection + end + end + return left, right +end + +function PolygonShape:collidesWith(other) + if other._type ~= Shape.POLYGON then + return other:collidesWith(self) + end + + -- else: type is POLYGON, use the SAT + return SAT(combine_axes(self:getAxes(), other:getAxes()), self, other) +end + +function PolygonShape:draw(mode) + local mode = mode or 'line' + love.graphics.polygon(mode, self._polygon:unpack()) +end + +function PolygonShape:centroid() + return self._polygon.centroid:unpack() +end + +function PolygonShape:move(x,y) + -- y not given => x is a vector + if y then x = vector(x,y) end + self._polygon:move(x) +end + +function PolygonShape:rotate(angle, center) + self._polygon:rotate(angle, center) +end + + +--------------------------------- +-- Concave (but simple) polygon +-- +CompoundShape = Class{name = 'CompoundShape', 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] = PolygonShape(s) + end +end} +CompoundShape:inherit(Shape) + +function CompoundShape:collidesWith(other) + local sep, collide = vector(0,0), false + for _,s in ipairs(self._shapes) do + local status, separating_vector = s:collidesWith(other) + collide = collide or status + if status then + sep = sep + separating_vector + end + end + return collide, sep +end + +function CompoundShape: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 CompoundShape:centroid() + return self._polygon.centroid:unpack() +end + +function CompoundShape:move(x,y) + -- y not give => x is a vector + if y then x = vector(x,y) end + self._polygon:move(x) + for _,p in ipairs(self._shapes) do + p:move(x) + end +end + +function CompoundShape:rotate(angle) + self._polygon:rotate(angle) + for _,p in ipairs(self._shapes) do + p:rotate(angle, self._polygon.centroid) + end +end + +------------------- +-- Perfect circle +-- +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) + +function CircleShape:collidesWith(other) + if other._type == Shape.CIRCLE then + return SAT({(other._center - self._center):normalize_inplace()}, self, other) + elseif other._type == Shape.COMPOUND then + return other:collidesWith(self) + end + -- else: other._type == POLYGON + -- retrieve closest edge to center + local function getClosest(center, points, distOld, k, i, inc) + local distNew = (points[i] - center):len2() + if distOld < distNew then return points[k],distOld end + k, i = i, i + inc + if i > #points then i = 1 end + if i < 1 then i = #points end + return getClosest(center, points, distNew, k, i, inc) + end + + local closestLeft,dl = getClosest(self._center, other._polygon.vertices, math.huge, 1,2, 1) + local closestRight,dr = getClosest(self._center, other._polygon.vertices, math.huge, 2,1, -1) + local closest = dl < dr and closestLeft or closestRight + return SAT(combine_axes(other:getAxes(), {(closest - self._center):normalize_inplace()}), self, other) +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 CircleShape:centroid() + return self._center:unpack() +end + +function CircleShape:move(x,y) + -- y not given => x is a vector + if y then x = vector(x,y) end + self._center = self._center + x +end + +function CircleShape:rotate(angle) + -- yeah, right +end + +function CircleShape:projectOn(axis) + -- v:projectOn(a) * a = v * a (see PolygonShape) + -- 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 + +function CircleShape:centroid() + return self._center:unpack() +end diff --git a/spatialhash.lua b/spatialhash.lua new file mode 100644 index 0000000..8c70996 --- /dev/null +++ b/spatialhash.lua @@ -0,0 +1,128 @@ +--[[ +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 _PATH = (...):gsub("spatialhash$", "") +local Class = require(_PATH .. 'class') +local vector = require(_PATH .. 'vector') +Class = Class.new +vector = vector.new + +-- special cell accesor metamethods, so vectors are converted +-- to a string before using as keys +local cell_meta = {} +function cell_meta.__newindex(tbl, key, val) + return rawset(tbl, tostring(key), val) +end +function cell_meta.__index(tbl, key) + local key = tostring(key) + local ret = rawget(tbl, key) + if not ret then + ret = setmetatable({}, {__mode = "kv"}) + rawset(tbl, key, ret) + end + return ret +end + +Spatialhash = Class{name = 'Spatialhash', function(self, cell_size) + self.cell_size = cell_size or 100 + self.cells = setmetatable({}, cell_meta) +end} + +function Spatialhash:cellCoords(v) + local v = v / self.cell_size + v.x, v.y = math.floor(v.x), math.floor(v.y) + return v +end + +function Spatialhash:cell(v) + return self.cells[ self:cellCoords(v) ] +end + +function Spatialhash:insert(obj, ul, lr) + local ul = self:cellCoords(ul) + local lr = self:cellCoords(lr) + for i = ul.x,lr.x do + for k = ul.y,lr.y do + self.cells[vector(i,k)][obj] = obj + end + end +end + +function Spatialhash:remove(obj, ul, lr) + -- no bbox given. => must check all cells + if not ul or not lr then + for _,cell in pairs(self.cells) do + cell[obj] = nil + end + return + end + + local ul = self:cellCoords(ul) + local lr = self:cellCoords(lr) + -- els: remove only from bbox + for i = ul.x,lr.x do + for k = ul.y,lr.y do + self.cells[vector(i,k)][obj] = nil + end + end +end + +-- update an objects position +function Spatialhash:update(obj, ul_old, lr_old, ul_new, lr_new) + -- cells where the object has to be updated + local xmin, xmax = math.min(ul_old.x, ul_new.x), math.max(lr_old.x, lr_new.x) + local ymin, ymax = math.min(ul_old.y, ul_new.y), math.max(lr_old.y, lr_new.y) + + -- check for regions that are only occupied by either the old or the new bbox + -- remove or add accordingly + for i = xmin,xmax do + for k = ymin,ymax do + local region_old = i >= ul_old.x and i <= ul_old.x and k >= ul_old.y and k <= ul_old.y + local region_new = i >= ul_new.x and i <= ul_new.x and k >= ul_new.y and k <= ul_new.y + if region_new and not region_old then + self.cells[vector(i,k)][obj] = obj + elseif not region_new and region_old then + self.cells[vector(i,k)][obj] = nil + end + end + end +end + +function Spatialhash:getNeighbors(obj, ul, lr) + local ul = self:cellCoords(ul) + local lr = self:cellCoords(lr) + local set,items = {}, {} + for i = ul.x,lr.x do + for k = ul.y,lr.y do + local cell = self.cells[ vector(i,k) ] or {} + for other,_ in pairs(cell) do + if other ~= obj then set[other] = other end + end + end + end + for other,_ in pairs(set) do items[#items+1] = other end + return items +end diff --git a/vector.lua b/vector.lua new file mode 100644 index 0000000..9dc8651 --- /dev/null +++ b/vector.lua @@ -0,0 +1,145 @@ +--[[ +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 setmetatable = setmetatable +local tonumber = tonumber +local type = type +local sqrt = math.sqrt +local cos = math.cos +local sin = math.sin +module(...) + +local Vector = {} +Vector.__index = Vector + +function new(x,y) + local v = {x = x or 0, y = y or 0} + setmetatable(v, Vector) + return v +end +local vector = new + +function isvector(v) + return getmetatable(v) == Vector +end + +function Vector:clone() + return vector(self.x, self.y) +end + +function Vector:unpack() + return self.x, self.y +end + +function Vector:__tostring() + return "("..tonumber(self.x)..","..tonumber(self.y)..")" +end + +function Vector.__unm(a) + return vector(-a.x, -a.y) +end + +function Vector.__add(a,b) + return vector(a.x+b.x, a.y+b.y) +end + +function Vector.__sub(a,b) + return vector(a.x-b.x, a.y-b.y) +end + +function Vector.__mul(a,b) + if type(a) == "number" then + return vector(a*b.x, a*b.y) + elseif type(b) == "number" then + return vector(b*a.x, b*a.y) + else + return a.x*b.x + a.y*b.y + end +end + +function Vector.__div(a,b) + return vector(a.x / b, a.y / b) +end + +function Vector.__eq(a,b) + return a.x == b.x and a.y == b.y +end + +function Vector.__lt(a,b) + return a.x < b.x or (a.x == b.x and a.y < b.y) +end + +function Vector.__le(a,b) + return a.x <= b.x and a.y <= b.y +end + +function Vector.permul(a,b) + return vector(a.x*b.x, a.y*b.y) +end + +function Vector:len2() + return self * self +end + +function Vector:len() + return sqrt(self*self) +end + +function Vector.dist(a, b) + return (b-a):len() +end + +function Vector:normalize_inplace() + local l = self:len() + self.x, self.y = self.x / l, self.y / l + return self +end + +function Vector:normalized() + return self / self:len() +end + +function Vector:rotate_inplace(phi) + local c, s = cos(phi), sin(phi) + self.x, self.y = c * self.x - s * self.y, s * self.x + c * self.y + return self +end + +function Vector:rotated(phi) + return self:clone():rotate_inplace(phi) +end + +function Vector:perpendicular() + return vector(-self.y, self.x) +end + +function Vector:projectOn(v) + return (self * v) * v / v:len2() +end + +function Vector:cross(other) + return self.x * other.y - self.y * other.x +end