Walking edge loops across a mesh. From C to Python

So I’ve been messing around with different methods of walking across a mesh to get edge loops and one of the methods I found was described in this Stack Exchange post:

It’s super easy:

loop = loop.link_loop_prev.link_loop_radial_prev.link_loop_prev

However, the method only works properly when all of the mesh normals are facing the same direction–either all in or all out, no mixture of flipped faces. If there are mixed normals it will deviate and grab the wrong loop because bmesh loops exist relative to the face normal’s direction.

Blender has internal logic to deal with this and @ideasman42 provided a link to the relevant source code in the Stack Exchange comments. I don’t know C (I hardly ‘know’ Python for that matter :stuck_out_tongue: ) but I’ve attempted to translate this from C to Python without success. It never returns the correct loop.

Can anyone point out what I’m doing wrong? Is it an error in translation? Am I feeding it the wrong edge/loop? Am I missing something else from elsewhere in Blender’s C source?

Here’s the C code with added comments of what I think is going on. (Note: The second function calls the first one so you might want to start reading there first.)

/**
 * Given a edge and a loop (assumes the edge is manifold). returns
 * the other faces loop, sharing the same vertex.
 *
 * <pre>
 * +-------------------+
 * |                   |
 * |                   |
 * |l_other <-- return |
 * +-------------------+ <-- A manifold edge between 2 faces
 * |l    e  <-- edge   |
 * |^ <-------- loop   |
 * |                   |
 * +-------------------+
 * </pre>
 */
BMLoop *BM_edge_other_loop(BMEdge *e, BMLoop *l)  // Takes an edge and a loop
{
	BMLoop *l_other;  // Declaring the var?  Not a thing in Python.

	// BLI_assert(BM_edge_is_manifold(e));  // TOO strict, just check if we have another radial face
	BLI_assert(e->l && e->l->radial_next != e->l);  // Checking if we're at the mesh boundary?
	BLI_assert(BM_vert_in_edge(e, l->v));  // Check if starting loop's vert is in the edge?

	l_other = (l->e == e) ? l : l->prev;  // Google says the ? : in C is a conditional operator.  ? If true then value x : otherwise value y
	l_other = l_other->radial_next;  // After setting l_other, immediately set it as its own link_loop_radial_next
	BLI_assert(l_other->e == e);  // Another check if we're at the mesh boundary? (Because we just did a radial link)

	if (l_other->v == l->v) {  // If l_other vert is our starting loop vert
		/* pass */
	}
	else if (l_other->next->v == l->v) {  // elif the next loop's vert is the starting loop vert
		l_other = l_other->next;  // Use the next loop instead
	}
	else {
		BLI_assert(0);  // If none of the above, assert
	}

	return l_other;
}

/**
 * Utility function to step around a fan of loops,
 * using an edge to mark the previous side.
 *
 * \note all edges must be manifold,
 * once a non manifold edge is hit, return NULL.
 *
 * <pre>
 *                ,.,-->|
 *            _,-'      |
 *          ,'          | (notice how 'e_step'
 *         /            |  and 'l' define the
 *        /             |  direction the arrow
 *       |     return   |  points).
 *       |     loop --> |
 * ---------------------+---------------------
 *         ^      l --> |
 *         |            |
 *  assign e_step       |
 *                      |
 *   begin e_step ----> |
 *                      |
 * </pre>
 */

BMLoop *BM_vert_step_fan_loop(BMLoop *l, BMEdge **e_step)  // Takes a loop and an edge
{
	BMEdge *e_prev = *e_step;  // Declare new e_prev var and set it to the provided e_step
	BMEdge *e_next;  // Declaring empty var?  Not a thing in Python.
	if (l->e == e_prev) {  // If the starting loop's edge is e_step
		e_next = l->prev->e;  // Then next edge is loop.link_loop_prev.edge
	}
	else if (l->prev->e == e_prev) {  // elif previous loop's edge is already the e_prev
		e_next = l->e;  // Then the next edge is the starting loop's edge
	}
	else {  // else assert!
		BLI_assert(0);
		return NULL;
	}

	if (BM_edge_is_manifold(e_next)) {  // If the new edge is manifold, continue
		return BM_edge_other_loop((*e_step = e_next), l);  // Run the other function, passing e_next and the loop we started with as args
	}
	else {
		return NULL;
	}
}

And here’s my Pythonification that I’m running from the Script Editor. Just select a manifold edge and click Run in the script editor. It will print the next loop and its edge (and a bunch of other steps along the way).

import bpy
import bmesh


def BM_edge_other_loop(edge, loop):
    ### Pseudo-python. (there isn't an "edge.loop" in the bmesh python API so we'd need a bit more work but I'm skipping asserts for now)
    # if edge.loop and edge.loop.link_loop_radial_next != edge.loop:
    # if BM_vert_in_edge(edge, loop.vert) ### I can't actually find where this is defined in the source code.. just several places where it's used.

    if loop.edge == edge:
        l_other = loop
        print("Loop's edge is input edge. Setting other loop as the starting loop.")
    else:
        l_other = loop.link_loop_prev
        print("Setting other loop as previous loop")
    print("l_other first value:", l_other)
    
    l_other = l_other.link_loop_radial_next
    print("l_other radial value:", l_other)

    if l_other.edge == edge:
        print("We would assert here.")  # Skipping asserts for now.

    if l_other.vert == loop.vert:
        print("Loops have the same vert. Passing.")
        pass
    elif l_other.link_loop_next.vert == loop.vert:
        l_other = l_other.link_loop_next
        print("Setting other loop as link_loop_next")
    else:
        print("Nope!")  # Skipping asserts for now.  We'll just print some nonsense instead.
    
    print("l_other final value:", l_other)
    print("l_other's edge:", l_other.edge.index)
    return l_other


def BM_vert_step_fan_loop(loop, e_step):
    print("Starting loop's edge:", loop.edge.index)
    print("e_step is:", e_step.index)

    e_prev = e_step

    if loop.edge == e_prev:
        e_next = loop.link_loop_prev.edge
        print("Matched on first If")
    elif loop.link_loop_prev.edge == e_prev:
        e_next = loop.edge
        print("Matched on Elif")
    else:
        print("No match")
        return None

    print("e_next is:", e_next.index)

    if e_next.is_manifold:
        return BM_edge_other_loop(e_next, loop)
    else:
        print("Nonmanifold edge.")
        return None


#####################
print("---BEGIN---")

bm = bmesh.from_edit_mesh(bpy.context.object.data)
active_edge = bm.select_history.active
# previous_active_edge = bm.select_history[len(bm.select_history) - 2]

loop = active_edge.link_loops[0]
e_step = loop.link_loop_prev.edge
# e_step = previous_active_edge

BM_vert_step_fan_loop(loop, e_step)

print("---END---")

Actually, here try this one instead. I built a while loop (note: manually setting the allowed number of iterations because it WILL get stuck in an infinite loop otherwise). Currently I haven’t implemented any safety checks for things like mesh boundaries or non-quad faces.

My test object is a default cube, subdivided 4 times, with the normals flipped on a random selection of faces so I don’t have to worry about running into things like mesh borders while I’m testing.

Also made 1 modification in BM_edge_other_loop at the part with if l_other.vert == loop.vert: because passing seemed to break things in some cases and getting the link_loop_prev seemed to fix that. Sorry for the absurd number of print statements. Just trying to step my way through what’s happening.

Copy/paste to script editor, select an edge, and hit run.

import bpy
import bmesh


def BM_edge_other_loop(edge, loop):
    ### Pseudo-python. (there isn't an "edge.loop" in the bmesh python API so we'd need a bit more work but I'm skipping asserts for now)
    # if edge.loop and edge.loop.link_loop_radial_next != edge.loop:
    # if BM_vert_in_edge(edge, loop.vert) ### I can't actually find where this is defined in the source code.. just several places where it's used.

    if loop.edge == edge:
        l_other = loop
        print("Loop's edge is input edge. Setting other loop as the starting loop.")
    else:
        l_other = loop.link_loop_prev
        print("Setting other loop as previous loop")
    print("l_other first value:", l_other)
    
    l_other = l_other.link_loop_radial_next
    print("l_other radial value:", l_other)

#    if l_other.edge == edge:
#        print("We would assert here.")  # Skipping asserts for now.

    if l_other.vert == loop.vert:
        print("Loops have the same vert. Setting other loop as link_loop_prev instead of passing.")
        l_other = l_other.link_loop_prev  # Modified this one spot to get link_loop_prev instead of pass because that seemed to fix at least 1 broken case
#        pass
    elif l_other.link_loop_next.vert == loop.vert:
        l_other = l_other.link_loop_next
        print("Setting other loop as link_loop_next")
    else:
        print("Nope!")  # Skipping asserts for now.  We'll just print some nonsense instead.
        return None
    
    print("l_other final value:", l_other)
    print("l_other's edge:", l_other.edge.index)
    return l_other


def BM_vert_step_fan_loop(loop, e_step):
    print("Starting loop's edge:", loop.edge.index)
    print("e_step is:", e_step.index)

    e_prev = e_step

    if loop.edge == e_prev:
        e_next = loop.link_loop_prev.edge
        print("Matched on first If")
    elif loop.link_loop_prev.edge == e_prev:
        e_next = loop.edge
        print("Matched on Elif")
    else:
        print("No match")
        return None

    print("e_next is:", e_next.index)

    if e_next.is_manifold:
        return BM_edge_other_loop(e_next, loop)
    else:
        print("Nonmanifold edge.")
        return None


#####################
print("---BEGIN---")

bm = bmesh.from_edit_mesh(bpy.context.object.data)
active_edge = bm.select_history.active
e_step = active_edge
loop = e_step.link_loops[0].link_loop_next

new_sel = []
i = 0
while i <= 63:
    print("---")
    new_loop = BM_vert_step_fan_loop(loop, e_step)
    e_step = new_loop.edge
    
    new_sel.append(e_step.index)


    cur_vert = loop.vert
    oth_vert = loop.link_loop_radial_next.vert
    print("cur_vert:", cur_vert.index)
    print("oth_vert:", oth_vert.index)

    if cur_vert != oth_vert:
        print("AAAAAAAAAAAAAAAAAAAAAAAAAAAA")
        loop = new_loop.link_loop_next
    else:
        print("CCCCCCCCCCCCCCCCCCCCCCCCCCCC")
        loop = new_loop.link_loop_radial_next

    bm.select_history.add(e_step)
    i += 1

print("---END---")

for i in new_sel:
    bm.edges[i].select = True

bm.select_flush_mode()
bpy.context.object.data.update()

This would be a non-issue if Blender’s internal walkers were exposed to the Python API. Then they could be called directly.

Alright, because some future person might see this thread here is the result: The problem was in my own while loop.

The way that I was taking the output and deriving the next loop to continue stepping forward was flawed. My translation from C to Python didn’t require any additional changes–aside from that one line I had already changed (mentioned in 2nd post). I’m still not sure why the original C function has a pass in BM_edge_other_loop because it does not seem to function properly unless I change pass to l_other = l_other.link_loop_prev (in the Python translation).

So anyway my while loop went from this:

active_edge = bm.select_history.active
e_step = active_edge
loop = e_step.link_loops[0].link_loop_next

new_sel = []
i = 0
while i <= 63:
    new_loop = BM_vert_step_fan_loop(loop, e_step)
    e_step = new_loop.edge    
    new_sel.append(e_step.index)

    cur_vert = loop.vert
    oth_vert = loop.link_loop_radial_next.vert
    print("cur_vert:", cur_vert.index)
    print("oth_vert:", oth_vert.index)

    if cur_vert != oth_vert:
        loop = new_loop.link_loop_next
    else:
        loop = new_loop.link_loop_radial_next

    i += 1

:arrow_down: To this:

active_edge = bm.select_history.active

e_step = active_edge
# You can uncomment the # in the next line to reverse direction
loop = e_step.link_loops[0]#.link_loop_next

pcv = loop.vert  # Previous Current Vert (loop's vert)
pov = loop.edge.other_vert(loop.vert)  # Previous Other Vert

new_sel = []
i = 0
while i <= 63:
    new_loop = BM_vert_step_fan_loop(loop, e_step)
    if new_loop is None:
        print("REEEEEEEEE")
        break
    e_step = new_loop.edge
    
    new_sel.append(e_step.index)

    cur_vert = new_loop.vert
    oth_vert = new_loop.edge.other_vert(new_loop.vert)
    rad_vert = new_loop.link_loop_radial_next.vert

    if cur_vert == rad_vert and oth_vert != pcv:
        loop = new_loop.link_loop_next
        pcv = oth_vert
        pov = cur_vert
    elif oth_vert == pcv:
        loop = new_loop
        pcv = cur_vert
        pov = oth_vert
    elif cur_vert ==  pcv:
        loop = new_loop.link_loop_radial_next
        pcv = oth_vert
        pov = cur_vert
    else:
        print("Y U NO GO?")
        break

    i += 1

The important changes were the addition of pcv and pov and rad_vert and changing the way oth_vert is derived. With these we can get a direction for which way we came from when comparing the previous loop to the one we just got back from BM_vert_step_fan_loop. Then we can assign the next loop accordingly.

Here is the full proof of concept (warts and all) with most of the print statements commented out:

import bpy
import bmesh
import time


def BM_edge_other_loop(edge, loop):
    ### Pseudo-python. (there isn't an "edge.loop" in the bmesh python API so we'd need a bit more work but I'm skipping asserts for now)
    # if edge.loop and edge.loop.link_loop_radial_next != edge.loop:
    # if BM_vert_in_edge(edge, loop.vert) ### I can't actually find where this is defined in the source code.. just several places where it's used.

    if loop.edge == edge:
        l_other = loop
#        print("Loop's edge is input edge. Setting other loop as the starting loop.")
    else:
        l_other = loop.link_loop_prev
#        print("Setting other loop as previous loop")
#    print("l_other first value:", l_other)
    
    l_other = l_other.link_loop_radial_next
#    print("l_other radial value:", l_other)

#    if l_other.edge == edge:
#        print("We would assert here.")  # Skipping asserts for now.

    if l_other.vert == loop.vert:
#        print("Loops have the same vert. Setting other loop as link_loop_prev instead of passing.")
        l_other = l_other.link_loop_prev  # Modified this one spot to get link_loop_prev instead of pass because that seemed to fix at least 1 broken case
#        pass  # This isn't useful
    elif l_other.link_loop_next.vert == loop.vert:
        l_other = l_other.link_loop_next
#        print("Setting other loop as link_loop_next")
    else:
        print("Nope!")  # Skipping asserts for now.  We'll just print some nonsense instead.
        return None
    
#    print("l_other final value:", l_other)
#    print("l_other's edge:", l_other.edge.index)
    return l_other


def BM_vert_step_fan_loop(loop, e_step):
#    print("Starting loop's edge:", loop.edge.index)
#    print("e_step is:", e_step.index)

    e_prev = e_step

    if loop.edge == e_prev:
        e_next = loop.link_loop_prev.edge
#        print("Matched on first If")
    elif loop.link_loop_prev.edge == e_prev:
        e_next = loop.edge
#        print("Matched on Elif")
    else:
        print("No match")
        return None

#    print("e_next is:", e_next.index)

    if e_next.is_manifold:
        return BM_edge_other_loop(e_next, loop)
    else:
        print("Nonmanifold edge.")
        return None


#####################
print("---BEGIN---")
t0 = time.perf_counter()
bm = bmesh.from_edit_mesh(bpy.context.object.data)
active_edge = bm.select_history.active

e_step = active_edge
# You can uncomment the # in the next line to reverse direction
loop = e_step.link_loops[0]#.link_loop_next
pcv = loop.vert  # Previous Current Vert (loop's vert)
pov = loop.edge.other_vert(loop.vert)  # Previous Other Vert

new_sel = []
i = 0
while i <= 63:
#    print("---")
#    print("loop face:", loop.face.index)
    new_loop = BM_vert_step_fan_loop(loop, e_step)
    if new_loop is None:
        print("REEEEEEEEE")
        break
    e_step = new_loop.edge
    
    new_sel.append(e_step.index)
#    print("new_loop face:", new_loop.face.index)

    cur_vert = new_loop.vert
    oth_vert = new_loop.edge.other_vert(new_loop.vert)
    rad_vert = new_loop.link_loop_radial_next.vert
#    print("pcv:", pcv.index)
#    print("pov:", pov.index)
#    print("cur_vert:", cur_vert.index)
#    print("oth_vert:", oth_vert.index)
#    print("rad_vert:", rad_vert.index)

    if cur_vert == rad_vert and oth_vert != pcv:
#        print("AAAAAAAAAAAAAAAAAAAAAAAAAAAA")
        loop = new_loop.link_loop_next
        pcv = oth_vert
        pov = cur_vert
    elif oth_vert == pcv:
#        print("BBBBBBBBBBBBBBBBBBBBBBBBBBBB")
        loop = new_loop
        pcv = cur_vert
        pov = oth_vert
    elif cur_vert ==  pcv:
#        print("CCCCCCCCCCCCCCCCCCCCCCCCCCCC")
        loop = new_loop.link_loop_radial_next
        pcv = oth_vert
        pov = cur_vert
    else:
        print("Y U NO GO?")
        break

    i += 1


t1 = time.perf_counter()
print("Runtime: %.15f sec" % (t1 - t0))  # Delete me later
print("---END---")

for i in new_sel:
    bm.edges[i].select = True

bm.select_flush_mode()
bpy.context.object.data.update()
4 Likes