Polygon editing example with physics

I thought I’d post some sample code that didn’t make it into 1.3 due to time restrictions. This example shows you how to use the triangulate function to draw arbitrary polygons as well as methods for editing them using touches. The example also shows how physics interact with these polygons as well.


-- distPointToLineSeg(): shortest distance of a point to a line segment.
function distPointToLineSeg(p , s1, s2)
    local v = s2 - s1
    local w = p - s1

    c1 = w:dot(v)
    if c1 <= 0 then
        return p:dist(s1)
    end

    c2 = v:dot(v)
    if c2 <= c1 then
        return p:dist(s2)
    end

    b = c1 / c2;
    pb = s1 + b * v;
    return p:dist(pb)
end
--===================================================================

-- Use this function to perform your initial setup
function setup()
    debugDraw = PhysicsDebugDraw()
    
    print("Hello Polygon!")
    print("1. Tap in clockwise order to create a polygon.")
    print("2. Drag existing points to move them.")
    print("3. Drag on lines to add new points.")
    
    -- the mesh to draw the polygon with
    polyMesh = mesh()
    -- the current set of vertices for the polygon
    verts = {}
    -- the polygon fill color
    col = color(255, 188, 0, 255)
    
    index = -1
    touchID = -1
    
    -- rigid body for the polygon
    polyBody = nil
    
    timer = 0
end

-- This function gets called once every frame
function draw()
    
    timer = timer + DeltaTime
    -- create a circle every 2 seconds
    if timer > 2 then
        local body = physics.body(CIRCLE, 25)
        body.restitution = 0.5
        body.x = WIDTH/2
        body.y = HEIGHT
        debugDraw:addBody(body)
        timer = 0
    end
    
    -- This sets the background color to black
    background(0, 0, 0)

    -- draw physics objects
    debugDraw:draw()

    -- draw the polygon interia
    fill(col)
    polyMesh:draw()
    
    pushStyle()
    lineCapMode(PROJECT)
    fill(255, 255, 255, 255)
    
    -- draw the polygon outline
    local pv = verts[1]
    for k,v in ipairs(verts) do
        noStroke()
        ellipse(v.x, v.y, 10, 10)
        stroke(col)
        strokeWidth(5)
        line(pv.x, pv.y, v.x, v.y)
        pv = v
    end
    if pv then
        line(pv.x, pv.y, verts[1].x, verts[1].y)
    end
    popStyle()
    
end

function touched(touch)
    local tv = vec2(touch.x, touch.y)
    
    if touch.state == BEGAN and index == -1 then        
        -- find the closest vertex within 50 px of thr touch
        touchID = touch.id
        local minDist = math.huge
        for k,v in ipairs(verts) do
            local dist = v:dist(tv)
            if dist < minDist and dist < 50 then
                minDist = dist
                index = k
            end
        end
       
        -- if no point is found near the touch, insert a new one           
        if index == -1 then
            index = #verts
            if index == 0 then
                index = index + 1
            end
            
            -- if touch is within 50px to a line, insert point on line
            if #verts > 2 then
                local minDist = math.huge
                local pv = verts[index]
                for k,v in ipairs(verts) do
                    local dist = distPointToLineSeg(tv, pv, v)
                    if dist < minDist and dist < 50 then
                        minDist = dist
                        index = k
                    end
                    pv = v
                end
            end
            
            table.insert(verts, index, tv)
        else
            verts[index] = tv
        end
        
    elseif touch.state == MOVING and touch.id == touchID then
        verts[index] = tv 
    elseif touch.state == ENDED and touch.id == touchID then
        index = -1
    end
    
    -- use triangulate to generate triangles from the polygon outline for the mesh
    polyMesh.vertices = triangulate(verts)
    if polyBody then
        polyBody:destroy()
    end
    if #verts > 2 then
        polyBody = physics.body(POLYGON, unpack(verts))
        polyBody.type = STATIC
    end
end

PhysicsDebugDraw = class()

function PhysicsDebugDraw:init()
    self.bodies = {}
    self.joints = {}
    self.touchMap = {}
    self.contacts = {}
end

function PhysicsDebugDraw:addBody(body)
    table.insert(self.bodies,body)
end

function PhysicsDebugDraw:addJoint(joint)
    table.insert(self.joints,joint)
end

function PhysicsDebugDraw:clear()
    -- deactivate all bodies
    
    for i,body in ipairs(self.bodies) do
        body:destroy()
    end
  
    for i,joint in ipairs(self.joints) do
        joint:destroy()
    end      
    
    self.bodies = {}
    self.joints = {}
    self.contacts = {}
    self.touchMap = {}
end

function PhysicsDebugDraw:draw()
    
    pushStyle()
    smooth()
    strokeWidth(5)
    stroke(128,0,128)
    
    local gain = 2.0
    local damp = 0.5
    for k,v in pairs(self.touchMap) do
        local worldAnchor = v.body:getWorldPoint(v.anchor)
        local touchPoint = v.tp
        local diff = touchPoint - worldAnchor
        local vel = v.body:getLinearVelocityFromWorldPoint(worldAnchor)
        v.body:applyForce( (1/1) * diff * gain - vel * damp, worldAnchor)
        
        line(touchPoint.x, touchPoint.y, worldAnchor.x, worldAnchor.y)
    end
    
    stroke(0,255,0,255)
    strokeWidth(5)
    for k,joint in pairs(self.joints) do
        local a = joint.anchorA
        local b = joint.anchorB
        line(a.x,a.y,b.x,b.y)
    end
    
    stroke(255,255,255,255)
    noFill()
    
    
    for i,body in ipairs(self.bodies) do
        pushMatrix()
        translate(body.x, body.y)
        rotate(body.angle)
    
        if body.type == STATIC then
            stroke(255,255,255,255)
        elseif body.type == DYNAMIC then
            stroke(150,255,150,255)
        elseif body.type == KINEMATIC then
            stroke(150,150,255,255)
        end
    
        if body.shapeType == POLYGON then
            strokeWidth(5.0)
            local points = body.points
            for j = 1,#points do
                a = points[j]
                b = points[(j % #points)+1]
                line(a.x, a.y, b.x, b.y)
            end
        elseif body.shapeType == CHAIN or body.shapeType == EDGE then
            strokeWidth(5.0)
            local points = body.points
            for j = 1,#points-1 do
                a = points[j]
                b = points[j+1]
                line(a.x, a.y, b.x, b.y)
            end      
        elseif body.shapeType == CIRCLE then
            strokeWidth(5.0)
            line(0,0,body.radius-3,0)
            strokeWidth(2.5)
            ellipse(0,0,body.radius*2)
        end
        
        popMatrix()
    end 
    
    stroke(255, 0, 0, 255)
    fill(255, 0, 0, 255)

    for k,v in pairs(self.contacts) do
        for m,n in ipairs(v.points) do
            ellipse(n.x, n.y, 10, 10)
        end
    end
    
    popStyle()
end

That’s fun just to play with. It will come in handy as well.

Thanks for that @John! Will be useful in upcoming projects!

I still love this. So, much so it’s called 112 in my list of projects.

I’m now using it to freestyle draw vertices. I’ve found a place to add a clearoutput() and print that allow the vertices to be cut and pasted out.

elseif touch.state == ENDED and touch.id == touchID then
        index = -1
        clearOutput()
        print(unpack(verts))
    end

This is an awesome old project! I’ve attached a zip that updates the deprecated “unpack” syntax.

Does anybody know how one would set the mesh to have an image in it?

@UberGoober great wee program.

Here a non perfect solution but will hopefully push you in the right direction for mapping an image


-- John Polygon Editing

-- distPointToLineSeg(): shortest distance of a point to a line segment.
function distPointToLineSeg(p , s1, s2)
    local v = s2 - s1
    local w = p - s1
    
    c1 = w:dot(v)
    if c1 <= 0 then
        return p:dist(s1)
    end
    
    c2 = v:dot(v)
    if c2 <= c1 then
        return p:dist(s2)
    end
    
    b = c1 / c2;
    pb = s1 + b * v;
    return p:dist(pb)
end
--===================================================================

-- Use this function to perform your initial setup
function setup()
    debugDraw = PhysicsDebugDraw()
    
    print("Hello Polygon!")
    print("1. Tap in clockwise order to create a polygon.")
    print("2. Drag existing points to move them.")
    print("3. Drag on lines to add new points.")
    
    -- the mesh to draw the polygon with
    polyMesh = mesh()
    -- the current set of vertices for the polygon
    verts = {}
    -- the polygon fill color
    col = color(255, 188, 0, 255)
    
    index = -1
    touchID = -1
    
    -- rigid body for the polygon
    polyBody = nil
    
    timer = 0
  
  img=readImage(asset.builtin.Cargo_Bot.Crate_Blue_3)
  polyMesh.texture=img
end

-- This function gets called once every frame
function draw()
    
    timer = timer + DeltaTime
    -- create a circle every 2 seconds
    if timer > 0.8 then
        local body = physics.body(CIRCLE, 25)
        body.restitution = 0.5
        body.x = WIDTH/2
        body.y = HEIGHT
        debugDraw:addBody(body)
        timer = 0
    end
    
    -- This sets the background color to black
    background(0, 0, 0)
    
    -- draw physics objects
    debugDraw:draw()
    
    -- draw the polygon interia
    fill(col)

  
  
    polyMesh:draw()
    
    pushStyle()
    lineCapMode(PROJECT)
    fill(255, 255, 255, 255)
    
    -- draw the polygon outline
    local pv = verts[1]
    for k,v in ipairs(verts) do
        noStroke()
        ellipse(v.x, v.y, 10, 10)
        stroke(col)
        strokeWidth(2)
        line(pv.x, pv.y, v.x, v.y)
        pv = v
    end
    if pv then
        line(pv.x, pv.y, verts[1].x, verts[1].y)
    end
    popStyle()
    
end

function touched(touch)
    local tv = vec2(touch.x, touch.y)
    
    if touch.state == BEGAN and index == -1 then        
        -- find the closest vertex within 50 px of thr touch
        touchID = touch.id
        local minDist = math.huge
        for k,v in ipairs(verts) do
            local dist = v:dist(tv)
            if dist < minDist and dist < 50 then
                minDist = dist
                index = k
            end
        end
        
        -- if no point is found near the touch, insert a new one           
        if index == -1 then
            index = #verts
            if index == 0 then
                index = index + 1
            end
            
            -- if touch is within 50px to a line, insert point on line
            if #verts > 2 then
                local minDist = math.huge
                local pv = verts[index]
                for k,v in ipairs(verts) do
                    local dist = distPointToLineSeg(tv, pv, v)
                    if dist < minDist and dist < 50 then
                        minDist = dist
                        index = k
                    end
                    pv = v
                end
            end
            
            table.insert(verts, index, tv)
        else
            verts[index] = tv
        end
        
    elseif touch.state == MOVING and touch.id == touchID then
        verts[index] = tv 
    elseif touch.state == ENDED and touch.id == touchID then
        index = -1
    end
    
    -- use triangulate to generate triangles from the polygon outline for the mesh
    polyMesh.vertices = triangulate(verts)

--add in texture coordinates
  local t={}
local flag=0
  local minx=0
  local miny=0
  local maxx=0
  local maxy=0
  for i,p in pairs(verts) do
    if flag==0 then
      minx=p.x
      miny=p.y
      maxx=p.x
      maxy=p.y
      flag=1
    else
      if p.x<minx then minx=p.x end
      if p.y<miny then miny=p.y end
      if p.x>maxx then maxx=p.x end
      if p.y>maxy then maxy=p.y end
    end
  end


  
  for i,p in pairs(verts) do
    local nx=(p.x-minx)/(maxx-minx)
    local ny=(p.y-miny)/(maxy-miny)    
    table.insert(t,vec2(nx,ny))
  end
  
  polyMesh.texCoords=triangulate(t)
  polyMesh:setColors(color(255))
    if polyBody then
        polyBody:destroy()
    end
    if #verts > 2 then
        polyBody = physics.body(POLYGON, table.unpack(verts))
        polyBody.type = STATIC
    end
end

PhysicsDebugDraw = class()

function PhysicsDebugDraw:init()
    self.bodies = {}
    self.joints = {}
    self.touchMap = {}
    self.contacts = {}
end

function PhysicsDebugDraw:addBody(body)
    table.insert(self.bodies,body)
end

function PhysicsDebugDraw:addJoint(joint)
    table.insert(self.joints,joint)
end

function PhysicsDebugDraw:clear()
    -- deactivate all bodies
    
    for i,body in ipairs(self.bodies) do
        body:destroy()
    end
    
    for i,joint in ipairs(self.joints) do
        joint:destroy()
    end      
    
    self.bodies = {}
    self.joints = {}
    self.contacts = {}
    self.touchMap = {}
end

function PhysicsDebugDraw:draw()
    
    pushStyle()
    smooth()
    strokeWidth(5)
    stroke(128,0,128)
    
    local gain = 2.0
    local damp = 0.5
    for k,v in pairs(self.touchMap) do
        local worldAnchor = v.body:getWorldPoint(v.anchor)
        local touchPoint = v.tp
        local diff = touchPoint - worldAnchor
        local vel = v.body:getLinearVelocityFromWorldPoint(worldAnchor)
        v.body:applyForce( (1/1) * diff * gain - vel * damp, worldAnchor)
        
        line(touchPoint.x, touchPoint.y, worldAnchor.x, worldAnchor.y)
    end
    
    stroke(0,255,0,255)
    strokeWidth(5)
    for k,joint in pairs(self.joints) do
        local a = joint.anchorA
        local b = joint.anchorB
        line(a.x,a.y,b.x,b.y)
    end
    
    stroke(255,255,255,255)
    noFill()
    
    
    for i,body in ipairs(self.bodies) do
        pushMatrix()
        translate(body.x, body.y)
        rotate(body.angle)
        
        if body.type == STATIC then
            stroke(255,255,255,255)
        elseif body.type == DYNAMIC then
            stroke(150,255,150,255)
        elseif body.type == KINEMATIC then
            stroke(150,150,255,255)
        end
        
        if body.shapeType == POLYGON then
            strokeWidth(5.0)

            local points = body.points
            for j = 1,#points do
                a = points[j]
                b = points[(j % #points)+1]
                line(a.x, a.y, b.x, b.y)
            end
        elseif body.shapeType == CHAIN or body.shapeType == EDGE then
            strokeWidth(5.0)
            local points = body.points
            for j = 1,#points-1 do
                a = points[j]
                b = points[j+1]
                line(a.x, a.y, b.x, b.y)
            end      
        elseif body.shapeType == CIRCLE then
            strokeWidth(5.0)
            line(0,0,body.radius-3,0)
            strokeWidth(2.5)
            ellipse(0,0,body.radius*2)
        end
        
        popMatrix()
    end 
    
    stroke(255, 0, 0, 255)
    fill(255, 0, 0, 255)
    
    for k,v in pairs(self.contacts) do
        for m,n in ipairs(v.points) do
            ellipse(n.x, n.y, 10, 10)
        end
    end
    
    popStyle()
end

Thanks @West, though of course it’s @John ’s not mine. Did I seem like I was claiming it was mine?

@UberGoober not at all. Thanks for resurfacing it

@West I can’t make hide nor hair of the code you added. Can you explain it a little?

@UberGoober I can try, but I don’t fully understand meshes/triangulation which I suspect is leading to the discrepancies in the images. Anyway, here is my train of thought:

In setup

img=readImage(asset.builtin.Cargo_Bot.Crate_Blue_3)
  polyMesh.texture=img

This assigns the image to be used as the texture.

verts - this is the array of the polygon vertices as input by the user

 polyMesh.vertices = triangulate(verts)

The triangulate turns the user inputted vertices into an array of mesh vertices. In a mesh the polygon is represented by a series of triangles. For example a rectangle would be represented by two triangles, and expressed by the 6 vertices - 3 for the first triangle and three for the second (even though some of the vertices are shared, I think they need to be explicitly represented.

To overlay a texture, each vertex in the mesh (polyMesh.vertices) needs to reference a point/position in the texture image. This goes into polyMesh.texcoords as a list of equivalent texture coordinates. These coordinates need to run between 0 and 1 for both left to right and bottom to top of the image.

This bit of code is finding the bounding box of the user inputted polygon

local flag=0
  local minx=0
  local miny=0
  local maxx=0
  local maxy=0
  for i,p in pairs(verts) do
    if flag==0 then
      minx=p.x
      miny=p.y
      maxx=p.x
      maxy=p.y
      flag=1
    else
      if p.x<minx then minx=p.x end
      if p.y<miny then miny=p.y end
      if p.x>maxx then maxx=p.x end
      if p.y>maxy then maxy=p.y end
    end
  end

then (hopefully) scaling each point to this bounding box (for example if the leftmost user inputted point was at x=100 and the right most point was at 300, then a point at 200 would be in the middle and would have a texture coordinate of x=0.5)

 for i,p in pairs(verts) do
    local nx=(p.x-minx)/(maxx-minx)
    local ny=(p.y-miny)/(maxy-miny)    
    table.insert(t,vec2(nx,ny))
  end

So the array t, should contain a scaled version of the user inputted polygon, between 0 and 1.

Finally, we need to have the array of the mesh texcoords, rather than the user inputted texCoords so we need to triangulate the texcoords too (like we did with the vertices at the start)

polyMesh.texCoords=triangulate(t)

I am assuming that the triangulate function will work in the same way on both the vertices and the texcoords - but this may be where it is falling down.

Maybe a better way would be to map the polyMesh.vertices (after initial triangulation) to the texture rather than fitting the user inputted vertices (verts) to the texture then triangulating.

@West… I think I get it but…

So it’s like this: first, we need to know the bounding box of the entire polygon mesh, and the position of each mesh vertex in relation to that bounding box.

Then, we need to know what part of the image is being used, in other words which four points on the image should correspond to each corner of the bounding box of the polygon.

Finally, once we have the bounding box of the polygon mapped to a box somewhere on the image (or maybe the whole image), we can get the texCoords of each vertex, by translating the (relative) vertex coordinates into the (absolute) coordinates in the bounding box of the image.

…is that right in theory?

@UberGoober Yes I think so. The issue is you are mapping a rectangle (the texture) onto a polygon, but didn’t specify how this was to be mapped.

The bounding box approach is to allow a cookie cutter type approach - you are effectively stretching the polygon in the x and y direction to the size of the texture image until one of a polygon vertex touches the each edge of the texture then chop out the shape of polygon from the texture.

An alternative would be to map each point perimeter of the polygon to a set of equally spaced points around the perimeter of the mesh - then the texture would be squashed and distorted on to the shape - I think this might be a bit harder to implement though

@UberGoober @West Here’s a simple example. Drag your finger around the screen to enclose an area. When you lift your finger, what you enclosed will be cut out of the background image.

viewer.mode=FULLSCREEN

function setup()  
    count=0
    img=readImage(asset.builtin.Cargo_Bot.Startup_Screen)
    tab={}
    mtab={}
    m=mesh()
    fill(255)
    sizeX,sizeY=WIDTH,HEIGHT
end

function draw()
    background(0)
    if result~=nil then
        sprite(result,WIDTH/2,HEIGHT/2)
    else
        sprite(img,WIDTH/2,HEIGHT/2)
        for a,b in pairs(tab) do
            ellipse(b.x,b.y,5)
        end
    end
end

function touched(t)
    if t.state==CHANGED then
        count=count+1
        table.insert(tab,t)
        if #tab>1 then
            table.insert(mtab,(vec2(tab[1].x,tab[1].y)))
            table.insert(mtab,(vec2(tab[count-1].x,tab[count-1].y)))
            table.insert(mtab,(vec2(tab[count].x,tab[count].y)))
        end
    elseif t.state==ENDED then
        process()
    end        
end

function process()
    mask=image(sizeX,sizeY)
    setContext(mask)   
    m.vertices=mtab
    m:setColors(255,255,255)
    m.draw(m)
    setContext()    
    original=image(WIDTH,HEIGHT)
    setContext(original)
    sprite(img,WIDTH/2,HEIGHT/2)
    setContext()    
    result=image(sizeX,sizeY)
    setContext(result)
    sprite(mask,sizeX/2,sizeY/2)    
    blendMode(MULTIPLY)      
    sprite(original,sizeX/2,sizeY/2)
    blendMode(NORMAL)    
    setContext()
end

@dave1707 I am not sure but I think your example may not be quite applicable to the problem, because you’re using a mesh shape as a mask for drawing a sprite, instead of applying an image to a mesh itself as a texture.

It’s a really clever solution to the visual needs of this demo, neatly avoiding all that triangulation jazz.

But I’m not clear if it helps with of drawing an image inside a 2D physics body—for example, if you used this code on a 2D square that was bouncing off a floor, the sprite wouldn’t stay aligned with the cube’s position and rotation, would it?

@UberGoober So you’re creating a mesh of different shapes that you want an image applied to. I’ll have to look thru what else I have and see if I have something for that would work.

Moved code that was here to another discussion.