├── docs ├── tutorial.rst ├── Class.rst ├── Vector.rst ├── reference.rst ├── license.rst ├── index.rst ├── SpatialHash.rst ├── Polygon.rst ├── Makefile ├── Shapes.rst ├── MainModule.rst └── conf.py ├── hc-0.1-1.rockspec ├── README.md ├── vector-light.lua ├── class.lua ├── init.lua ├── gjk.lua ├── spatialhash.lua ├── shapes.lua └── polygon.lua /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | To be rewritten. 5 | -------------------------------------------------------------------------------- /docs/Class.rst: -------------------------------------------------------------------------------- 1 | HC.class 2 | ======== 3 | 4 | :: 5 | 6 | class = require 'HC.class' 7 | 8 | See ``hump.class``. 9 | -------------------------------------------------------------------------------- /docs/Vector.rst: -------------------------------------------------------------------------------- 1 | HC.vector 2 | ========= 3 | 4 | :: 5 | 6 | vector = require 'HC.vector' 7 | 8 | See ``hump.vector_light``. 9 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | HC is composed of several parts. Most of the time, you will only have to deal 5 | with the main module and the `Shapes` sub-module, but the other modules are 6 | at your disposal if you need them. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | HC 12 | HC.shapes 13 | HC.polygon 14 | HC.spatialhash 15 | HC.vector 16 | HC.class 17 | -------------------------------------------------------------------------------- /hc-0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "HC" 2 | version = "0.1-1" 3 | source = { 4 | url = "git://github.com/vrld/HC.git" 5 | } 6 | description = { 7 | summary="General purpose 2D collision detection in pure Lua", 8 | homepage = "https://hc.readthedocs.io", 9 | license = "MIT", 10 | } 11 | dependencies = { 12 | "lua = 5.1" 13 | } 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["hardoncollider"] = "init.lua", 18 | ["hardoncollider.class"] = "class.lua", 19 | ["hardoncollider.gjk"] = "gjk.lua", 20 | ["hardoncollider.polygon"] = "polygon.lua", 21 | ["hardoncollider.shapes"] = "shapes.lua", 22 | ["hardoncollider.spatialhash"] = "spatialhash.lua", 23 | ["hardoncollider.vector-light"] = "vector-light.lua", 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | Copyright (c) 2011-2015 Matthias Richter 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | Except as contained in this notice, the name(s) of the above copyright holders 17 | shall not be used in advertising or otherwise to promote the sale, use or 18 | other dealings in this Software without prior written authorization. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## HC - General Purpose 2D Collision Detection System with [LÖVE](https://love2d.org) 2 | 3 | Documentation and examples here: http://hc.readthedocs.org/ 4 | 5 | ```lua 6 | HC = require 'HC' 7 | 8 | -- array to hold collision messages 9 | local text = {} 10 | 11 | function love.load() 12 | -- add a rectangle to the scene 13 | rect = HC.rectangle(200,400,400,20) 14 | 15 | -- add a circle to the scene 16 | mouse = HC.circle(400,300,20) 17 | mouse:moveTo(love.mouse.getPosition()) 18 | end 19 | 20 | function love.update(dt) 21 | -- move circle to mouse position 22 | mouse:moveTo(love.mouse.getPosition()) 23 | 24 | -- rotate rectangle 25 | rect:rotate(dt) 26 | 27 | -- check for collisions 28 | for shape, delta in pairs(HC.collisions(mouse)) do 29 | text[#text+1] = string.format("Colliding. Separating vector = (%s,%s)", 30 | delta.x, delta.y) 31 | end 32 | 33 | while #text > 40 do 34 | table.remove(text, 1) 35 | end 36 | end 37 | 38 | function love.draw() 39 | -- print messages 40 | for i = 1,#text do 41 | love.graphics.setColor(255,255,255, 255 - (i-1) * 6) 42 | love.graphics.print(text[#text - (i-1)], 10, i * 15) 43 | end 44 | 45 | -- shapes can be drawn to the screen 46 | love.graphics.setColor(255,255,255) 47 | rect:draw('fill') 48 | mouse:draw('fill') 49 | end 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. HC documentation master file, created by 2 | sphinx-quickstart on Thu Oct 8 20:31:43 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | HC - General purpose collision detection with LÖVE_ 7 | =================================================== 8 | 9 | HC is a Lua module to simplify one important aspect in computer games: 10 | Collision detection. 11 | 12 | It can detect collisions between arbitrary positioned and rotated shapes. 13 | Built-in shapes are points, circles and polygons. 14 | Any non-intersecting polygons are supported, even concave ones. 15 | You can add other types of shapes if you need them. 16 | 17 | The main interface is simple: 18 | 19 | 1. Set up your scene, 20 | 2. Check for collisions, 21 | 3. React to collisions. 22 | 23 | First steps 24 | ----------- 25 | 26 | This is an example on how to use HC. One shape will stick to the mouse 27 | position, while the other will stay in the same place:: 28 | 29 | HC = require 'HC' 30 | 31 | -- array to hold collision messages 32 | local text = {} 33 | 34 | function love.load() 35 | -- add a rectangle to the scene 36 | rect = HC.rectangle(200,400,400,20) 37 | 38 | -- add a circle to the scene 39 | mouse = HC.circle(400,300,20) 40 | mouse:moveTo(love.mouse.getPosition()) 41 | end 42 | 43 | function love.update(dt) 44 | -- move circle to mouse position 45 | mouse:moveTo(love.mouse.getPosition()) 46 | 47 | -- rotate rectangle 48 | rect:rotate(dt) 49 | 50 | -- check for collisions 51 | for shape, delta in pairs(HC.collisions(mouse)) do 52 | text[#text+1] = string.format("Colliding. Separating vector = (%s,%s)", 53 | delta.x, delta.y) 54 | end 55 | 56 | while #text > 40 do 57 | table.remove(text, 1) 58 | end 59 | end 60 | 61 | function love.draw() 62 | -- print messages 63 | for i = 1,#text do 64 | love.graphics.setColor(255,255,255, 255 - (i-1) * 6) 65 | love.graphics.print(text[#text - (i-1)], 10, i * 15) 66 | end 67 | 68 | -- shapes can be drawn to the screen 69 | love.graphics.setColor(255,255,255) 70 | rect:draw('fill') 71 | mouse:draw('fill') 72 | end 73 | 74 | 75 | Get HC 76 | ------ 77 | 78 | You can download the latest packaged version as `zip `_- or `tar `_-archive directly 79 | from github_. 80 | 81 | You can also have a look at the sourcecode online `here `_. 82 | 83 | If you use the Git command line client, you can clone the repository by 84 | running:: 85 | 86 | git clone git://github.com/vrld/HC.git 87 | 88 | Once done, you can check for updates by running:: 89 | 90 | git pull 91 | 92 | from inside the directory. 93 | 94 | 95 | Read on 96 | ------- 97 | 98 | .. toctree:: 99 | :maxdepth: 2 100 | 101 | reference 102 | tutorial 103 | license 104 | 105 | 106 | Indices and tables 107 | ^^^^^^^^^^^^^^^^^^ 108 | 109 | * :ref:`genindex` 110 | * :ref:`modindex` 111 | * :ref:`search` 112 | 113 | 114 | .. _LÖVE: http://love2d.org 115 | .. _github: https://github.com 116 | -------------------------------------------------------------------------------- /vector-light.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2012 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local sqrt, cos, sin = math.sqrt, math.cos, math.sin 28 | 29 | local function str(x,y) 30 | return "("..tonumber(x)..","..tonumber(y)..")" 31 | end 32 | 33 | local function mul(s, x,y) 34 | return s*x, s*y 35 | end 36 | 37 | local function div(s, x,y) 38 | return x/s, y/s 39 | end 40 | 41 | local function add(x1,y1, x2,y2) 42 | return x1+x2, y1+y2 43 | end 44 | 45 | local function sub(x1,y1, x2,y2) 46 | return x1-x2, y1-y2 47 | end 48 | 49 | local function permul(x1,y1, x2,y2) 50 | return x1*x2, y1*y2 51 | end 52 | 53 | local function dot(x1,y1, x2,y2) 54 | return x1*x2 + y1*y2 55 | end 56 | 57 | local function det(x1,y1, x2,y2) 58 | return x1*y2 - y1*x2 59 | end 60 | 61 | local function eq(x1,y1, x2,y2) 62 | return x1 == x2 and y1 == y2 63 | end 64 | 65 | local function lt(x1,y1, x2,y2) 66 | return x1 < x2 or (x1 == x2 and y1 < y2) 67 | end 68 | 69 | local function le(x1,y1, x2,y2) 70 | return x1 <= x2 and y1 <= y2 71 | end 72 | 73 | local function len2(x,y) 74 | return x*x + y*y 75 | end 76 | 77 | local function len(x,y) 78 | return sqrt(x*x + y*y) 79 | end 80 | 81 | local function dist(x1,y1, x2,y2) 82 | return len(x1-x2, y1-y2) 83 | end 84 | 85 | local function normalize(x,y) 86 | local l = len(x,y) 87 | return x/l, y/l 88 | end 89 | 90 | local function rotate(phi, x,y) 91 | local c, s = cos(phi), sin(phi) 92 | return c*x - s*y, s*x + c*y 93 | end 94 | 95 | local function perpendicular(x,y) 96 | return -y, x 97 | end 98 | 99 | local function project(x,y, u,v) 100 | local s = (x*u + y*v) / (u*u + v*v) 101 | return s*u, s*v 102 | end 103 | 104 | local function mirror(x,y, u,v) 105 | local s = 2 * (x*u + y*v) / (u*u + v*v) 106 | return s*u - x, s*v - y 107 | end 108 | 109 | 110 | -- the module 111 | return { 112 | str = str, 113 | 114 | -- arithmetic 115 | mul = mul, 116 | div = div, 117 | add = add, 118 | sub = sub, 119 | permul = permul, 120 | dot = dot, 121 | det = det, 122 | cross = det, 123 | 124 | -- relation 125 | eq = eq, 126 | lt = lt, 127 | le = le, 128 | 129 | -- misc operations 130 | len2 = len2, 131 | len = len, 132 | dist = dist, 133 | normalize = normalize, 134 | rotate = rotate, 135 | perpendicular = perpendicular, 136 | project = project, 137 | mirror = mirror, 138 | } 139 | -------------------------------------------------------------------------------- /class.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2011 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local function __NULL__() end 28 | 29 | -- class "inheritance" by copying functions 30 | local function inherit(class, interface, ...) 31 | if not interface then return end 32 | assert(type(interface) == "table", "Can only inherit from other classes.") 33 | 34 | -- __index and construct are not overwritten as for them class[name] is defined 35 | for name, func in pairs(interface) do 36 | if not class[name] then 37 | class[name] = func 38 | end 39 | end 40 | for super in pairs(interface.__is_a or {}) do 41 | class.__is_a[super] = true 42 | end 43 | 44 | return inherit(class, ...) 45 | end 46 | 47 | -- class builder 48 | local function new(args) 49 | local super = {} 50 | local name = '' 51 | local constructor = args or __NULL__ 52 | if type(args) == "table" then 53 | -- nasty hack to check if args.inherits is a table of classes or a class or nil 54 | super = (args.inherits or {}).__is_a and {args.inherits} or args.inherits or {} 55 | name = args.name or name 56 | constructor = args[1] or __NULL__ 57 | end 58 | assert(type(constructor) == "function", 'constructor has to be nil or a function') 59 | 60 | -- build class 61 | local class = {} 62 | class.__index = class 63 | class.__tostring = function() return (""):format(tostring(class)) end 64 | class.construct = constructor or __NULL__ 65 | class.inherit = inherit 66 | class.__is_a = {[class] = true} 67 | class.is_a = function(self, other) return not not self.__is_a[other] end 68 | 69 | -- inherit superclasses (see above) 70 | inherit(class, unpack(super)) 71 | 72 | -- syntactic sugar 73 | local meta = { 74 | __call = function(self, ...) 75 | local obj = {} 76 | setmetatable(obj, self) 77 | self.construct(obj, ...) 78 | return obj 79 | end, 80 | __tostring = function() return name end 81 | } 82 | return setmetatable(class, meta) 83 | end 84 | 85 | -- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons). 86 | if common_class ~= false and not common then 87 | common = {} 88 | function common.class(name, prototype, parent) 89 | local init = prototype.init or (parent or {}).init 90 | return new{name = name, inherits = {prototype, parent}, init} 91 | end 92 | function common.instance(class, ...) 93 | return class(...) 94 | end 95 | end 96 | 97 | -- the module 98 | return setmetatable({new = new, inherit = inherit}, 99 | {__call = function(_,...) return new(...) end}) 100 | -------------------------------------------------------------------------------- /docs/SpatialHash.rst: -------------------------------------------------------------------------------- 1 | HC.spatialhash 2 | ============== 3 | 4 | :: 5 | 6 | spatialhash = require 'HC.spatialhash' 7 | 8 | A spatial hash implementation that supports scenes of arbitrary size. The hash 9 | is sparse, which means that cells will only be created when needed. 10 | 11 | 12 | .. class:: Spatialhash([cellsize = 100]) 13 | 14 | :param number cellsize: Width and height of a cell (optional). 15 | 16 | Create a new spatial hash with a given cell size. 17 | 18 | Choosing a good cell size depends on your application. To get a decent speedup, 19 | the average cell should not contain too many objects, nor should a single 20 | object occupy too many cells. A good rule of thumb is to choose the cell size 21 | so that the average object will occupy only one cell. 22 | 23 | .. note:: 24 | The syntax depends on used class system. The shown syntax works when using 25 | the bundled `hump.class `_ or 26 | `slither `_. 27 | 28 | **Example**:: 29 | 30 | Spatialhash = require 'hardoncollider.spatialhash' 31 | hash = Spatialhash(150) 32 | 33 | 34 | .. function:: Spatialhash:cellCoords(x,y) 35 | 36 | :param numbers x, y: The position to query. 37 | :returns: Coordinates of the cell which would contain ``x,y``. 38 | 39 | Get coordinates of a given value, i.e. the cell index in which a given point 40 | would be placed. 41 | 42 | **Example**:: 43 | 44 | local mx,my = love.mouse.getPosition() 45 | cx, cy = hash:cellCoords(mx, my) 46 | 47 | 48 | .. function:: Spatialhash:cell(i,k) 49 | 50 | :param numbers i, k: The cell index. 51 | :returns: Set of objects contained in the cell. 52 | 53 | Get the cell with given coordinates. 54 | 55 | A cell is a table which's keys and value are the objects stored in the cell, 56 | i.e.:: 57 | 58 | cell = { 59 | [obj1] = obj1, 60 | [obj2] = obj2, 61 | ... 62 | } 63 | 64 | You can iterate over the objects in a cell using ``pairs()``:: 65 | 66 | for object in pairs(cell) do stuff(object) end 67 | 68 | **Example**:: 69 | 70 | local mx,my = love.mouse.getPosition() 71 | cx, cy = hash:cellCoords(mx, my) 72 | cell = hash:cell(cx, cy) 73 | 74 | 75 | .. function:: Spatialhash:cellAt(x,y) 76 | 77 | :param numbers x, y: The position to query. 78 | :returns: Set of objects contained in the cell. 79 | 80 | Get the cell that contains point x,y. 81 | 82 | Same as ``hash:cell(hash:cellCoords(x,y))`` 83 | 84 | **Example**:: 85 | 86 | local mx,my = love.mouse.getPosition() 87 | cell = hash:cellAt(mx, my) 88 | 89 | 90 | .. function:: Spatialhash:shapes() 91 | 92 | :returns: Set of all shapes in the hash. 93 | 94 | Get *all* shapes that are recorded in the hash. 95 | 96 | 97 | .. function:: Spatialhash:inSameCells(x1,y1, x2,y2) 98 | 99 | :param numbers x1,y1: Upper left corner of the query bounding box. 100 | :param numbers x2,y2: Lower right corner of the query bounding box. 101 | :returns: Set of all shapes in the same cell as the bbox. 102 | 103 | Get the shapes that are in the same cell as the defined bounding box. 104 | 105 | 106 | .. function:: Spatialhash:register(obj, x1,y1, x2,y2) 107 | 108 | :param mixed obj: Object to place in the hash. It can be of any type except `nil`. 109 | :param numbers x1,y1: Upper left corner of the bounding box. 110 | :param numbers x2,y2: Lower right corner of the bounding box. 111 | 112 | Insert an object into the hash using a given bounding box. 113 | 114 | **Example**:: 115 | 116 | hash:register(shape, shape:bbox()) 117 | 118 | 119 | .. function:: Spatialhash:remove(obj[, x1,y1[, x2,y2]]) 120 | 121 | :param mixed obj: The object to delete 122 | :param numbers x1,y1: Upper left corner of the bounding box (optional). 123 | :param numbers x2,y2: Lower right corner of the bounding box (optional). 124 | 125 | Remove an object from the hash using a bounding box. 126 | 127 | If no bounding box is given, search the whole hash to delete the object. 128 | 129 | **Example**:: 130 | 131 | hash:remove(shape, shape:bbox()) 132 | hash:remove(object_with_unknown_position) 133 | 134 | 135 | .. function:: Spatialhash:update(obj, x1,y1, x2,y2, x3,y3, x4,y4) 136 | 137 | :param mixed obj: The object to be updated. 138 | :param numbers x1,y1: Upper left corner of the bounding box before the object was moved. 139 | :param numbers x2,y2: Lower right corner of the bounding box before the object was moved. 140 | :param numbers x3,y3: Upper left corner of the bounding box after the object was moved. 141 | :param numbers x4,y4: Lower right corner of the bounding box after the object was moved. 142 | 143 | Update an objects position given the old bounding box and the new bounding box. 144 | 145 | **Example**:: 146 | 147 | hash:update(shape, -100,-30, 0,60, -100,-70, 0,20) 148 | 149 | 150 | .. function:: Spatialhash:draw(draw_mode[, show_empty = true[, print_key = false]]) 151 | 152 | :param string draw_mode: Either 'fill' or 'line'. See the LÖVE wiki. 153 | :param boolean show_empty: Wether to draw empty cells (optional). 154 | :param boolean print_key: Wether to print cell coordinates (optional). 155 | 156 | Draw hash cells on the screen, mostly for debug purposes 157 | 158 | **Example**:: 159 | 160 | love.graphics.setColor(160,140,100,100) 161 | hash:draw('line', true, true) 162 | hash:draw('fill', false) 163 | 164 | 165 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2011 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local _NAME, common_local = ..., common 28 | if not (type(common) == 'table' and common.class and common.instance) then 29 | assert(common_class ~= false, 'No class commons specification available.') 30 | require(_NAME .. '.class') 31 | end 32 | local Shapes = require(_NAME .. '.shapes') 33 | local Spatialhash = require(_NAME .. '.spatialhash') 34 | 35 | -- reset global table `common' (required by class commons) 36 | if common_local ~= common then 37 | common_local, common = common, common_local 38 | end 39 | 40 | local newPolygonShape = Shapes.newPolygonShape 41 | local newCircleShape = Shapes.newCircleShape 42 | local newPointShape = Shapes.newPointShape 43 | 44 | local HC = {} 45 | function HC:init(cell_size) 46 | self:resetHash(cell_size) 47 | end 48 | 49 | function HC:hash() return self._hash end -- consistent interface with global HC instance 50 | 51 | -- spatial hash management 52 | function HC:resetHash(cell_size) 53 | self._hash = common_local.instance(Spatialhash, cell_size or 100) 54 | return self 55 | end 56 | 57 | function HC:register(shape) 58 | self._hash:register(shape, shape:bbox()) 59 | 60 | -- keep track of where/how big the shape is 61 | for _, f in ipairs({'move', 'rotate', 'scale'}) do 62 | local old_function = shape[f] 63 | shape[f] = function(this, ...) 64 | local x1,y1,x2,y2 = this:bbox() 65 | old_function(this, ...) 66 | self._hash:update(this, x1,y1,x2,y2, this:bbox()) 67 | return this 68 | end 69 | end 70 | 71 | return shape 72 | end 73 | 74 | function HC:remove(shape) 75 | self._hash:remove(shape, shape:bbox()) 76 | for _, f in ipairs({'move', 'rotate', 'scale'}) do 77 | shape[f] = function() 78 | error(f.."() called on a removed shape") 79 | end 80 | end 81 | return self 82 | end 83 | 84 | -- shape constructors 85 | function HC:polygon(...) 86 | return self:register(newPolygonShape(...)) 87 | end 88 | 89 | function HC:rectangle(x,y,w,h) 90 | return self:polygon(x,y, x+w,y, x+w,y+h, x,y+h) 91 | end 92 | 93 | function HC:circle(x,y,r) 94 | return self:register(newCircleShape(x,y,r)) 95 | end 96 | 97 | function HC:point(x,y) 98 | return self:register(newPointShape(x,y)) 99 | end 100 | 101 | -- collision detection 102 | function HC:neighbors(shape) 103 | local neighbors = self._hash:inSameCells(shape:bbox()) 104 | rawset(neighbors, shape, nil) 105 | return neighbors 106 | end 107 | 108 | function HC:collisions(shape) 109 | local candidates = self:neighbors(shape) 110 | for other in pairs(candidates) do 111 | local collides, dx, dy = shape:collidesWith(other) 112 | if collides then 113 | rawset(candidates, other, {dx,dy, x=dx, y=dy}) 114 | else 115 | rawset(candidates, other, nil) 116 | end 117 | end 118 | return candidates 119 | end 120 | 121 | function HC:raycast(x, y, dx, dy, range) 122 | local dxr, dyr = dx * range, dy * range 123 | local bbox = { x + dxr , y + dyr, x, y } 124 | local candidates = self._hash:inSameCells(unpack(bbox)) 125 | 126 | for col in pairs(candidates) do 127 | local rparams = col:intersectionsWithRay(x, y, dx, dy) 128 | if #rparams > 0 then 129 | for i, rparam in pairs(rparams) do 130 | if rparam < 0 or rparam > range then 131 | rawset(rparams, i, nil) 132 | else 133 | local hitx, hity = x + (rparam * dx), y + (rparam * dy) 134 | rawset(rparams, i, { x = hitx, y = hity }) 135 | end 136 | end 137 | rawset(candidates, col, rparams) 138 | else 139 | rawset(candidates, col, nil) 140 | end 141 | end 142 | return candidates 143 | end 144 | 145 | function HC:shapesAt(x, y) 146 | local candidates = {} 147 | for c in pairs(self._hash:cellAt(x, y)) do 148 | if c:contains(x, y) then 149 | rawset(candidates, c, c) 150 | end 151 | end 152 | return candidates 153 | end 154 | 155 | -- the class and the instance 156 | HC = common_local.class('HardonCollider', HC) 157 | local instance = common_local.instance(HC) 158 | 159 | -- the module 160 | return setmetatable({ 161 | new = function(...) return common_local.instance(HC, ...) end, 162 | resetHash = function(...) return instance:resetHash(...) end, 163 | register = function(...) return instance:register(...) end, 164 | remove = function(...) return instance:remove(...) end, 165 | 166 | polygon = function(...) return instance:polygon(...) end, 167 | rectangle = function(...) return instance:rectangle(...) end, 168 | circle = function(...) return instance:circle(...) end, 169 | point = function(...) return instance:point(...) end, 170 | 171 | neighbors = function(...) return instance:neighbors(...) end, 172 | collisions = function(...) return instance:collisions(...) end, 173 | shapesAt = function(...) return instance:shapesAt(...) end, 174 | hash = function() return instance.hash() end, 175 | }, {__call = function(_, ...) return common_local.instance(HC, ...) end}) 176 | -------------------------------------------------------------------------------- /gjk.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2012 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local _PACKAGE = (...):match("^(.+)%.[^%.]+") 28 | local vector = require(_PACKAGE .. '.vector-light') 29 | local huge, abs = math.huge, math.abs 30 | 31 | local simplex, edge = {}, {} 32 | 33 | local function support(shape_a, shape_b, dx, dy) 34 | local x,y = shape_a:support(dx,dy) 35 | return vector.sub(x,y, shape_b:support(-dx, -dy)) 36 | end 37 | 38 | -- returns closest edge to the origin 39 | local function closest_edge(n) 40 | edge.dist = huge 41 | 42 | local i = n-1 43 | for k = 1,n-1,2 do 44 | local ax,ay = simplex[i], simplex[i+1] 45 | local bx,by = simplex[k], simplex[k+1] 46 | i = k 47 | 48 | local ex,ey = vector.perpendicular(bx-ax, by-ay) 49 | local nx,ny = vector.normalize(ex,ey) 50 | local d = vector.dot(ax,ay, nx,ny) 51 | 52 | if d < edge.dist then 53 | edge.dist = d 54 | edge.nx, edge.ny = nx, ny 55 | edge.i = k 56 | end 57 | end 58 | end 59 | 60 | local function EPA(shape_a, shape_b) 61 | -- make sure simplex is oriented counter clockwise 62 | local cx,cy, bx,by, ax,ay = unpack(simplex, 1, 6) 63 | if vector.dot(ax-bx,ay-by, cx-bx,cy-by) < 0 then 64 | simplex[1],simplex[2] = ax,ay 65 | simplex[5],simplex[6] = cx,cy 66 | end 67 | 68 | -- the expanding polytype algorithm 69 | local is_either_circle = shape_a._center or shape_b._center 70 | local last_diff_dist, n = huge, 6 71 | while true do 72 | closest_edge(n) 73 | local px,py = support(shape_a, shape_b, edge.nx, edge.ny) 74 | local d = vector.dot(px,py, edge.nx, edge.ny) 75 | 76 | local diff_dist = d - edge.dist 77 | if diff_dist < 1e-6 or (is_either_circle and abs(last_diff_dist - diff_dist) < 1e-10) then 78 | return -d*edge.nx, -d*edge.ny 79 | end 80 | last_diff_dist = diff_dist 81 | 82 | -- simplex = {..., simplex[edge.i-1], px, py, simplex[edge.i] 83 | for i = n, edge.i, -1 do 84 | simplex[i+2] = simplex[i] 85 | end 86 | simplex[edge.i+0] = px 87 | simplex[edge.i+1] = py 88 | n = n + 2 89 | end 90 | end 91 | 92 | -- : : origin must be in plane between A and B 93 | -- B o------o A since A is the furthest point on the MD 94 | -- : : in direction of the origin. 95 | local function do_line() 96 | local bx,by, ax,ay = unpack(simplex, 1, 4) 97 | 98 | local abx,aby = bx-ax, by-ay 99 | 100 | local dx,dy = vector.perpendicular(abx,aby) 101 | 102 | if vector.dot(dx,dy, -ax,-ay) < 0 then 103 | dx,dy = -dx,-dy 104 | end 105 | return dx,dy 106 | end 107 | 108 | -- B .' 109 | -- o-._ 1 110 | -- | `-. .' The origin can only be in regions 1, 3 or 4: 111 | -- | 4 o A 2 A lies on the edge of the MD and we came 112 | -- | _.-' '. from left of BC. 113 | -- o-' 3 114 | -- C '. 115 | local function do_triangle() 116 | local cx,cy, bx,by, ax,ay = unpack(simplex, 1, 6) 117 | local aox,aoy = -ax,-ay 118 | local abx,aby = bx-ax, by-ay 119 | local acx,acy = cx-ax, cy-ay 120 | 121 | -- test region 1 122 | local dx,dy = vector.perpendicular(abx,aby) 123 | if vector.dot(dx,dy, acx,acy) > 0 then 124 | dx,dy = -dx,-dy 125 | end 126 | if vector.dot(dx,dy, aox,aoy) > 0 then 127 | -- simplex = {bx,by, ax,ay} 128 | simplex[1], simplex[2] = bx,by 129 | simplex[3], simplex[4] = ax,ay 130 | return 4, dx,dy 131 | end 132 | 133 | -- test region 3 134 | dx,dy = vector.perpendicular(acx,acy) 135 | if vector.dot(dx,dy, abx,aby) > 0 then 136 | dx,dy = -dx,-dy 137 | end 138 | if vector.dot(dx,dy, aox, aoy) > 0 then 139 | -- simplex = {cx,cy, ax,ay} 140 | simplex[3], simplex[4] = ax,ay 141 | return 4, dx,dy 142 | end 143 | 144 | -- must be in region 4 145 | return 6 146 | end 147 | 148 | local function GJK(shape_a, shape_b) 149 | local ax,ay = support(shape_a, shape_b, 1,0) 150 | if ax == 0 and ay == 0 then 151 | -- only true if shape_a and shape_b are touching in a vertex, e.g. 152 | -- .--- .---. 153 | -- | A | .-. | B | support(A, 1,0) = x 154 | -- '---x---. or : A :x---' support(B, -1,0) = x 155 | -- | B | `-' => support(A,B,1,0) = x - x = 0 156 | -- '---' 157 | -- Since CircleShape:support(dx,dy) normalizes dx,dy we have to opt 158 | -- out or the algorithm blows up. In accordance to the cases below 159 | -- choose to judge this situation as not colliding. 160 | return false 161 | end 162 | 163 | simplex[1], simplex[2] = ax, ay 164 | local dx,dy = -ax,-ay 165 | 166 | -- first iteration: line case 167 | ax,ay = support(shape_a, shape_b, dx,dy) 168 | if vector.dot(ax,ay, dx,dy) <= 0 then 169 | return false 170 | end 171 | 172 | simplex[3], simplex[4] = ax,ay 173 | dx, dy = do_line() 174 | 175 | local n 176 | 177 | -- all other iterations must be the triangle case 178 | while true do 179 | ax,ay = support(shape_a, shape_b, dx,dy) 180 | 181 | if vector.dot(ax,ay, dx,dy) <= 0 then 182 | return false 183 | end 184 | 185 | simplex[5], simplex[6] = ax,ay 186 | n, dx, dy = do_triangle() 187 | 188 | if n == 6 then 189 | return true, EPA(shape_a, shape_b) 190 | end 191 | end 192 | end 193 | 194 | return GJK 195 | -------------------------------------------------------------------------------- /spatialhash.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2011 Matthias Richter 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | Except as contained in this notice, the name(s) of the above copyright holders 12 | shall not be used in advertising or otherwise to promote the sale, use or 13 | other dealings in this Software without prior written authorization. 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | ]]-- 22 | 23 | local floor = math.floor 24 | local min, max = math.min, math.max 25 | 26 | local _PACKAGE, common_local = (...):match("^(.+)%.[^%.]+"), common 27 | if not (type(common) == 'table' and common.class and common.instance) then 28 | assert(common_class ~= false, 'No class commons specification available.') 29 | require(_PACKAGE .. '.class') 30 | common_local, common = common, common_local 31 | end 32 | local vector = require(_PACKAGE .. '.vector-light') 33 | 34 | local Spatialhash = {} 35 | function Spatialhash:init(cell_size) 36 | self.cell_size = cell_size or 100 37 | self.cells = {} 38 | end 39 | 40 | function Spatialhash:cellCoords(x,y) 41 | return floor(x / self.cell_size), floor(y / self.cell_size) 42 | end 43 | 44 | function Spatialhash:cell(i,k) 45 | local row = rawget(self.cells, i) 46 | if not row then 47 | row = {} 48 | rawset(self.cells, i, row) 49 | end 50 | 51 | local cell = rawget(row, k) 52 | if not cell then 53 | cell = {} 54 | rawset(row, k, cell) 55 | end 56 | 57 | return cell 58 | end 59 | 60 | function Spatialhash:cellAt(x,y) 61 | return self:cell(self:cellCoords(x,y)) 62 | end 63 | 64 | -- get all shapes 65 | function Spatialhash:shapes() 66 | local set = {} 67 | for i,row in pairs(self.cells) do 68 | for k,cell in pairs(row) do 69 | for obj in pairs(cell) do 70 | rawset(set, obj, obj) 71 | end 72 | end 73 | end 74 | return set 75 | end 76 | 77 | -- get all shapes that are in the same cells as the bbox x1,y1 '--. x2,y2 78 | function Spatialhash:inSameCells(x1,y1, x2,y2) 79 | local set = {} 80 | x1, y1 = self:cellCoords(x1, y1) 81 | x2, y2 = self:cellCoords(x2, y2) 82 | for i = x1,x2 do 83 | for k = y1,y2 do 84 | for obj in pairs(self:cell(i,k)) do 85 | rawset(set, obj, obj) 86 | end 87 | end 88 | end 89 | return set 90 | end 91 | 92 | function Spatialhash:register(obj, x1, y1, x2, y2) 93 | x1, y1 = self:cellCoords(x1, y1) 94 | x2, y2 = self:cellCoords(x2, y2) 95 | for i = x1,x2 do 96 | for k = y1,y2 do 97 | rawset(self:cell(i,k), obj, obj) 98 | end 99 | end 100 | end 101 | 102 | function Spatialhash:remove(obj, x1, y1, x2,y2) 103 | -- no bbox given. => must check all cells 104 | if not (x1 and y1 and x2 and y2) then 105 | for _,row in pairs(self.cells) do 106 | for _,cell in pairs(row) do 107 | rawset(cell, obj, nil) 108 | end 109 | end 110 | return 111 | end 112 | 113 | -- else: remove only from bbox 114 | x1,y1 = self:cellCoords(x1,y1) 115 | x2,y2 = self:cellCoords(x2,y2) 116 | for i = x1,x2 do 117 | for k = y1,y2 do 118 | rawset(self:cell(i,k), obj, nil) 119 | end 120 | end 121 | end 122 | 123 | -- update an objects position 124 | function Spatialhash:update(obj, old_x1,old_y1, old_x2,old_y2, new_x1,new_y1, new_x2,new_y2) 125 | old_x1, old_y1 = self:cellCoords(old_x1, old_y1) 126 | old_x2, old_y2 = self:cellCoords(old_x2, old_y2) 127 | 128 | new_x1, new_y1 = self:cellCoords(new_x1, new_y1) 129 | new_x2, new_y2 = self:cellCoords(new_x2, new_y2) 130 | 131 | if old_x1 == new_x1 and old_y1 == new_y1 and 132 | old_x2 == new_x2 and old_y2 == new_y2 then 133 | return 134 | end 135 | 136 | for i = old_x1,old_x2 do 137 | for k = old_y1,old_y2 do 138 | rawset(self:cell(i,k), obj, nil) 139 | end 140 | end 141 | for i = new_x1,new_x2 do 142 | for k = new_y1,new_y2 do 143 | rawset(self:cell(i,k), obj, obj) 144 | end 145 | end 146 | end 147 | 148 | function Spatialhash:intersectionsWithSegment(x1, y1, x2, y2) 149 | local odx, ody = x2 - x1, y2 - y1 150 | local len, cur = vector.len(odx, ody), 0 151 | local dx, dy = vector.normalize(odx, ody) 152 | local step = self.cell_size / 2 153 | local visited = {} 154 | local points = {} 155 | local mt = math.huge 156 | 157 | while (cur + step < len) do 158 | local cx, cy = x1 + dx * cur, y1 + dy * cur 159 | local shapes = self:cellAt(cx, cy) 160 | cur = cur + step 161 | 162 | for _, shape in pairs(shapes) do 163 | if (not visited[shape]) then 164 | local ints = shape:intersectionsWithRay(x1, y1, dx, dy) 165 | 166 | for _, t in ipairs(ints) do 167 | if (t >= 0 and t <= len) then 168 | local px, py = vector.add(x1, y1, vector.mul(t, dx, dy)) 169 | table.insert(points, {shape, t, px, py}) 170 | end 171 | end 172 | 173 | visited[shape] = true 174 | end 175 | end 176 | end 177 | 178 | table.sort(points, function(a, b) 179 | return a[2] < b[2] 180 | end) 181 | 182 | return points 183 | end 184 | 185 | function Spatialhash:draw(how, show_empty, print_key) 186 | if show_empty == nil then show_empty = true end 187 | for k1,v in pairs(self.cells) do 188 | for k2,cell in pairs(v) do 189 | local is_empty = (next(cell) == nil) 190 | if show_empty or not is_empty then 191 | local x = k1 * self.cell_size 192 | local y = k2 * self.cell_size 193 | love.graphics.rectangle(how or 'line', x,y, self.cell_size, self.cell_size) 194 | 195 | if print_key then 196 | love.graphics.print(("%d:%d"):format(k1,k2), x+3,y+3) 197 | end 198 | end 199 | end 200 | end 201 | end 202 | 203 | return common_local.class('Spatialhash', Spatialhash) 204 | -------------------------------------------------------------------------------- /docs/Polygon.rst: -------------------------------------------------------------------------------- 1 | HC.polygon 2 | ============== 3 | 4 | :: 5 | 6 | polygon = require 'HC.polygon' 7 | 8 | Polygon class with some handy algorithms. Does not provide collision detection 9 | - this functionality is provided by :func:`newPolygonShape` instead. 10 | 11 | .. class:: Polygon(x1,y1, ..., xn,yn) 12 | 13 | :param numbers x1,y1, ..., xn,yn: The corners of the polygon. At least three corners are needed. 14 | :returns: The polygon object. 15 | 16 | Construct a polygon. 17 | 18 | At least three points that are not collinear (i.e. not lying on a straight 19 | line) are needed to construct the polygon. If there are collinear points, these 20 | points will be removed. The shape of the polygon is not changed. 21 | 22 | .. note:: 23 | The syntax depends on used class system. The shown syntax works when using 24 | the bundled `hump.class `_ or 25 | `slither `_. 26 | 27 | **Example**:: 28 | 29 | Polygon = require 'HC.polygon' 30 | poly = Polygon(10,10, 40,50, 70,10, 40,30) 31 | 32 | 33 | .. function:: Polygon:unpack() 34 | 35 | :returns: ``x1,y1, ..., xn,yn`` - The vertices of the polygon. 36 | 37 | Get the polygon's vertices. Useful for drawing with ``love.graphics.polygon()``. 38 | 39 | **Example**:: 40 | 41 | love.graphics.draw('line', poly:unpack()) 42 | 43 | 44 | .. function:: Polygon:clone() 45 | 46 | :returns: A copy of the polygon. 47 | 48 | Get a copy of the polygon. 49 | 50 | .. note:: 51 | Since Lua uses references when simply assigning an existing polygon to a 52 | variable, unexpected things can happen when operating on the variable. Consider 53 | this code:: 54 | 55 | p1 = Polygon(10,10, 40,50, 70,10, 40,30) 56 | p2 = p1 -- p2 is a reference 57 | p3 = p1:clone() -- p3 is a clone 58 | p2:rotate(math.pi) -- p1 will be rotated, too! 59 | p3:rotate(-math.pi) -- only p3 will be rotated 60 | 61 | **Example**:: 62 | 63 | copy = poly:clone() 64 | copy:move(10,20) 65 | 66 | 67 | .. function:: Polygon:bbox() 68 | 69 | :returns: ``x1, y1, x2, y2`` - Corners of the counding box. 70 | 71 | Get axis aligned bounding box. 72 | ``x1, y1`` defines the upper left corner, while ``x2, y2`` define the lower 73 | right corner. 74 | 75 | **Example**:: 76 | 77 | x1,y1,x2,y2 = poly:bbox() 78 | -- draw bounding box 79 | love.graphics.rectangle('line', x1,y2, x2-x1, y2-y1) 80 | 81 | 82 | .. function:: Polygon:isConvex() 83 | 84 | :returns: ``true`` if the polygon is convex, ``false`` otherwise. 85 | 86 | Test if a polygon is convex, i.e. a line line between any two points inside the 87 | polygon will lie in the interior of the polygon. 88 | 89 | **Example**:: 90 | 91 | -- split into convex sub polygons 92 | if not poly:isConvex() then 93 | list = poly:splitConvex() 94 | else 95 | list = {poly:clone()} 96 | end 97 | 98 | 99 | .. function:: Polygon:move(x,y) 100 | 101 | :param numbers x, y: Coordinates of the direction to move. 102 | 103 | Move a polygon in a direction.. 104 | 105 | **Example**:: 106 | 107 | poly:move(10,-5) -- move 10 units right and 5 units up 108 | 109 | 110 | .. function:: Polygon:rotate(angle[, cx, cy]) 111 | 112 | :param number angle: The angle to rotate in radians. 113 | :param numbers cx, cy: The rotation center (optional). 114 | 115 | Rotate the polygon. You can define a rotation center. If it is omitted, the 116 | polygon will be rotated around it's centroid. 117 | 118 | **Example**:: 119 | 120 | p1:rotate(math.pi/2) -- rotate p1 by 90° around it's center 121 | p2:rotate(math.pi/4, 100,100) -- rotate p2 by 45° around the point 100,100 122 | 123 | 124 | .. function:: Polygon:triangulate() 125 | 126 | :returns: ``table`` of Polygons: Triangles that the polygon is composed of. 127 | 128 | Split the polygon into triangles. 129 | 130 | **Example**:: 131 | 132 | triangles = poly:triangulate() 133 | for i,triangle in ipairs(triangles) do 134 | triangles.move(math.random(5,10), math.random(5,10)) 135 | end 136 | 137 | 138 | .. function:: Polygon:splitConvex() 139 | 140 | :returns: ``table`` of Polygons: Convex polygons that form the original polygon. 141 | 142 | Split the polygon into convex sub polygons. 143 | 144 | **Example**:: 145 | 146 | convex = concave_polygon:splitConvex() 147 | function love.draw() 148 | for i,poly in ipairs(convex) do 149 | love.graphics.polygon('fill', poly:unpack()) 150 | end 151 | end 152 | 153 | 154 | .. function:: Polygon:mergedWith(other) 155 | 156 | :param Polygon other: The polygon to merge with. 157 | :returns: The merged polygon, or nil if the two polygons don't share an edge. 158 | 159 | Create a merged polygon of two polygons **if, and only if** the two polygons 160 | share one complete edge. If the polygons share more than one edge, the result 161 | may be erroneous. 162 | 163 | This function does not change either polygon, but rather creates a new one. 164 | 165 | **Example**:: 166 | 167 | merged = p1:mergedWith(p2) 168 | 169 | 170 | .. function:: Polygon:contains(x, y) 171 | 172 | :param numbers x, y: Point to test. 173 | :returns: ``true`` if ``x,y`` lies in the interior of the polygon. 174 | 175 | Test if the polygon contains a given point. 176 | 177 | **Example**:: 178 | 179 | if button:contains(love.mouse.getPosition()) then 180 | button:setHovered(true) 181 | end 182 | 183 | 184 | .. function:: Polygon:intersectionsWithRay(x, y, dx, dy) 185 | 186 | :param numbers x, y: Starting point of the ray. 187 | :param numbers dx, dy: Direction of the ray. 188 | :returns: Table of ray parameters. 189 | 190 | Test if the polygon intersects the given ray. 191 | The ray parameters of the intersections are returned as a table. 192 | The position of the intersections can be computed as 193 | ``(x,y) + ray_parameter * (dx, dy)``. 194 | 195 | 196 | .. function:: Polygon:intersectsRay(x, y, dx, dy) 197 | 198 | :param numbers x, y: Starting point of the ray. 199 | :param numbers dx, dy: Direction of the ray. 200 | :returns: ``intersects, ray_parameter`` - intersection indicator and ray paremter. 201 | 202 | Test if the polygon intersects a ray. 203 | If the shape intersects the ray, the point of intersection can be computed by 204 | ``(x,y) + ray_parameter * (dx, dy)``. 205 | 206 | 207 | **Example**:: 208 | 209 | if poly:intersectsRay(400,300, dx,dy) then 210 | love.graphics.setLine(2) -- highlight polygon 211 | end 212 | 213 | 214 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/HC.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/HC.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/HC" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/HC" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/Shapes.rst: -------------------------------------------------------------------------------- 1 | HC.shapes 2 | ========= 3 | 4 | :: 5 | 6 | shapes = require 'HC.shapes' 7 | 8 | Shape classes with collision detection methods. 9 | 10 | This module defines methods to move, rotate and draw shapes created with the 11 | main module. 12 | 13 | As each shape is at it's core a Lua table, you can attach values and add 14 | functions to it. Be careful not to use keys that name a function or start with 15 | an underscore, e.g. `move` or `_rotation`, since these are used internally. 16 | Everything else is fine. 17 | 18 | If you don't want to use the full blown module, you can still use these classes 19 | to test for colliding shapes. 20 | This may be useful for scenes where the shapes don't move very much and only 21 | few collisions are of interest - for example graphical user interfaces. 22 | 23 | 24 | .. _shape-baseclass: 25 | 26 | Base Class 27 | ---------- 28 | 29 | .. class:: Shape(type) 30 | 31 | :param any type: Arbitrary type identifier of the shape's type. 32 | 33 | Base class for all shapes. All shapes must conform to the interface defined below. 34 | 35 | .. function:: Shape:move(dx, dy) 36 | 37 | :param numbers dx,dy: Coordinates to move the shape by. 38 | 39 | Move the shape *by* some distance. 40 | 41 | **Example**:: 42 | 43 | circle:move(10, 15) -- move the circle 10 units right and 15 units down. 44 | 45 | 46 | .. function:: Shape:moveTo(x, y) 47 | 48 | :param numbers x,y: Coordinates to move the shape to. 49 | 50 | Move the shape *to* some point. Most shapes will be *centered* on the point 51 | ``(x,y)``. 52 | 53 | .. note:: 54 | Equivalent to:: 55 | 56 | local cx,cy = shape:center() 57 | shape:move(x-cx, y-cy) 58 | 59 | **Example**:: 60 | 61 | local x,y = love.mouse.getPosition() 62 | circle:moveTo(x, y) -- move the circle with the mouse 63 | 64 | 65 | .. function:: Shape:center() 66 | 67 | :returns: ``x, y`` - The center of the shape. 68 | 69 | Get the shape's center. 70 | 71 | **Example**:: 72 | 73 | local cx, cy = circle:center() 74 | print("Circle at:", cx, cy) 75 | 76 | .. function:: Shape:rotate(angle[, cx, cy]) 77 | 78 | :param number angle: Amount of rotation in radians. 79 | :param numbers cx, cy: Rotation center; defaults to the shape's center if omitted (optional). 80 | 81 | Rotate the shape *by* some angle. A rotation center can be specified. If no 82 | center is given, rotate around the center of the shape. 83 | 84 | **Example**:: 85 | 86 | rectangle:rotate(math.pi/4) 87 | 88 | 89 | .. function:: Shape:setRotation(angle[, cx, cy]) 90 | 91 | :param number angle: Amount of rotation in radians. 92 | :param numbers cx, cy: Rotation center; defaults to the shape's center if omitted (optional). 93 | 94 | Set the rotation of a shape. A rotation center can be specified. If no center 95 | is given, rotate around the center of the shape. 96 | 97 | .. note:: 98 | Equivalent to:: 99 | 100 | shape:rotate(angle - shape:rotation(), cx,cy) 101 | 102 | **Example**:: 103 | 104 | rectangle:setRotation(math.pi, 100,100) 105 | 106 | 107 | .. function:: Shape:rotation() 108 | 109 | :returns: The shape's rotation in radians. 110 | 111 | Get the rotation of the shape in radians. 112 | 113 | 114 | .. function:: Shape:scale(s) 115 | 116 | :param number s: Scale factor; must be > 0. 117 | 118 | Scale the shape relative to it's center. 119 | 120 | .. note:: 121 | 122 | There is no way to query the scale of a shape. 123 | 124 | **Example**:: 125 | 126 | circle:scale(2) -- double the size 127 | 128 | 129 | .. function:: Shape:outcircle() 130 | 131 | :returns: ``x, y, r`` - Parameters of the outcircle. 132 | 133 | Get parameters of a circle that fully encloses the shape. 134 | 135 | **Example**:: 136 | 137 | if player:hasShield() then 138 | love.graphics.circle('line', player:outcircle()) 139 | end 140 | 141 | .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 142 | 143 | .. function:: Shape:bbox() 144 | 145 | :returns: ``x1, y1, x2, y2`` - Corners of the counding box. 146 | 147 | Get axis aligned bounding box. 148 | ``x1, y1`` defines the upper left corner, while ``x2, y2`` define the lower 149 | right corner. 150 | 151 | **Example**:: 152 | 153 | -- draw bounding box 154 | local x1,y1, x2,y2 = shape:bbox() 155 | love.graphics.rectangle('line', x1,y1, x2-x1,y2-y1) 156 | 157 | 158 | .. function:: Shape:draw(mode) 159 | 160 | :param DrawMode mode: How to draw the shape. Either 'line' or 'fill'. 161 | 162 | Draw the shape either filled or as outline. Mostly for debug-purposes. 163 | 164 | **Example**:: 165 | 166 | circle:draw('fill') 167 | 168 | .. function:: Shape:support(dx,dy) 169 | 170 | :param numbers dx, dy: Search direction. 171 | :returns: The furthest vertex in direction `dx, dy`. 172 | 173 | Get furthest vertex of the shape with respect to the direction ``dx, dy``. 174 | 175 | Used in the collision detection algorithm, but may be useful for other things - 176 | e.g. lighting - too. 177 | 178 | **Example**:: 179 | 180 | -- get vertices that produce a shadow volume 181 | local x1,y1 = circle:support(lx, ly) 182 | local x2,y2 = circle:support(-lx, -ly) 183 | 184 | 185 | .. function:: Shape:collidesWith(other) 186 | 187 | :param Shape other: Test for collision with this shape. 188 | :returns: ``collide, dx, dy`` - Collision indicator and separating vector. 189 | 190 | Test if two shapes collide. 191 | 192 | The separating vector ``dx, dy`` will only be defined if ``collide`` is ``true``. 193 | If defined, the separating vector will point in the direction of ``other``, 194 | i.e. ``dx, dy`` is the direction and magnitude to move ``other`` so that the 195 | shapes do not collide anymore. 196 | 197 | **Example**:: 198 | 199 | if circle:collidesWith(rectangle) then 200 | print("collision detected!") 201 | end 202 | 203 | 204 | .. function:: Shape:contains(x, y) 205 | 206 | :param numbers x, y: Point to test. 207 | :returns: ``true`` if ``x,y`` lies in the interior of the shape. 208 | 209 | Test if the shape contains a given point. 210 | 211 | **Example**:: 212 | 213 | if unit.shape:contains(love.mouse.getPosition) then 214 | unit:setHovered(true) 215 | end 216 | 217 | 218 | .. function:: Shape:intersectionsWithRay(x, y, dx, dy) 219 | 220 | :param numbers x, y: Starting point of the ray. 221 | :param numbers dx, dy: Direction of the ray. 222 | :returns: Table of ray parameters. 223 | 224 | Test if the shape intersects the given ray. 225 | The ray parameters of the intersections are returned as a table. 226 | The position of the intersections can be computed as 227 | ``(x,y) + ray_parameter * (dx, dy)``. 228 | 229 | 230 | **Example**:: 231 | 232 | local ts = player:intersectionsWithRay(x,y, dx,dy) 233 | for _, t in ipairs(t) do 234 | -- find point of intersection 235 | local vx,vy = vector.add(x, y, vector.mul(t, dx, dy)) 236 | player:addMark(vx,vy) 237 | end 238 | 239 | 240 | .. function:: Shape:intersectsRay(x, y, dx, dy) 241 | 242 | :param numbers x, y: Starting point of the ray. 243 | :param numbers dx, dy: Direction of the ray. 244 | :returns: ``intersects, ray_parameter`` - intersection indicator and ray paremter. 245 | 246 | Test if the shape intersects the given ray. 247 | If the shape intersects the ray, the point of intersection can be computed by 248 | ``(x,y) + ray_parameter * (dx, dy)``. 249 | 250 | 251 | **Example**:: 252 | 253 | local intersecting, t = player:intersectsRay(x,y, dx,dy) 254 | if intersecting then 255 | -- find point of intersection 256 | local vx,vy = vector.add(x, y, vector.mul(t, dx, dy)) 257 | player:addMark(vx,vy) 258 | end 259 | 260 | 261 | 262 | .. _custom-shapes: 263 | 264 | Custom Shapes 265 | ------------- 266 | 267 | Custom shapes must implement at least the following methods (as defined above) 268 | 269 | - :func:`Shape:move` 270 | - :func:`Shape:rotate` 271 | - :func:`Shape:scale` 272 | - :func:`Shape:bbox` 273 | - :func:`Shape:collidesWith` 274 | 275 | 276 | .. _builtin-shapes: 277 | 278 | Built-in Shapes 279 | --------------- 280 | 281 | .. class:: ConcavePolygonShape 282 | 283 | .. class:: ConvexPolygonShape 284 | 285 | .. class:: CircleShape 286 | 287 | .. function:: CircleShape:draw(mode, segments) 288 | 289 | :param DrawMode mode: How to draw the shape. Either 'line' or 'fill'. 290 | :param number segments: The number of segments to draw the circle with. 291 | 292 | Draw the circle shape either filled or as an outline and with the specified number of segments. 293 | 294 | .. class:: PointShape 295 | 296 | .. function:: newPolygonShape(...) 297 | 298 | :param numbers ...: Vertices of the :class:`Polygon`. 299 | :returns: :class:`ConcavePolygonShape` or :class:`ConvexPolygonShape`. 300 | 301 | .. function:: newCircleShape(cx, cy, radius) 302 | 303 | :param numbers cx,cy: Center of the circle. 304 | :param number radius: Radius of the circle. 305 | :returns: :class:`CircleShape`. 306 | 307 | .. function:: newPointShape 308 | 309 | :param numbers x, y: Position of the point. 310 | :returns: :class:`PointShape`. 311 | -------------------------------------------------------------------------------- /docs/MainModule.rst: -------------------------------------------------------------------------------- 1 | Main Module 2 | =========== 3 | 4 | :: 5 | 6 | HC = require 'HC' 7 | 8 | The purpose of the main modules is to connect shapes with the spatial hash -- a 9 | data structure to quickly look up neighboring shapes -- and to provide 10 | utilities to tell which shapes intersect (collide) with each other. 11 | 12 | Most of the time, HC will be run as a singleton; you can, however, also create 13 | several instances, that each hold their own little worlds. 14 | 15 | Initialization 16 | -------------- 17 | 18 | .. function:: HC.new([cell_size = 100]) 19 | 20 | :param number cell_size: Resolution of the internal search structure (optional). 21 | :returns: Collider instance. 22 | 23 | Creates a new collider instance that holds a separate spatial hash. 24 | Collider instances carry the same methods as the main module. 25 | The only difference is that function calls must use the colon-syntax (see 26 | below). 27 | 28 | Useful if you want to maintain several collision layers or several separate 29 | game worlds. 30 | 31 | The ``cell_size`` somewhat governs the performance of :func:`HC.neighbors` and 32 | :func:`HC.collisions`. How this parameter affects the performance depends on 33 | how many shapes there are, how big these shapes are and (somewhat) how the 34 | shapes are distributed. 35 | A rule of thumb would be to set ``cell_size`` to be about four times the size 36 | of the average object. 37 | Or just leave it as is and worry about it only if you run into performance 38 | problems that can be traced back to the spatial hash. 39 | 40 | **Example**:: 41 | 42 | collider = HC.new(150) -- create separate world 43 | 44 | -- method calls with colon syntax 45 | ball = collider:circle(100,100,20) 46 | rect = collider:rectangle(110,90,20,100) 47 | 48 | for shape, delta in pairs(collider:collisions(ball)) do 49 | shape:move(delta.x, delta.y) 50 | end 51 | 52 | 53 | .. function:: HC.resetHash([cell_size = 100]) 54 | 55 | :param number cell_size: Resolution of the internal search structure (optional). 56 | 57 | Reset the internal search structure, the spatial hash. 58 | This clears *all* shapes that were registered beforehand, meaning that HC will 59 | not be able to find any collisions with those shapes anymore. 60 | 61 | **Example**:: 62 | 63 | function new_stage() 64 | actors = {} -- clear the stage on our side 65 | HC.resetHash() -- as well as on HC's side 66 | end 67 | 68 | 69 | 70 | Shapes 71 | ------ 72 | 73 | See also the :doc:`Shapes` sub-module. 74 | 75 | .. function:: HC.rectangle(x, y, w, h) 76 | 77 | :param numbers x,y: Upper left corner of the rectangle. 78 | :param numbers w,h: Width and height of the rectangle. 79 | :returns: The rectangle :class:`Shape` added to the scene. 80 | 81 | Add a rectangle shape to the scene. 82 | 83 | .. note:: 84 | :class:`Shape` transformations, e.g. :func:`Shape.moveTo` and 85 | :func:`Shape.rotate` will be with respect to the *center, not* the upper left 86 | corner of the rectangle! 87 | 88 | **Example**:: 89 | 90 | rect = HC.rectangle(100, 120, 200, 40) 91 | rect:rotate(23) 92 | 93 | 94 | .. function:: HC.polygon(x1,y1,...,xn,yn) 95 | 96 | :param numbers x1,y1,...,xn,yn: The corners of the polygon. At least three 97 | corners that do not lie on a straight line 98 | are required. 99 | :returns: The polygon :class:`Shape` added to the scene. 100 | 101 | Add a polygon to the scene. Any non-self-intersection polygon will work. 102 | The polygon will be closed; the first and the last point do not need to be the 103 | same. 104 | 105 | .. note:: 106 | If three consecutive points lie on a line, the middle point will be discarded. 107 | This means you cannot construct polygon shapes that are lines. 108 | 109 | .. note:: 110 | :class:`Shape` transformations, e.g. :func:`Shape.moveTo` and 111 | :func:`Shape.rotate` will be with respect to the center of the polygon. 112 | 113 | **Example**:: 114 | 115 | shape = HC.polygon(10,10, 40,50, 70,10, 40,30) 116 | shape:move(42, 5) 117 | 118 | 119 | .. function:: HC.circle(cx, cy, radius) 120 | 121 | :param numbers cx,cy: Center of the circle. 122 | :param number radius: Radius of the circle. 123 | :returns: The circle :class:`Shape` added to the scene. 124 | 125 | Add a circle shape to the scene. 126 | 127 | **Example**:: 128 | 129 | circle = HC.circle(400, 300, 100) 130 | 131 | 132 | .. function:: HC.point(x, y) 133 | 134 | :param numbers x, y: Position of the point. 135 | :returns: The point :class:`Shape` added to the scene. 136 | 137 | Add a point shape to the scene. 138 | 139 | Point shapes are most useful for bullets and such, because detecting collisions 140 | between a point and any other shape is a little faster than detecting collision 141 | between two non-point shapes. In case of a collision, the separating vector 142 | will not be valid. 143 | 144 | **Example**:: 145 | 146 | bullets[#bullets+1] = HC.point(player.pos.x, player.pos.y) 147 | 148 | 149 | .. function:: HC.register(shape) 150 | 151 | :param Shape shape: The :class:`Shape` to add to the spatial hash. 152 | 153 | Add a shape to the bookkeeping system. 154 | :func:`HC.neighbors` and :func:`Hc.collisions` works only with registered 155 | shapes. 156 | You don't need to (and should not) register any shapes created with the above 157 | functions. 158 | 159 | Overwrites :func:`Shape.move`, :func:`Shape.rotate`, and :func:`Shape.scale` 160 | with versions that update the :doc:`SpatialHash`. 161 | 162 | This function is mostly only useful if you provide a custom shape. 163 | See :ref:`custom-shapes`. 164 | 165 | 166 | .. function:: HC.remove(shape) 167 | 168 | :param Shape shape: The :class:`Shape` to remove from the spatial hash. 169 | 170 | Remove a shape to the bookkeeping system. 171 | 172 | .. warning:: 173 | This will also invalidate the functions :func:`Shape.move`, 174 | :func:`Shape.rotate`, and :func:`Shape.scale`. 175 | Make sure you delete the shape from your own actor list(s). 176 | 177 | **Example**:: 178 | 179 | for i = #bullets,1,-1 do 180 | if bullets[i]:collidesWith(player) 181 | player:takeDamage() 182 | 183 | HC.remove(bullets[i]) -- remove bullet from HC 184 | table.remove(bullets, i) -- remove bullet from own actor list 185 | end 186 | end 187 | 188 | 189 | Collision Detection 190 | ------------------- 191 | 192 | .. function:: HC.collisions(shape) 193 | 194 | :param Shape shape: Query shape. 195 | :returns: Table of colliding shapes and separating vectors. 196 | 197 | 198 | Get shapes that are colliding with ``shape`` and the vector to separate the shapes. 199 | The separating vector points in the direction that ``shape`` has to move to clear 200 | the collission. 201 | The length of the vector is the minimal amount that either shape has to move to 202 | clear the collission. 203 | 204 | The table is a *set*, meaning that the shapes are stored in *keys* of the table. 205 | The *values* are the separating vector. 206 | You can iterate over the shapes using ``pairs`` (see example). 207 | 208 | **Example**:: 209 | 210 | local collisions = HC.collisions(shape) 211 | for other, separating_vector in pairs(collisions) do 212 | shape:move( separating_vector.x/2, separating_vector.y/2) 213 | other:move(-separating_vector.x/2, -separating_vector.y/2) 214 | end 215 | 216 | 217 | .. function:: HC.neighbors(shape) 218 | 219 | :param Shape shape: Query shape. 220 | :returns: Table of neighboring shapes, where the keys of the table are the shapes. 221 | 222 | Get other shapes in that are close to ``shape``. 223 | The table is a *set*, meaning that the shapes are stored in *keys* of the table. 224 | You can iterate over the shapes using ``pairs`` (see example). 225 | 226 | .. note:: 227 | The result depends on the size and position of ``shape`` as well as the 228 | grid size of the spatial hash: :func:`HC.neighbors` returns the shapes that 229 | are in the same cell(s) as ``shape``. 230 | 231 | **Example**:: 232 | 233 | local candidates = HC.neighbors(shape) 234 | for other in pairs(candidates) do 235 | local collides, dx, dy = shape:collidesWith(other) 236 | if collides then 237 | other:move(dx, dy) 238 | end 239 | end 240 | 241 | .. function:: HC.shapesAt(x, y) 242 | 243 | :param numbers x,y: Point to query. 244 | :returns: Table of shapes at the point, where the keys of the table are the shapes. 245 | 246 | Get shapes that contain the point (x,y). 247 | The table is a *set*, meaning that the shapes are stored in *keys* of the table. 248 | You can iterate over the shapes using ``pairs`` (see example). 249 | 250 | **Example**:: 251 | 252 | local shapes = HC.shapesAt(love.mouse.getPosition) 253 | for s in pairs(shapes) do 254 | game.selectUnit(s) 255 | end 256 | 257 | .. function:: HC.raycast(x, y, dx, dy, range) 258 | 259 | :param numbers x,y: Origin point of ray 260 | :param numbers dx,dy: Direction vector of ray(normal vector) 261 | :param number range: Range of raycast 262 | :returns: Table of shapes that got hit and its hit points. 263 | 264 | Gets shapes that got hit by a given ray and the points which that shape intersects with the ray. 265 | The table is a *set*, meaning that the shapes are stored in *keys* of the table. The values are the points of intersection. You can iterate over the shapes using ``pairs`` (see example). 266 | 267 | **Example**:: 268 | 269 | local hits = HC.raycast(originx, originy, directionx, directiony, range) 270 | for shape, points in pairs(hits) do 271 | for _, point in ipairs(points) do 272 | love.graphics.points(point.x, point.y) 273 | end 274 | end 275 | 276 | 277 | .. function:: HC.hash() 278 | 279 | :returns: :class:`SpatialHash`. 280 | 281 | Get a reference to the :class:`SpatialHash` instance. 282 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # HC documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Oct 8 20:31:43 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.mathjax', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'HC' 52 | copyright = u'2015, Matthias Richter' 53 | author = u'Matthias Richter' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '0.1' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '0.1-1' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | # If true, `todo` and `todoList` produce output, else they produce nothing. 106 | todo_include_todos = False 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | 114 | # Theme options are theme-specific and customize the look and feel of a theme 115 | # further. For a list of options available for each theme, see the 116 | # documentation. 117 | #html_theme_options = {} 118 | 119 | # Add any paths that contain custom themes here, relative to this directory. 120 | 121 | # The name for this set of Sphinx documents. If None, it defaults to 122 | # " v documentation". 123 | #html_title = None 124 | 125 | # A shorter title for the navigation bar. Default is the same as html_title. 126 | #html_short_title = None 127 | 128 | # The name of an image file (relative to this directory) to place at the top 129 | # of the sidebar. 130 | #html_logo = None 131 | 132 | # The name of an image file (within the static path) to use as favicon of the 133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 134 | # pixels large. 135 | #html_favicon = None 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | html_static_path = ['_static'] 141 | 142 | # Add any extra paths that contain custom files (such as robots.txt or 143 | # .htaccess) here, relative to this directory. These files are copied 144 | # directly to the root of the documentation. 145 | #html_extra_path = [] 146 | 147 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 148 | # using the given strftime format. 149 | #html_last_updated_fmt = '%b %d, %Y' 150 | 151 | # If true, SmartyPants will be used to convert quotes and dashes to 152 | # typographically correct entities. 153 | #html_use_smartypants = True 154 | 155 | # Custom sidebar templates, maps document names to template names. 156 | #html_sidebars = {} 157 | 158 | # Additional templates that should be rendered to pages, maps page names to 159 | # template names. 160 | #html_additional_pages = {} 161 | 162 | # If false, no module index is generated. 163 | #html_domain_indices = True 164 | 165 | # If false, no index is generated. 166 | #html_use_index = True 167 | 168 | # If true, the index is split into individual pages for each letter. 169 | #html_split_index = False 170 | 171 | # If true, links to the reST sources are added to the pages. 172 | #html_show_sourcelink = True 173 | 174 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 175 | #html_show_sphinx = True 176 | 177 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 178 | #html_show_copyright = True 179 | 180 | # If true, an OpenSearch description file will be output, and all pages will 181 | # contain a tag referring to it. The value of this option must be the 182 | # base URL from which the finished HTML is served. 183 | #html_use_opensearch = '' 184 | 185 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 186 | #html_file_suffix = None 187 | 188 | # Language to be used for generating the HTML full-text search index. 189 | # Sphinx supports the following languages: 190 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 191 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 192 | #html_search_language = 'en' 193 | 194 | # A dictionary with options for the search language support, empty by default. 195 | # Now only 'ja' uses this config value 196 | #html_search_options = {'type': 'default'} 197 | 198 | # The name of a javascript file (relative to the configuration directory) that 199 | # implements a search results scorer. If empty, the default will be used. 200 | #html_search_scorer = 'scorer.js' 201 | 202 | # Output file base name for HTML help builder. 203 | htmlhelp_basename = 'HCdoc' 204 | 205 | # -- Options for LaTeX output --------------------------------------------- 206 | 207 | latex_elements = { 208 | # The paper size ('letterpaper' or 'a4paper'). 209 | #'papersize': 'letterpaper', 210 | 211 | # The font size ('10pt', '11pt' or '12pt'). 212 | #'pointsize': '10pt', 213 | 214 | # Additional stuff for the LaTeX preamble. 215 | #'preamble': '', 216 | 217 | # Latex figure (float) alignment 218 | #'figure_align': 'htbp', 219 | } 220 | 221 | # Grouping the document tree into LaTeX files. List of tuples 222 | # (source start file, target name, title, 223 | # author, documentclass [howto, manual, or own class]). 224 | latex_documents = [ 225 | (master_doc, 'HC.tex', u'HC Documentation', 226 | u'Matthias Richter', 'manual'), 227 | ] 228 | 229 | # The name of an image file (relative to this directory) to place at the top of 230 | # the title page. 231 | #latex_logo = None 232 | 233 | # For "manual" documents, if this is true, then toplevel headings are parts, 234 | # not chapters. 235 | #latex_use_parts = False 236 | 237 | # If true, show page references after internal links. 238 | #latex_show_pagerefs = False 239 | 240 | # If true, show URL addresses after external links. 241 | #latex_show_urls = False 242 | 243 | # Documents to append as an appendix to all manuals. 244 | #latex_appendices = [] 245 | 246 | # If false, no module index is generated. 247 | #latex_domain_indices = True 248 | 249 | 250 | # -- Options for manual page output --------------------------------------- 251 | 252 | # One entry per manual page. List of tuples 253 | # (source start file, name, description, authors, manual section). 254 | man_pages = [ 255 | (master_doc, 'hc', u'HC Documentation', 256 | [author], 1) 257 | ] 258 | 259 | # If true, show URL addresses after external links. 260 | #man_show_urls = False 261 | 262 | 263 | # -- Options for Texinfo output ------------------------------------------- 264 | 265 | # Grouping the document tree into Texinfo files. List of tuples 266 | # (source start file, target name, title, author, 267 | # dir menu entry, description, category) 268 | texinfo_documents = [ 269 | (master_doc, 'HC', u'HC Documentation', 270 | author, 'HC', 'One line description of project.', 271 | 'Miscellaneous'), 272 | ] 273 | 274 | # Documents to append as an appendix to all manuals. 275 | #texinfo_appendices = [] 276 | 277 | # If false, no module index is generated. 278 | #texinfo_domain_indices = True 279 | 280 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 281 | #texinfo_show_urls = 'footnote' 282 | 283 | # If true, do not generate a @detailmenu in the "Top" node's menu. 284 | #texinfo_no_detailmenu = False 285 | 286 | 287 | primary_domain = "js" 288 | highlight_language = "lua" 289 | 290 | import sphinx_rtd_theme 291 | html_theme = 'sphinx_rtd_theme' 292 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 293 | 294 | #import sphinx_bootstrap_theme 295 | #html_theme = 'bootstrap' 296 | #html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() 297 | -------------------------------------------------------------------------------- /shapes.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2011 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local math_min, math_sqrt, math_huge = math.min, math.sqrt, math.huge 28 | 29 | local _PACKAGE, common_local = (...):match("^(.+)%.[^%.]+"), common 30 | if not (type(common) == 'table' and common.class and common.instance) then 31 | assert(common_class ~= false, 'No class commons specification available.') 32 | require(_PACKAGE .. '.class') 33 | end 34 | local vector = require(_PACKAGE .. '.vector-light') 35 | local Polygon = require(_PACKAGE .. '.polygon') 36 | local GJK = require(_PACKAGE .. '.gjk') -- actual collision detection 37 | 38 | -- reset global table `common' (required by class commons) 39 | if common_local ~= common then 40 | common_local, common = common, common_local 41 | end 42 | 43 | -- 44 | -- base class 45 | -- 46 | local Shape = {} 47 | function Shape:init(t) 48 | self._type = t 49 | self._rotation = 0 50 | end 51 | 52 | function Shape:moveTo(x,y) 53 | local cx,cy = self:center() 54 | self:move(x - cx, y - cy) 55 | end 56 | 57 | function Shape:rotation() 58 | return self._rotation 59 | end 60 | 61 | function Shape:rotate(angle) 62 | self._rotation = self._rotation + angle 63 | end 64 | 65 | function Shape:setRotation(angle, x,y) 66 | return self:rotate(angle - self._rotation, x,y) 67 | end 68 | 69 | -- 70 | -- class definitions 71 | -- 72 | local ConvexPolygonShape = {} 73 | function ConvexPolygonShape:init(polygon) 74 | Shape.init(self, 'polygon') 75 | assert(polygon:isConvex(), "Polygon is not convex.") 76 | self._polygon = polygon 77 | end 78 | 79 | local ConcavePolygonShape = {} 80 | function ConcavePolygonShape:init(poly) 81 | Shape.init(self, 'compound') 82 | self._polygon = poly 83 | self._shapes = poly:splitConvex() 84 | for i,s in ipairs(self._shapes) do 85 | self._shapes[i] = common_local.instance(ConvexPolygonShape, s) 86 | end 87 | end 88 | 89 | local CircleShape = {} 90 | function CircleShape:init(cx,cy, radius) 91 | Shape.init(self, 'circle') 92 | self._center = {x = cx, y = cy} 93 | self._radius = radius 94 | end 95 | 96 | local PointShape = {} 97 | function PointShape:init(x,y) 98 | Shape.init(self, 'point') 99 | self._pos = {x = x, y = y} 100 | end 101 | 102 | -- 103 | -- collision functions 104 | -- 105 | function ConvexPolygonShape:support(dx,dy) 106 | local v = self._polygon.vertices 107 | local max, vmax = -math_huge 108 | for i = 1,#v do 109 | local d = vector.dot(v[i].x,v[i].y, dx,dy) 110 | if d > max then 111 | max, vmax = d, v[i] 112 | end 113 | end 114 | return vmax.x, vmax.y 115 | end 116 | 117 | function CircleShape:support(dx,dy) 118 | return vector.add(self._center.x, self._center.y, 119 | vector.mul(self._radius, vector.normalize(dx,dy))) 120 | end 121 | 122 | -- collision dispatching: 123 | -- let circle shape or compund shape handle the collision 124 | function ConvexPolygonShape:collidesWith(other) 125 | if self == other then return false end 126 | if other._type ~= 'polygon' then 127 | local collide, sx,sy = other:collidesWith(self) 128 | return collide, sx and -sx, sy and -sy 129 | end 130 | 131 | -- else: type is POLYGON 132 | return GJK(self, other) 133 | end 134 | 135 | function ConcavePolygonShape:collidesWith(other) 136 | if self == other then return false end 137 | if other._type == 'point' then 138 | return other:collidesWith(self) 139 | end 140 | 141 | -- TODO: better way of doing this. report all the separations? 142 | local collide,dx,dy = false,0,0 143 | for _,s in ipairs(self._shapes) do 144 | local status, sx,sy = s:collidesWith(other) 145 | collide = collide or status 146 | if status then 147 | if math.abs(dx) < math.abs(sx) then 148 | dx = sx 149 | end 150 | if math.abs(dy) < math.abs(sy) then 151 | dy = sy 152 | end 153 | end 154 | end 155 | return collide, dx, dy 156 | end 157 | 158 | function CircleShape:collidesWith(other) 159 | if self == other then return false end 160 | if other._type == 'circle' then 161 | local px,py = self._center.x-other._center.x, self._center.y-other._center.y 162 | local d = vector.len2(px,py) 163 | local radii = self._radius + other._radius 164 | if d < radii*radii then 165 | -- if circles overlap, push it out upwards 166 | if d == 0 then return true, 0,radii end 167 | -- otherwise push out in best direction 168 | return true, vector.mul(radii - math_sqrt(d), vector.normalize(px,py)) 169 | end 170 | return false 171 | elseif other._type == 'polygon' then 172 | return GJK(self, other) 173 | end 174 | 175 | -- else: let the other shape decide 176 | local collide, sx,sy = other:collidesWith(self) 177 | return collide, sx and -sx, sy and -sy 178 | end 179 | 180 | function PointShape:collidesWith(other) 181 | if self == other then return false end 182 | if other._type == 'point' then 183 | return (self._pos == other._pos), 0,0 184 | end 185 | return other:contains(self._pos.x, self._pos.y), 0,0 186 | end 187 | 188 | -- 189 | -- point location/ray intersection 190 | -- 191 | function ConvexPolygonShape:contains(x,y) 192 | return self._polygon:contains(x,y) 193 | end 194 | 195 | function ConcavePolygonShape:contains(x,y) 196 | return self._polygon:contains(x,y) 197 | end 198 | 199 | function CircleShape:contains(x,y) 200 | return vector.len2(x-self._center.x, y-self._center.y) < self._radius * self._radius 201 | end 202 | 203 | function PointShape:contains(x,y) 204 | return x == self._pos.x and y == self._pos.y 205 | end 206 | 207 | 208 | function ConcavePolygonShape:intersectsRay(x,y, dx,dy) 209 | return self._polygon:intersectsRay(x,y, dx,dy) 210 | end 211 | 212 | function ConvexPolygonShape:intersectsRay(x,y, dx,dy) 213 | return self._polygon:intersectsRay(x,y, dx,dy) 214 | end 215 | 216 | function ConcavePolygonShape:intersectionsWithRay(x,y, dx,dy) 217 | return self._polygon:intersectionsWithRay(x,y, dx,dy) 218 | end 219 | 220 | function ConvexPolygonShape:intersectionsWithRay(x,y, dx,dy) 221 | return self._polygon:intersectionsWithRay(x,y, dx,dy) 222 | end 223 | 224 | -- circle intersection if distance of ray/center is smaller 225 | -- than radius. 226 | -- with r(s) = p + d*s = (x,y) + (dx,dy) * s defining the ray and 227 | -- (x - cx)^2 + (y - cy)^2 = r^2, this problem is eqivalent to 228 | -- solving [with c = (cx,cy)]: 229 | -- 230 | -- d*d s^2 + 2 d*(p-c) s + (p-c)*(p-c)-r^2 = 0 231 | function CircleShape:intersectionsWithRay(x,y, dx,dy) 232 | local pcx,pcy = x-self._center.x, y-self._center.y 233 | 234 | local a = vector.len2(dx,dy) 235 | local b = 2 * vector.dot(dx,dy, pcx,pcy) 236 | local c = vector.len2(pcx,pcy) - self._radius * self._radius 237 | local discr = b*b - 4*a*c 238 | 239 | if discr < 0 then return {} end 240 | 241 | discr = math_sqrt(discr) 242 | local ts, t1, t2 = {}, discr-b, -discr-b 243 | if t1 >= 0 then ts[#ts+1] = t1/(2*a) end 244 | if t2 >= 0 then ts[#ts+1] = t2/(2*a) end 245 | return ts 246 | end 247 | 248 | function CircleShape:intersectsRay(x,y, dx,dy) 249 | local tmin = math_huge 250 | for _, t in ipairs(self:intersectionsWithRay(x,y,dx,dy)) do 251 | tmin = math_min(t, tmin) 252 | end 253 | return tmin ~= math_huge, tmin 254 | end 255 | 256 | -- point shape intersects ray if it lies on the ray 257 | function PointShape:intersectsRay(x,y, dx,dy) 258 | local px,py = self._pos.x-x, self._pos.y-y 259 | local t = px/dx 260 | -- see (px,py) and (dx,dy) point in same direction 261 | return (t == py/dy), t 262 | end 263 | 264 | function PointShape:intersectionsWithRay(x,y, dx,dy) 265 | local intersects, t = self:intersectsRay(x,y, dx,dy) 266 | return intersects and {t} or {} 267 | end 268 | 269 | -- 270 | -- auxiliary 271 | -- 272 | function ConvexPolygonShape:center() 273 | return self._polygon.centroid.x, self._polygon.centroid.y 274 | end 275 | 276 | function ConcavePolygonShape:center() 277 | return self._polygon.centroid.x, self._polygon.centroid.y 278 | end 279 | 280 | function CircleShape:center() 281 | return self._center.x, self._center.y 282 | end 283 | 284 | function PointShape:center() 285 | return self._pos.x, self._pos.y 286 | end 287 | 288 | function ConvexPolygonShape:outcircle() 289 | local cx,cy = self:center() 290 | return cx,cy, self._polygon._radius 291 | end 292 | 293 | function ConcavePolygonShape:outcircle() 294 | local cx,cy = self:center() 295 | return cx,cy, self._polygon._radius 296 | end 297 | 298 | function CircleShape:outcircle() 299 | local cx,cy = self:center() 300 | return cx,cy, self._radius 301 | end 302 | 303 | function PointShape:outcircle() 304 | return self._pos.x, self._pos.y, 0 305 | end 306 | 307 | function ConvexPolygonShape:bbox() 308 | return self._polygon:bbox() 309 | end 310 | 311 | function ConcavePolygonShape:bbox() 312 | return self._polygon:bbox() 313 | end 314 | 315 | function CircleShape:bbox() 316 | local cx,cy = self:center() 317 | local r = self._radius 318 | return cx-r,cy-r, cx+r,cy+r 319 | end 320 | 321 | function PointShape:bbox() 322 | local x,y = self:center() 323 | return x,y,x,y 324 | end 325 | 326 | 327 | function ConvexPolygonShape:move(x,y) 328 | self._polygon:move(x,y) 329 | end 330 | 331 | function ConcavePolygonShape:move(x,y) 332 | self._polygon:move(x,y) 333 | for _,p in ipairs(self._shapes) do 334 | p:move(x,y) 335 | end 336 | end 337 | 338 | function CircleShape:move(x,y) 339 | self._center.x = self._center.x + x 340 | self._center.y = self._center.y + y 341 | end 342 | 343 | function PointShape:move(x,y) 344 | self._pos.x = self._pos.x + x 345 | self._pos.y = self._pos.y + y 346 | end 347 | 348 | 349 | function ConcavePolygonShape:rotate(angle,cx,cy) 350 | Shape.rotate(self, angle) 351 | if not (cx and cy) then 352 | cx,cy = self:center() 353 | end 354 | self._polygon:rotate(angle,cx,cy) 355 | for _,p in ipairs(self._shapes) do 356 | p:rotate(angle, cx,cy) 357 | end 358 | end 359 | 360 | function ConvexPolygonShape:rotate(angle, cx,cy) 361 | Shape.rotate(self, angle) 362 | self._polygon:rotate(angle, cx, cy) 363 | end 364 | 365 | function CircleShape:rotate(angle, cx,cy) 366 | Shape.rotate(self, angle) 367 | if not (cx and cy) then return end 368 | self._center.x,self._center.y = vector.add(cx,cy, vector.rotate(angle, self._center.x-cx, self._center.y-cy)) 369 | end 370 | 371 | function PointShape:rotate(angle, cx,cy) 372 | Shape.rotate(self, angle) 373 | if not (cx and cy) then return end 374 | self._pos.x,self._pos.y = vector.add(cx,cy, vector.rotate(angle, self._pos.x-cx, self._pos.y-cy)) 375 | end 376 | 377 | 378 | function ConcavePolygonShape:scale(s) 379 | assert(type(s) == "number" and s > 0, "Invalid argument. Scale must be greater than 0") 380 | local cx,cy = self:center() 381 | self._polygon:scale(s, cx,cy) 382 | for _, p in ipairs(self._shapes) do 383 | local dx,dy = vector.sub(cx,cy, p:center()) 384 | p:scale(s) 385 | p:moveTo(cx-dx*s, cy-dy*s) 386 | end 387 | end 388 | 389 | function ConvexPolygonShape:scale(s) 390 | assert(type(s) == "number" and s > 0, "Invalid argument. Scale must be greater than 0") 391 | self._polygon:scale(s, self:center()) 392 | end 393 | 394 | function CircleShape:scale(s) 395 | assert(type(s) == "number" and s > 0, "Invalid argument. Scale must be greater than 0") 396 | self._radius = self._radius * s 397 | end 398 | 399 | function PointShape:scale() 400 | -- nothing 401 | end 402 | 403 | 404 | function ConvexPolygonShape:draw(mode) 405 | mode = mode or 'line' 406 | love.graphics.polygon(mode, self._polygon:unpack()) 407 | end 408 | 409 | function ConcavePolygonShape:draw(mode, wireframe) 410 | local mode = mode or 'line' 411 | if mode == 'line' then 412 | love.graphics.polygon('line', self._polygon:unpack()) 413 | if not wireframe then return end 414 | end 415 | for _,p in ipairs(self._shapes) do 416 | love.graphics.polygon(mode, p._polygon:unpack()) 417 | end 418 | end 419 | 420 | function CircleShape:draw(mode, segments) 421 | local x, y, r = self:outcircle() 422 | love.graphics.circle(mode or 'line', x, y, r, segments) 423 | end 424 | 425 | function PointShape:draw() 426 | (love.graphics.points or love.graphics.point)(self:center()) 427 | end 428 | 429 | 430 | Shape = common_local.class('Shape', Shape) 431 | ConvexPolygonShape = common_local.class('ConvexPolygonShape', ConvexPolygonShape, Shape) 432 | ConcavePolygonShape = common_local.class('ConcavePolygonShape', ConcavePolygonShape, Shape) 433 | CircleShape = common_local.class('CircleShape', CircleShape, Shape) 434 | PointShape = common_local.class('PointShape', PointShape, Shape) 435 | 436 | local function newPolygonShape(polygon, ...) 437 | -- create from coordinates if needed 438 | if type(polygon) == "number" then 439 | polygon = common_local.instance(Polygon, polygon, ...) 440 | else 441 | polygon = polygon:clone() 442 | end 443 | 444 | if polygon:isConvex() then 445 | return common_local.instance(ConvexPolygonShape, polygon) 446 | end 447 | 448 | return common_local.instance(ConcavePolygonShape, polygon) 449 | end 450 | 451 | local function newCircleShape(...) 452 | return common_local.instance(CircleShape, ...) 453 | end 454 | 455 | local function newPointShape(...) 456 | return common_local.instance(PointShape, ...) 457 | end 458 | 459 | return { 460 | ConcavePolygonShape = ConcavePolygonShape, 461 | ConvexPolygonShape = ConvexPolygonShape, 462 | CircleShape = CircleShape, 463 | PointShape = PointShape, 464 | newPolygonShape = newPolygonShape, 465 | newCircleShape = newCircleShape, 466 | newPointShape = newPointShape, 467 | } 468 | 469 | -------------------------------------------------------------------------------- /polygon.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2011 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local _PACKAGE, common_local = (...):match("^(.+)%.[^%.]+"), common 28 | if not (type(common) == 'table' and common.class and common.instance) then 29 | assert(common_class ~= false, 'No class commons specification available.') 30 | require(_PACKAGE .. '.class') 31 | common_local, common = common, common_local 32 | end 33 | local vector = require(_PACKAGE .. '.vector-light') 34 | 35 | ---------------------------- 36 | -- Private helper functions 37 | -- 38 | -- create vertex list of coordinate pairs 39 | local function toVertexList(vertices, x,y, ...) 40 | if not (x and y) then return vertices end -- no more arguments 41 | 42 | vertices[#vertices + 1] = {x = x, y = y} -- set vertex 43 | return toVertexList(vertices, ...) -- recurse 44 | end 45 | 46 | -- returns true if three vertices lie on a line 47 | local function areCollinear(p, q, r, eps) 48 | return math.abs(vector.det(q.x-p.x, q.y-p.y, r.x-p.x,r.y-p.y)) <= (eps or 1e-32) 49 | end 50 | -- remove vertices that lie on a line 51 | local function removeCollinear(vertices) 52 | local ret = {} 53 | local i,k = #vertices - 1, #vertices 54 | for l=1,#vertices do 55 | if not areCollinear(vertices[i], vertices[k], vertices[l]) then 56 | ret[#ret+1] = vertices[k] 57 | end 58 | i,k = k,l 59 | end 60 | return ret 61 | end 62 | 63 | -- get index of rightmost vertex (for testing orientation) 64 | local function getIndexOfleftmost(vertices) 65 | local idx = 1 66 | for i = 2,#vertices do 67 | if vertices[i].x < vertices[idx].x then 68 | idx = i 69 | end 70 | end 71 | return idx 72 | end 73 | 74 | -- returns true if three points make a counter clockwise turn 75 | local function ccw(p, q, r) 76 | return vector.det(q.x-p.x, q.y-p.y, r.x-p.x, r.y-p.y) >= 0 77 | end 78 | 79 | -- test wether a and b lie on the same side of the line c->d 80 | local function onSameSide(a,b, c,d) 81 | local px, py = d.x-c.x, d.y-c.y 82 | local l = vector.det(px,py, a.x-c.x, a.y-c.y) 83 | local m = vector.det(px,py, b.x-c.x, b.y-c.y) 84 | return l*m >= 0 85 | end 86 | 87 | local function pointInTriangle(p, a,b,c) 88 | return onSameSide(p,a, b,c) and onSameSide(p,b, a,c) and onSameSide(p,c, a,b) 89 | end 90 | 91 | -- test whether any point in vertices (but pqr) lies in the triangle pqr 92 | -- note: vertices is *set*, not a list! 93 | local function anyPointInTriangle(vertices, p,q,r) 94 | for v in pairs(vertices) do 95 | if v ~= p and v ~= q and v ~= r and pointInTriangle(v, p,q,r) then 96 | return true 97 | end 98 | end 99 | return false 100 | end 101 | 102 | -- test is the triangle pqr is an "ear" of the polygon 103 | -- note: vertices is *set*, not a list! 104 | local function isEar(p,q,r, vertices) 105 | return ccw(p,q,r) and not anyPointInTriangle(vertices, p,q,r) 106 | end 107 | 108 | local function segmentsInterset(a,b, p,q) 109 | return not (onSameSide(a,b, p,q) or onSameSide(p,q, a,b)) 110 | end 111 | 112 | -- returns starting/ending indices of shared edge, i.e. if p and q share the 113 | -- edge with indices p1,p2 of p and q1,q2 of q, the return value is p1,q2 114 | local function getSharedEdge(p,q) 115 | local pindex = setmetatable({}, {__index = function(t,k) 116 | local s = {} 117 | t[k] = s 118 | return s 119 | end}) 120 | 121 | -- record indices of vertices in p by their coordinates 122 | for i = 1,#p do 123 | pindex[p[i].x][p[i].y] = i 124 | end 125 | 126 | -- iterate over all edges in q. if both endpoints of that 127 | -- edge are in p as well, return the indices of the starting 128 | -- vertex 129 | local i,k = #q,1 130 | for k = 1,#q do 131 | local v,w = q[i], q[k] 132 | if pindex[v.x][v.y] and pindex[w.x][w.y] then 133 | return pindex[w.x][w.y], k 134 | end 135 | i = k 136 | end 137 | end 138 | 139 | ----------------- 140 | -- Polygon class 141 | -- 142 | local Polygon = {} 143 | function Polygon:init(...) 144 | local vertices = removeCollinear( toVertexList({}, ...) ) 145 | assert(#vertices >= 3, "Need at least 3 non collinear points to build polygon (got "..#vertices..")") 146 | 147 | -- assert polygon is oriented counter clockwise 148 | local r = getIndexOfleftmost(vertices) 149 | local q = r > 1 and r - 1 or #vertices 150 | local s = r < #vertices and r + 1 or 1 151 | if not ccw(vertices[q], vertices[r], vertices[s]) then -- reverse order if polygon is not ccw 152 | local tmp = {} 153 | for i=#vertices,1,-1 do 154 | tmp[#tmp + 1] = vertices[i] 155 | end 156 | vertices = tmp 157 | end 158 | 159 | -- assert polygon is not self-intersecting 160 | -- outer: only need to check segments #vert;1, 1;2, ..., #vert-3;#vert-2 161 | -- inner: only need to check unconnected segments 162 | local q,p = vertices[#vertices] 163 | for i = 1,#vertices-2 do 164 | p, q = q, vertices[i] 165 | for k = i+1,#vertices-1 do 166 | local a,b = vertices[k], vertices[k+1] 167 | assert(not segmentsInterset(p,q, a,b), 'Polygon may not intersect itself') 168 | end 169 | end 170 | 171 | self.vertices = vertices 172 | -- make vertices immutable 173 | setmetatable(self.vertices, {__newindex = function() error("Thou shall not change a polygon's vertices!") end}) 174 | 175 | -- compute polygon area and centroid 176 | local p,q = vertices[#vertices], vertices[1] 177 | local det = vector.det(p.x,p.y, q.x,q.y) -- also used below 178 | self.area = det 179 | for i = 2,#vertices do 180 | p,q = q,vertices[i] 181 | self.area = self.area + vector.det(p.x,p.y, q.x,q.y) 182 | end 183 | self.area = self.area / 2 184 | 185 | p,q = vertices[#vertices], vertices[1] 186 | self.centroid = {x = (p.x+q.x)*det, y = (p.y+q.y)*det} 187 | for i = 2,#vertices do 188 | p,q = q,vertices[i] 189 | det = vector.det(p.x,p.y, q.x,q.y) 190 | self.centroid.x = self.centroid.x + (p.x+q.x) * det 191 | self.centroid.y = self.centroid.y + (p.y+q.y) * det 192 | end 193 | self.centroid.x = self.centroid.x / (6 * self.area) 194 | self.centroid.y = self.centroid.y / (6 * self.area) 195 | 196 | -- get outcircle 197 | self._radius = 0 198 | for i = 1,#vertices do 199 | self._radius = math.max(self._radius, 200 | vector.dist(vertices[i].x,vertices[i].y, self.centroid.x,self.centroid.y)) 201 | end 202 | end 203 | local newPolygon 204 | 205 | 206 | -- return vertices as x1,y1,x2,y2, ..., xn,yn 207 | function Polygon:unpack() 208 | local v = {} 209 | for i = 1,#self.vertices do 210 | v[2*i-1] = self.vertices[i].x 211 | v[2*i] = self.vertices[i].y 212 | end 213 | return unpack(v) 214 | end 215 | 216 | -- deep copy of the polygon 217 | function Polygon:clone() 218 | return Polygon( self:unpack() ) 219 | end 220 | 221 | -- get bounding box 222 | function Polygon:bbox() 223 | local ulx,uly = self.vertices[1].x, self.vertices[1].y 224 | local lrx,lry = ulx,uly 225 | for i=2,#self.vertices do 226 | local p = self.vertices[i] 227 | if ulx > p.x then ulx = p.x end 228 | if uly > p.y then uly = p.y end 229 | 230 | if lrx < p.x then lrx = p.x end 231 | if lry < p.y then lry = p.y end 232 | end 233 | 234 | return ulx,uly, lrx,lry 235 | end 236 | 237 | -- a polygon is convex if all edges are oriented ccw 238 | function Polygon:isConvex() 239 | local function isConvex() 240 | local v = self.vertices 241 | if #v == 3 then return true end 242 | 243 | if not ccw(v[#v], v[1], v[2]) then 244 | return false 245 | end 246 | for i = 2,#v-1 do 247 | if not ccw(v[i-1], v[i], v[i+1]) then 248 | return false 249 | end 250 | end 251 | if not ccw(v[#v-1], v[#v], v[1]) then 252 | return false 253 | end 254 | return true 255 | end 256 | 257 | -- replace function so that this will only be computed once 258 | local status = isConvex() 259 | self.isConvex = function() return status end 260 | return status 261 | end 262 | 263 | function Polygon:move(dx, dy) 264 | if not dy then 265 | dx, dy = dx:unpack() 266 | end 267 | for i,v in ipairs(self.vertices) do 268 | v.x = v.x + dx 269 | v.y = v.y + dy 270 | end 271 | self.centroid.x = self.centroid.x + dx 272 | self.centroid.y = self.centroid.y + dy 273 | end 274 | 275 | function Polygon:rotate(angle, cx, cy) 276 | if not (cx and cy) then 277 | cx,cy = self.centroid.x, self.centroid.y 278 | end 279 | for i,v in ipairs(self.vertices) do 280 | -- v = (v - center):rotate(angle) + center 281 | v.x,v.y = vector.add(cx,cy, vector.rotate(angle, v.x-cx, v.y-cy)) 282 | end 283 | local v = self.centroid 284 | v.x,v.y = vector.add(cx,cy, vector.rotate(angle, v.x-cx, v.y-cy)) 285 | end 286 | 287 | function Polygon:scale(s, cx,cy) 288 | if not (cx and cy) then 289 | cx,cy = self.centroid.x, self.centroid.y 290 | end 291 | for i,v in ipairs(self.vertices) do 292 | -- v = (v - center) * s + center 293 | v.x,v.y = vector.add(cx,cy, vector.mul(s, v.x-cx, v.y-cy)) 294 | end 295 | self._radius = self._radius * s 296 | end 297 | 298 | -- triangulation by the method of kong 299 | function Polygon:triangulate() 300 | if #self.vertices == 3 then return {self:clone()} end 301 | 302 | local vertices = self.vertices 303 | 304 | local next_idx, prev_idx = {}, {} 305 | for i = 1,#vertices do 306 | next_idx[i], prev_idx[i] = i+1,i-1 307 | end 308 | next_idx[#next_idx], prev_idx[1] = 1, #prev_idx 309 | 310 | local concave = {} 311 | for i, v in ipairs(vertices) do 312 | if not ccw(vertices[prev_idx[i]], v, vertices[next_idx[i]]) then 313 | concave[v] = true 314 | end 315 | end 316 | 317 | local triangles = {} 318 | local n_vert, current, skipped, next, prev = #vertices, 1, 0 319 | while n_vert > 3 do 320 | next, prev = next_idx[current], prev_idx[current] 321 | local p,q,r = vertices[prev], vertices[current], vertices[next] 322 | if isEar(p,q,r, concave) then 323 | if not areCollinear(p, q, r) then 324 | triangles[#triangles+1] = newPolygon(p.x,p.y, q.x,q.y, r.x,r.y) 325 | next_idx[prev], prev_idx[next] = next, prev 326 | concave[q] = nil 327 | n_vert, skipped = n_vert - 1, 0 328 | end 329 | else 330 | skipped = skipped + 1 331 | assert(skipped <= n_vert, "Cannot triangulate polygon") 332 | end 333 | current = next 334 | end 335 | 336 | next, prev = next_idx[current], prev_idx[current] 337 | local p,q,r = vertices[prev], vertices[current], vertices[next] 338 | triangles[#triangles+1] = newPolygon(p.x,p.y, q.x,q.y, r.x,r.y) 339 | return triangles 340 | end 341 | 342 | -- return merged polygon if possible or nil otherwise 343 | function Polygon:mergedWith(other) 344 | local p,q = getSharedEdge(self.vertices, other.vertices) 345 | assert(p and q, "Polygons do not share an edge") 346 | 347 | local ret = {} 348 | for i = 1,p-1 do 349 | ret[#ret+1] = self.vertices[i].x 350 | ret[#ret+1] = self.vertices[i].y 351 | end 352 | 353 | for i = 0,#other.vertices-2 do 354 | i = ((i-1 + q) % #other.vertices) + 1 355 | ret[#ret+1] = other.vertices[i].x 356 | ret[#ret+1] = other.vertices[i].y 357 | end 358 | 359 | for i = p+1,#self.vertices do 360 | ret[#ret+1] = self.vertices[i].x 361 | ret[#ret+1] = self.vertices[i].y 362 | end 363 | 364 | return newPolygon(unpack(ret)) 365 | end 366 | 367 | -- split polygon into convex polygons. 368 | -- note that this won't be the optimal split in most cases, as 369 | -- finding the optimal split is a really hard problem. 370 | -- the method is to first triangulate and then greedily merge 371 | -- the triangles. 372 | function Polygon:splitConvex() 373 | -- edge case: polygon is a triangle or already convex 374 | if #self.vertices <= 3 or self:isConvex() then return {self:clone()} end 375 | 376 | local convex = self:triangulate() 377 | local i = 1 378 | repeat 379 | local p = convex[i] 380 | local k = i + 1 381 | while k <= #convex do 382 | local success, merged = pcall(function() return p:mergedWith(convex[k]) end) 383 | if success and merged:isConvex() then 384 | convex[i] = merged 385 | p = convex[i] 386 | table.remove(convex, k) 387 | else 388 | k = k + 1 389 | end 390 | end 391 | i = i + 1 392 | until i >= #convex 393 | 394 | return convex 395 | end 396 | 397 | function Polygon:contains(x,y) 398 | -- test if an edge cuts the ray 399 | local function cut_ray(p,q) 400 | return ((p.y > y and q.y < y) or (p.y < y and q.y > y)) -- possible cut 401 | and (x - p.x < (y - p.y) * (q.x - p.x) / (q.y - p.y)) -- x < cut.x 402 | end 403 | 404 | -- test if the ray crosses boundary from interior to exterior. 405 | -- this is needed due to edge cases, when the ray passes through 406 | -- polygon corners 407 | local function cross_boundary(p,q) 408 | return (p.y == y and p.x > x and q.y < y) 409 | or (q.y == y and q.x > x and p.y < y) 410 | end 411 | 412 | local v = self.vertices 413 | local in_polygon = false 414 | local p,q = v[#v],v[#v] 415 | for i = 1, #v do 416 | p,q = q,v[i] 417 | if cut_ray(p,q) or cross_boundary(p,q) then 418 | in_polygon = not in_polygon 419 | end 420 | end 421 | return in_polygon 422 | end 423 | 424 | function Polygon:intersectionsWithRay(x,y, dx,dy) 425 | local nx,ny = vector.perpendicular(dx,dy) 426 | local wx,wy,det 427 | 428 | local ts = {} -- ray parameters of each intersection 429 | local q1,q2 = nil, self.vertices[#self.vertices] 430 | for i = 1, #self.vertices do 431 | q1,q2 = q2,self.vertices[i] 432 | wx,wy = q2.x - q1.x, q2.y - q1.y 433 | det = vector.det(dx,dy, wx,wy) 434 | 435 | if det ~= 0 then 436 | -- there is an intersection point. check if it lies on both 437 | -- the ray and the segment. 438 | local rx,ry = q2.x - x, q2.y - y 439 | local l = vector.det(rx,ry, wx,wy) / det 440 | local m = vector.det(dx,dy, rx,ry) / det 441 | if m >= 0 and m <= 1 then 442 | -- we cannot jump out early here (i.e. when l > tmin) because 443 | -- the polygon might be concave 444 | ts[#ts+1] = l 445 | end 446 | else 447 | -- lines parralel or incident. get distance of line to 448 | -- anchor point. if they are incident, check if an endpoint 449 | -- lies on the ray 450 | local dist = vector.dot(q1.x-x,q1.y-y, nx,ny) 451 | if dist == 0 then 452 | local l = vector.dot(dx,dy, q1.x-x,q1.y-y) 453 | local m = vector.dot(dx,dy, q2.x-x,q2.y-y) 454 | if l >= m then 455 | ts[#ts+1] = l 456 | else 457 | ts[#ts+1] = m 458 | end 459 | end 460 | end 461 | end 462 | 463 | return ts 464 | end 465 | 466 | function Polygon:intersectsRay(x,y, dx,dy) 467 | local tmin = math.huge 468 | for _, t in ipairs(self:intersectionsWithRay(x,y,dx,dy)) do 469 | tmin = math.min(tmin, t) 470 | end 471 | return tmin ~= math.huge, tmin 472 | end 473 | 474 | Polygon = common_local.class('Polygon', Polygon) 475 | newPolygon = function(...) return common_local.instance(Polygon, ...) end 476 | return Polygon 477 | --------------------------------------------------------------------------------