Is there a way to get islands of edge loops without brute force?

Given a mesh with no faces, like so, is there way to return three lists of vertices, where each list contains a sequence of vertices for each separate island of edges in the order they are connected (like a polyline)? For the circle, where there is no start or end, any vertex may be arbitrarily used as the start/end vertex in the list.

With brute force, I can check all edges and see which share a vertex, but is there a smarter way?

Given you are not using face data I would just convert the mesh to curve:

Each spline becomes its own island and has its own start/end points in sequence.

import bpy

obj = bpy.context.object

bpy.ops.object.convert(target='CURVE')

print(f"curve: {obj.name}")

for i, s in enumerate(obj.data.splines):
start_vert =  s.points[0].co
end_vert = s.points[-1].co
print(f"spline: {i}")
print(f"start index: 0")
print(f"starts location: {start_vert}")
print(f"end index: {len(s.points)-1}")
print(f"end location: {end_vert}")


providing output:

curve: Circle
spline: 0
start index: 0
starts location: <Vector (3.0000, 0.0000, 0.0000, 0.0000)>
end index: 1
end location: <Vector (3.3874, 0.5697, 0.0000, 0.0000)>
spline: 1
start index: 0
starts location: <Vector (2.0000, 0.0000, 0.0000, 0.0000)>
end index: 4
end location: <Vector (1.9818, 1.0072, 0.0000, 0.0000)>
spline: 2
start index: 0
starts location: <Vector (0.0000, 1.0000, 0.0000, 0.0000)>
end index: 31
end location: <Vector (-0.1951, 0.9808, 0.0000, 0.0000)>


Not sure is there has a convenient function in blender api to do this. But my idea is separate by Loose Parts to single object and for every object duplicated, find the correct order of the vertices. finally delete them and restore the select data.

Suppose the object is:

First, do the separate function. You can use

bpy.ops.mesh.separate(type='LOOSE')


to achieve.

here is the function, it will return the objects are separated.

# Suppose in Object mode, and the object selected
import bpy, bmesh

def separate_obj():
context_obj_old     = bpy.context.object
data_objects_old    = [obj for obj in bpy.data.objects]
old_selected        = bpy.context.selected_objects
for obj in old_selected:
obj.select_set(False)
bpy.context.object.select_set(True)
bpy.ops.object.duplicate()
object_duplicated   = bpy.context.object
bpy.ops.object.mode_set(mode = 'EDIT')

obj     = bpy.context.object
me      = obj.data
bm      = bmesh.from_edit_mesh(me)

verts       = bm.verts
edges       = bm.edges
verts.ensure_lookup_table()

bm.select_mode |= {'VERT'}
bm.select_flush_mode()

# deslect all
for v in verts:
v.select_set(False)

while verts:
# select first vert
verts[0].select_set(True)

bmesh.update_edit_mesh(me)
bpy.ops.mesh.separate(type='SELECTED')
verts.ensure_lookup_table()

bpy.ops.object.mode_set(mode = 'OBJECT')
islands = [obj for obj in bpy.data.objects if obj not in data_objects_old and obj != object_duplicated]

for i in islands:
i.select_set(False)

object_duplicated.select_set(True)
bpy.ops.object.delete(use_global=True, confirm=False)

for obj in old_selected:
obj.select_set(True)

bpy.context.view_layer.objects.active = context_obj_old
return islands

islands = separate_obj()
print(islands)
print("")


next step is find the correct order of vertices for every objects

Full code

# Suppose in Object mode, and the object selected
import bpy, bmesh

def separate_obj():
context_obj_old     = bpy.context.object
data_objects_old    = [obj for obj in bpy.data.objects]
old_selected        = bpy.context.selected_objects
for obj in old_selected:
obj.select_set(False)
bpy.context.object.select_set(True)
bpy.ops.object.duplicate()
object_duplicated   = bpy.context.object
bpy.ops.object.mode_set(mode = 'EDIT')

obj     = bpy.context.object
me      = obj.data
bm      = bmesh.from_edit_mesh(me)

verts       = bm.verts
edges       = bm.edges
verts.ensure_lookup_table()

bm.select_mode |= {'VERT'}
bm.select_flush_mode()

# deslect all
for v in verts:
v.select_set(False)

while verts:
# select first vert
verts[0].select_set(True)

bmesh.update_edit_mesh(me)
bpy.ops.mesh.separate(type='SELECTED')
verts.ensure_lookup_table()

bpy.ops.object.mode_set(mode = 'OBJECT')
islands = [obj for obj in bpy.data.objects if obj not in data_objects_old and obj != object_duplicated]

for i in islands:
i.select_set(False)

object_duplicated.select_set(True)
bpy.ops.object.delete(use_global=True, confirm=False)

for obj in old_selected:
obj.select_set(True)

bpy.context.view_layer.objects.active = context_obj_old
return islands

islands = separate_obj()
print(islands)
print("")

# -- store selected data
context_obj_old     = bpy.context.object
old_selected        = bpy.context.selected_objects
for obj in old_selected:
obj.select_set(False)
# -- store selected data END

list_coords = []

for obj in islands:
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode = 'EDIT')
me      = obj.data
bm      = bmesh.from_edit_mesh(me)

verts       = bm.verts
edges       = bm.edges
verts.ensure_lookup_table()
edges.ensure_lookup_table()

bm.select_mode |= {'VERT'}
bm.select_flush_mode()

# deslect all
for v in verts:
v.select_set(False)

bm.select_mode |= {'EDGE'}
bm.select_flush_mode()

coords = []
first_edge = None

if edges:
for e in edges:
if l0 > 2 or l1 > 2:
first_edge = False
break

if l0 == 1 or l1 == 1:
first_edge = e
break

if first_edge is None:      # means closed path
first_edge = edges[0]
elif first_edge is False:   # complex path
list_coords.append(coords)
bpy.ops.object.mode_set(mode = 'OBJECT')
continue

p1          = first_edge.verts[0]
p2          = first_edge.verts[1]

p1, p2  = p2, p1

if len_link > 2: # complex path
list_coords.append(coords)
bpy.ops.object.mode_set(mode = 'OBJECT')
continue
list_coords.append([*p1.co], [*p2.co])
bpy.ops.object.mode_set(mode = 'OBJECT')
continue

else:

coords.append([*p1.co])
coords.append([*p2.co])
# print(p1.co)
# print(p2.co)

all_edges = [e for e in edges]

all_edges.remove(first_edge)

while all_edges:
p1          = p2
first_edge  = next_edge
# print(p1.co, first_edge.verts[0].co, first_edge.verts[1].co)
if p1 == first_edge.verts[0]:
p2 = first_edge.verts[1]
else:
p2 = first_edge.verts[0]
# print(p2.co)

if len_link > 2: # complex path
coords.clear()
break
coords.append([*p2.co])
break

else:

coords.append([*p2.co])
all_edges.remove(first_edge)

else:
# one vert only
verts.ensure_lookup_table()
coords.append(verts[0].co.copy())

list_coords.append(coords)
bpy.ops.object.mode_set(mode = 'OBJECT')

for obj in islands:
obj.select_set(True)
bpy.ops.object.delete(use_global=True, confirm=False)

# -- restore selected data
for obj in old_selected:
obj.select_set(True)

bpy.context.view_layer.objects.active = context_obj_old
# -- restore selected data END

print("### This is local space ###")
for coords in list_coords:
print("[")
for v in coords:
print("    ",v)
print("]")

print("")
if list_coords[0][0] == list_coords[0][-1]:
print("list in index 0 is closed path, else it has two vertices on the same position")
else:
print("list in index 0 is not closed path")

print("-- END --")