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 when there is a mix of flipped normals.
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;
}
}
For reference here is my first attempt without modifications. See below for a more recent version of the script.
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---")
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.
And now here is my latest iteration of the script. 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.
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. This code does work but only when all normals are facing the same direction, which defeats the purpose of having the code in the first place.
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()
I suspect that this part is not really correct but when I try to set the loop to a link_loop_prev or link_loop_next (with or without a link_loop_radial) then no match is made in BM_edge_other_loop.
else:
print("CCCCCCCCCCCCCCCCCCCCCCCCCCCC")
loop = new_loop.link_loop_radial_next