I have a 2D mesh that has some non-closed edges, i.e., edges that end in a single vertex rather than forming a closed loop that could be used to generate a face (Image 1). What I'd like to have is a way to automatically connect each terminal vertex to the closest point on the nearest edge so that all edges form closed loops (Image 2). Obviously, I could manually subdivide the edge and then create a new edge between the two vertices, as I did to make these pictures, but this would be very tedious on my real mesh. Is there a tool in Blender (or an add-on) to do this automatically?
This is a script that will do it automatically (Edit Mode)
merge_by_distance = 0.0001 # you can set to 0 import bmesh from bpy import context as C from mathutils.bvhtree import BVHTree from collections import defaultdict def main(): ob = C.object me = ob.data bm = bmesh.from_edit_mesh(me) """Making a copy of the verts, because the collection will be modified (by adding more verts via extrusion) while iterating over them. "if you mutate something you're iterating over, you're living in a state of sin and deserve whatever happens to you" - Raymond Hettinger orig_edges is useful to not ensure_lookup_table after each extrusion""" orig_verts = bm.verts[:] orig_edges = bm.edges[:] bm_lemon = bm.copy() # https://blender.stackexchange.com/a/202194/60486 bmesh.ops.extrude_edge_only(bm_lemon, edges=bm_lemon.edges) edge_cuts = defaultdict(set) for v in orig_verts: if len(v.link_edges) != 1: # Not a dangling vert: either edgeless or connecting multiple edges continue """Unfortunately BVHTree can't be asked to give 2nd nearest element, and you don't want to find the edge the dangling vert is connected to. So for each vert, create a copy without that vert (and therefore without the edge connected to that vert)""" bm_temp = bm_lemon.copy() bm_temp.verts.ensure_lookup_table() bm_temp.verts.remove(bm_temp.verts[v.index]) tree = BVHTree.FromBMesh(bm_temp) found_location, _, i, _ = tree.find_nearest(v.co) """ `i` is the index of the face. One of the edges of the face is the original edge that extruded to this face. Since extrusion doesn't change original indices and doesn't remove geometry, the original edge must have the smallest index""" bm_temp.faces.ensure_lookup_table() edge_i = min(e.index for e in bm_temp.faces[i].edges) """Normally no need to explicitly free bmesh data, but I'm creating a lot of bmesh copies here, so I'm worried I could run out of memory before the script ends... I don't free() bm_lemon though""" bm_temp.free() new_vert = bmesh.ops.extrude_vert_indiv(bm, verts=[v])['verts'] new_vert.co = found_location found_location.freeze() # make immutable so it can be added to a set edge_cuts[orig_edges[edge_i]].add(found_location) for e, cuts in edge_cuts.items(): result = bmesh.ops.subdivide_edges(bm, edges=[e], cuts=len(cuts)) new_verts = result['geom_inner'] """Sort by distance to edge's first vertex, to ensure vertices aren't shuffled when setting their positions. I don't care how close the cuts are, because I'm using Merge by Distance later anyway. `new_verts` are PROBABLY already sorted, even if, not clear if ascending or descending, and the documentation doesn't describe it, so it could change from version to version... Avoid relying on undocumented behavior if you can.""" new_verts.sort( key=lambda v: (v.co-e.verts.co).length) cuts = sorted(cuts, key=lambda c: ( c-e.verts.co).length) for v, cut in zip(new_verts, cuts): v.co = cut bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=merge_by_distance) bmesh.update_edit_mesh(me) main()