The following is a response I received from Testure on Blender DevTalk:
It’s not only possible to do using bmesh, it is the correct way to do it.
In Blender, UVs are stored in loops, and loops can only be accessed through bmfaces. It’s super weird, but here’s a really rough breakdown of how it works:
First of all, when I talk about “loops” i’m not talking about edgeloops. In Bmesh, each face has N loops (where N is the number of edges in the face. Think of a loop as being a vert with a pointer to the next and previous verts that make up the face’s boundary. A loop is also like Bmesh’s attic. Any type of data that is not raw geometry coordinates gets crammed in there and you have to spend a lot of time digging through crap to find what you need (I could go on a pretty good rant about this but I digress). Anyway, that’s oversimplifying it a bit, but that should help you understand what I’m talking about.
OK so once you have a loop, you have to specify what loop layer you want data from. In your case, you want UVs- so you have to get and verify the UV layer using bm.loops.layers.uv.verify(). You’ll need that to manipulate UVs on any given loop.
Another tip- BMesh has no concept of UV islands or UV boundaries, or anything like that. If you have two quad faces joined by one edge, you might think that there are 6 verts, and thus- 6 UVs. You would be incorrect, there are two quads, so there are FOUR loops per face, and therefore 8 loops (thus, 8 UVs). If you want to “weld” two Uvs together, you simply give them the same coordinate, as long as they share a vertex Blender will treat them like they’re welded when a user goes to click on it in the UV editor. This is important to note- because if you’re trying to transform a vert’s UVs, it likely has around 4 UVs that all need to be transformed with it, or you’ve effectively “unwelded” the UVs and created a new split in the uv island.
I threw this simple script together, it should help you understand the nuance a bit better:
import bpy
import bmesh
from mathutils import Vector
def main():
me = bpy.context.edit_object.data
bm = bmesh.from_edit_mesh(me)
uv_layer = bm.loops.layers.uv.verify()
# it's useful to organize your UVs into a dictionary if you're manipulating a lot of stuff all at once,
# otherwise you're going to be looping through bmface.loops a LOT.
uv_verts = {}
for face in bm.faces:
print(f"Face #{face.index}")
for loop in face.loops:
print(f"\tv{loop.vert.index}: {loop[uv_layer].uv}")
if loop.vert not in uv_verts:
uv_verts[loop.vert] = [ loop[uv_layer] ]
else:
uv_verts[loop.vert].append( loop[uv_layer] )
# now that we have a dictionary of UV verts, it's fairly simple to do transformations on the UVs on a per vertex basis
# here we take all of the selected UV verts and move them up and to the right by .1
for vert in uv_verts:
for uv_loop in uv_verts[vert]:
if uv_loop.select:
uv_loop.uv += Vector( (10.0, 10.0) )
bmesh.update_edit_mesh(me, False, False)
if bpy.context.object.mode == "EDIT" and bpy.context.object.type == "MESH":
main()
Also, this is a really old doc so some of the info might be out of date/incorrect, but it was useful for me when I was trying to understanding how BMesh actually works.