mirror of
https://github.com/vrld/HC.git
synced 2024-11-18 12:54:23 +00:00
Initial commit
This commit is contained in:
commit
3ad0242195
71
class.lua
Normal file
71
class.lua
Normal file
@ -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 = '<unnamed class>'
|
||||
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("<instance of %s>", 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
|
189
init.lua
Normal file
189
init.lua
Normal file
@ -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
|
316
polygon.lua
Normal file
316
polygon.lua
Normal file
@ -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
|
257
shape.lua
Normal file
257
shape.lua
Normal file
@ -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
|
128
spatialhash.lua
Normal file
128
spatialhash.lua
Normal file
@ -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
|
145
vector.lua
Normal file
145
vector.lua
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user