# Get the boundary edges of outer and inner polygons in a 3D mesh with holes in python

I have a bunch of meshes with different shapes (cuboids, L-shapes, pentagons, hexagons, ...) with holes, see attached image. The meshes are not flat, not always vertical, have different orientations and they are volumetric (with thickness). How can I get the boundary of edges on the outer polygon (pentagon in below picture) and inner polygon (the holes)? The following code is the closest I could get but it returns all the boundary vertices I believe.

def get_boundary(object):
""" get the boundary of the mesh """
# This returns more vertices than the boundary!!!

bm = bmesh.new()
bm.from_mesh(obj.data)

bm.edges.ensure_lookup_table()
boundary = []
for edge in bm.edges:
if edge.is_boundary:
coords = [vert.co for vert in edge.verts]
boundary.extend(coords)

# print(np.unique(np.asarray(boundary), axis=0)) # this sorts the points

return boundary


UPDATE

I managed to select coplanar faces on one side of the mesh. bpy.ops.mesh.select_similar(type='COPLANAR', threshold=0.1) Now I should make a new mesh from it, then is_boundary should give me the inner and outer polygon. But how should I create a new mesh from the selection in the edit mode and make sure the properties of edges are updated?

• You need to explain what you mean by the boundary. I have a guess, but I don't want to suggest something and possibly push you in the wrong direction. In your code, you check edges for is_boundary. A boundary edge is an edge that has only one face neighbor. So if you have a quad made out of 4 vertices, all edges are boundary. If you now subdivide it, all edges created by subdividing previous edges, are still boundary, but all edges created by cutting the face are internal, not boundary. After solidifying, no edge is a boundary. May 14, 2023 at 20:27
• I think you want is_convex instead. See the docs docs.blender.org/api/current/… May 14, 2023 at 20:34
• @MarkusvonBroady Sorry for the unclarity. I added one more image that shows in wireframe mode what edges I meant by inner and outer polygons. I can convert quads to triangles, so let's suppose we only have tris. May 15, 2023 at 9:24
• @Gorgious I think most of the edges are convex. If I use is_convex a lot of edges are selected. May 15, 2023 at 11:24
• May 16, 2023 at 10:18

Here is a working sample in Blender scripting. I used bmesh.ops.edgeloop_fill to get the edge loops after making a subset of coplanar faces. This code runs in Blender scripting but I can't run this in a standalone python using API without Blender UI, this line troubles me: bpy.ops.mesh.select_similar(type='COPLANAR', threshold=0.1). I don't know how to handle the active data between functions. @Gorgious any improvements, comment and corrections is appreciated.

def get_largest_face_inx(obj):
""" get the index of the largest face """

areas = [poly.area for poly in obj.data.polygons]

return np.argmax(areas)

def select_coplanar_faces(obj):
""" select coplanar faces """

# get the index of the largest face by area
inx = get_largest_face_inx(obj)

# select coplanar faces of this largest face
bpy.ops.object.mode_set(mode='OBJECT')
obj.data.polygons[inx].select = True
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_mode(type='FACE')
bpy.ops.mesh.select_similar(type='COPLANAR', threshold=0.1)

bpy.ops.object.mode_set(mode='OBJECT')
coplanars = [f for f in bpy.context.object.data.polygons if f.select is True]

return coplanars

def full_copy(obj):
obj_copy = bpy.context.active_object.copy()
obj_copy.data = obj_copy.data.copy()
obj_copy.select_set(True)
bpy.context.view_layer.objects.active = obj_copy

return obj_copy

### MAIN Function
obj = bpy.context.active_object
obj_copy = full_copy(obj)
obj.select_set(False)

# coplanar = select_coplanar_faces_bmesh(obj)
coplanar = select_coplanar_faces(obj_copy)
#print(get_mesh_area(obj))
print(sum([f.area for f in coplanar]))

#-------------------------------------------

#select_convex_loop(obj)

bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='INVERT')  # necessary to separate selected coplanars
bpy.ops.mesh.separate(type='SELECTED')    # create a new mesh from selected faces

obj_active = bpy.context.active_object
print(obj_active.name)

bpy.ops.mesh.select_mode(type='FACE')

bpy.ops.mesh.select_mode(type='EDGE')
me = obj_active.data
bm = bmesh.from_edit_mesh(me)
#bm.faces.active = None

ret = bmesh.ops.edgeloop_fill(bm,
edges=[e for e in bm.edges if e.is_boundary])

# the seperate edge loops
edge_loops = [f.edges[:] for f in ret["faces"]]

print(len(edge_loops))

loop_areas = [f.calc_area() for f in ret["faces"]]
print(loop_areas)

areas = [f.calc_area() for f in bm.faces]

bmesh.update_edit_mesh(me)