It seems what you need is as simple as:
- Save current vertex positions in the world space (multiply local coords by
ob.matrix_world).
- Do whatever you want to do, it seems you figured it out because you can align the empty the way you want, but also apply that to the object.
- Since changing the object's transformation matrix will move vertices in the world space, restore their positions to the same world space as in p. 1. - so set local coords to saved copy of world space, multiplied by
ob.matrix_world.inversed().
import bpy, bmesh
from bpy import context as C
ob = C.object
me = ob.data
bm = bmesh.from_edit_mesh(me)
mat = ob.matrix_world
an error is better than the script failing silently
co0, co1 = map(lambda x:x.co, bm.select_history.active.verts[:2])
direction = co1 - co0
_loc, rot, _scl = mat.decompose()
rot_matrix = rot.to_matrix().to_4x4()
direction_vec = rot_matrix @ direction
normal_quat = direction_vec.to_track_quat('Z', 'Y')
normal_euler = normal_quat.to_euler('XYZ')
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.empty_add(type='ARROWS', align='WORLD', rotation=normal_euler, scale=(1, 1, 1))
save in world space
coords = [mat @ v.co for v in me.vertices]
maybe you want to position your empty
C.object.location = mat @ (co0 + co1) / 2
easiest way to apply new rotation
ob.rotation_euler = normal_euler
C.view_layer.update()
restore to old world space
mat_inv = ob.matrix_world.inverted()
for v, prev_co in zip(me.vertices, coords):
v.co = mat_inv @ prev_co
I don't understand everything you're trying to do, so maybe the code could be improved - for example, you multiply direction by only rotation matrix - but what if the scales aren't uniform? Why not simply multiply it by mat?
V2
Less readable but faster code:
import bpy, bmesh
from bpy import context as C
from mathutils import Matrix
ob = C.object
me = ob.data
bm = bmesh.from_edit_mesh(me)
mat = ob.matrix_world
an error is better than the script failing silently
co0, co1 = map(lambda x:mat @ x.co, bm.select_history.active.verts[:2])
normal_quat = (co1 - co0).to_track_quat('Z', 'Y')
normal_euler = normal_quat.to_euler('XYZ')
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.empty_add(type='ARROWS', align='WORLD', rotation=normal_euler, scale=(1, 1, 1))
maybe you want to position your empty
C.object.location = (co0 + co1) / 2
make local space = world space
me.transform(mat)
a faster way to calculate new matrix - without refreshing the View Layer
t, _, s = mat.decompose()
T = Matrix.Translation(t)
R = normal_euler.to_matrix().to_4x4()
S = Matrix.Diagonal(s.to_4d())
ob.matrix_world = T @ R @ S
now apply the inverted matrix on the mesh
me.transform(ob.matrix_world.inverted())
V3
The below solution never exits Edit Mode and applies only a single transform:
import bpy, bmesh
from bpy import context as C, data as D
from mathutils import Matrix
ob = C.object
me = ob.data
bm = bmesh.from_edit_mesh(me)
mat = ob.matrix_world
co0, co1 = map(lambda x:mat @ x.co, bm.select_history.active.verts[:2])
normal_quat = (co1 - co0).to_track_quat('Z', 'Y')
normal_euler = normal_quat.to_euler('XYZ')
t, r, s = mat.decompose()
quat_diff = normal_quat.rotation_difference(r)
mat_diff = quat_diff.to_matrix().to_4x4()
T = Matrix.Translation(t)
R = normal_euler.to_matrix().to_4x4()
S = Matrix.Diagonal(s.to_4d())
ob.matrix_world = T @ R @ S
add empty without going to Object mode
https://blender.stackexchange.com/a/51291/60486
empty = D.objects.new("empty", None)
C.scene.collection.objects.link(empty)
empty.empty_display_size = 2
empty.empty_display_type = 'ARROWS'
empty.empty_display_size = 1
empty.rotation_euler = normal_euler
empty.location = (co0 + co1) / 2
def slow():
"""possibly slower, but stays in edit mode"""
bm.transform(mat_diff) # transform only once
for f in bm.faces:
f.normal_update()
bmesh.update_edit_mesh(me)
def fast():
"""possibly faster, but switches modes"""
global bm # to write to it
bm.transform(mat_diff) # transform only once
bmesh.update_edit_mesh(me)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(me)
def best():
"""Probably the best of both worlds"""
bm.transform(mat_diff) # transform only once
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bmesh.update_edit_mesh(me)
if it really is slow or fast probably depends on the amount
of geometry and for small geos doesn't matter
best()
https://blender.stackexchange.com/questions/230321/rotate-origin-only-to-match-other-object-rotation
I found this example, but the use case is different and I had trouble applying it in this specific context.
– razed May 21 '22 at 16:05