Point in concave polygon

I found myself in need of a point-in-polygon function, compatible with concave polygons (polygons containing internal angles greater than 90°, see http://en.wikipedia.org/wiki/Convex_polygon). My take on this can be seen below.

I used cross products to determine if a point is on the left or right side of the edges of a polygon. To make it work with concave polygons, a second polygon is defined that is ‘subtracted’ from the first. If a point is on the substracted polygon, it is defined as outside the polygon as a whole.

I can think of a function that determines these subtracted polygons automatically. But I was wondering if anyone has a more efficient take on this, maybe without using multiple polygons at all.

Thanks in advance!

--# Main
function setup()
    -- Vertices are defined in a counter-clockwise order
    local add = {   vec2(0,0),
                    vec2(300,100),
                    vec2(400,400),
                    vec2(200,400),
                    vec2(50,300)   }
    
    local sub = {   vec2(300,100),
                    vec2(400,400),
                    vec2(200,300),
                    vec2(150,150)   }
    
    poly = ObjPolygon(add,sub)
    
    -- p Tracks the last touch
    p = vec2(100,100)
    parameter.watch("InOrOut")
end


function touched(touch)
    p = vec2(touch.x, touch.y)
end


function draw()
    background(255)
    
    poly:draw()
    fill(255, 0, 0, 255)
    ellipseMode(CENTER)
    ellipse(p.x,p.y, 10)
    
    -- Checks whether p is located in or out of the polygon
    InOrOut = poly:inPoly(p)
end


function table.copy(t)
    local copy = {}
    
    for i,v in ipairs(t) do
        copy[i] = v
    end
    
    for k,v in pairs(t) do
        copy.k = v
    end
    
    return copy
end
--# Polygon
Polygon = class()

function Polygon:init(vertices)
    self.vertices = vertices
    self.m = mesh()
    self.m.vertices = triangulate(self.vertices)
end


function Polygon:draw()
    self.m:draw()
end

-- inPoly determines if P is located in the polygon through calculating the cross product of the point P with every edge. If this product is positive, P is right of the edge. If negative, it is on the left.
function Polygon:inPoly(P)
    local v = table.copy(self.vertices)
    table.insert(v, v[1])
    
    for i = 2,#v do
        local p = P - v[i-1]
        local q = v[i] - v[i-1]
        local cross = p.x*q.y - p.y*q.x
        
        if cross > 0 then return false end
    end
    
    return true
end

--# ObjPolygon
ObjPolygon = class()

function ObjPolygon:init(vAdd, vSub)
    self.add = Polygon(vAdd)
    if vSub ~= nil then
        self.sub = Polygon(vSub)
    end
end


function ObjPolygon:draw()
    fill(0)
    self.add:draw()
    if self.sub ~= nil then
        fill(127)
        self.sub:draw()
    end
end


function ObjPolygon:inPoly(P)
    -- If P is in sub, it's
    if self.sub ~= nil and self.sub:inPoly(P) == true then
        return false
    end
    
    if self.add:inPoly(P) == true then
        return true
    end
    
    return false
end

I’ve implemented this by working out the winding number. This works providing the polygon doesn’t have self-intersections.

The basic routine is to add up the angles subtended at the point as you iterate over the vertices. If the result is 0 then you’re outside the polygon, if it is +/- 2pi (or +/- 360) then you’re inside. If it is +/- pi (+/- 180) then you’re on the edge.

Here’s a version that I had. Just move the circle thru the triangles.


displayMode(FULLSCREEN)

function setup()
    tx,ty=200,600
    verts1={vec2(200,300),vec2(500,300),vec2(350,600),
            vec2(200,400),vec2(200,400),vec2(600,500)}
end

function draw()
    background(40, 40, 50)
    stroke(255)
    strokeWidth(2)
    j=#verts1
    for z=1,#verts1 do
        line(verts1[z].x,verts1[z].y,verts1[j].x,verts1[j].y)
        j=z
    end    
    ellipse(tx,ty,10)       
    check() 
    fill(255)   
    text(str,350,700)  
end

function check()    -- check if points tx,ty are within triangle
    j=#verts1
    c=-1
    for i=1,#verts1 do
        a1=false
        if verts1[i].y>ty then
            a1=true
        end
        a2=false
        if verts1[j].y>ty then
            a2=true
        end
        b1=verts1[j].x-verts1[i].x
        b2=ty-verts1[i].y
        b3=(verts1[j].y-verts1[i].y)
        if a1~=a2 and tx<(b1*b2/b3+verts1[i].x) then
            c = c *-1
        end
        j=i
    end 
    str="outside"
    if c==1 then
        str="inside"
    end
end

function touched(t)
    tx=t.x+100
    ty=t.y+100
end

Here’s my checking code:

function ProtoTile:checkTouch(t)
    local tv = vec2(t.x,t.y)
    local a = 0
    local pts = self.points
    local n = self.npoints
    for k=1,n do
        a = a + (pts[k]-tv):angleBetween(pts[k%n+1] - tv)
    end
    if math.abs(a) > 1 then
        return true
    end
    return false
end

Thank you both for your input! I already implemented your method yesterday, @Andrew_Stacey. I like the simplicity of the concept, and it’s very clear why it works.

I am not planning on using it with complex polygons anytime soon, so that’s a challenge for the future.