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 ) 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---")