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?
2 Answers
$\begingroup$
$\endgroup$
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'][0]
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[0].co).length)
cuts = sorted(cuts, key=lambda c: ( c-e.verts[0].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()
$\begingroup$
$\endgroup$
It's not automatic but you could do it quickly this way. Enable the Auto Merge and Split Edges &Faces options:
Then extrude your vertex, the options will create a vertex at the intersection, and at last remove the tip: