Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Polygon editing example with physics

edited January 2012 Posts: 709

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     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.x, verts.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

• edited January 2012 Posts: 622

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

• Posts: 118

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

• Posts: 622

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

• edited September 2021 Posts: 1,629

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?

• Posts: 904

@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 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.x, verts.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
• Posts: 1,629

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

• Posts: 904
@UberGoober not at all. Thanks for resurfacing it
• Posts: 1,629

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

• Posts: 904

@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

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.

• Posts: 1,629

@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?

• Posts: 904

@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

• Posts: 9,809

@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
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.x,tab.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()
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)
blendMode(MULTIPLY)
sprite(original,sizeX/2,sizeY/2)
blendMode(NORMAL)
setContext()
end

• Posts: 1,629

@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?

• Posts: 9,809

@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.

• Posts: 9,809

Moved code that was here to another discussion.