Using a ray cast, this is snapping cursor on face centers (pressing F), with rotation (F + shift).
in object mode, edit mode, with subdivision modifier
My script was on a active object following the cursor but I simplified for the example. this is quite the same
import bpy
import bmesh
from mathutils import Matrix
from bpy_extras import view3d_utils
class ModalOperator(bpy.types.Operator):
bl_idname = "object.modal_operator"
bl_label = "Simple Modal Operator"
def ray_cast(self, context, event):
scene = context.scene
region = context.region
rv3d = context.region_data
coord = event.mouse_region_x, event.mouse_region_y
viewlayer = context.view_layer
if bpy.app.version >= (2, 91, 0): #well actually under 2.93 but I let it
viewlayer = viewlayer.depsgraph
# get the ray from the viewport and mouse
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
return scene.ray_cast(viewlayer, ray_origin, view_vector)
def face_center(self, context, event):
result, location, normal, index, object, matrix = self.ray_cast(
context, event)
if object:
subsurf = [mod for mod in object.modifiers if mod.type == 'SUBSURF']
if subsurf:
depsgraph = context.evaluated_depsgraph_get()
if context.mode == 'OBJECT':
object_eval = object.evaluated_get(depsgraph)
face = object_eval.data.polygons[index]
else:
mesh_from_eval = bpy.data.meshes.new_from_object(
object_eval)
face = mesh_from_eval.polygons[index]
else:
face = object.data.polygons[index]
mw = object.matrix_world
me=object.data
loc = mw @ face.center
if object.mode == 'OBJECT':
if event.shift: # + rotation copy
bm = bmesh.new()
bm.from_mesh(me)
bm.transform(mw)
bm.normal_update() # if the obj has rotation
bm.faces.ensure_lookup_table()
f = bm.faces[index]
n = f.normal
t = f.calc_tangent_edge_pair().normalized()
bt = n.cross(t).normalized()
M = Matrix([t, bt, n]).transposed().to_4x4() #rotation
loc = M.translation = f.calc_center_median()
context.scene.cursor.location = (0,0,0)
context.scene.cursor.matrix = M # or on an object obj.matrix_world = M
bm.free()
else:
context.scene.cursor.location = loc
else: # edit
if event.shift:
bm = bmesh.from_edit_mesh(me)
bm.faces.ensure_lookup_table()
f = bm.faces[index]
n = [email protected]()
t = f.calc_tangent_edge_pair().normalized()@mw.inverted()
bt = n.cross(t).normalized()
R = Matrix([t, bt, n]).transposed().to_4x4()
context.scene.cursor.matrix = R
context.scene.cursor.location = loc
bmesh.update_edit_mesh(me)
else:
context.scene.cursor.location = loc
def modal(self, context, event):
# to face center
if event.type == 'F' and event.value == 'PRESS':
self.face_center(context, event)
return {'RUNNING_MODAL'}
elif event.type in {'RIGHTMOUSE', 'ESC'}:
return {'CANCELLED'}
elif event.type in {'SPACE', 'RET'}:
return {'FINISHED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def register():
bpy.utils.register_class(ModalOperator)
def unregister():
bpy.utils.unregister_class(ModalOperator)
if __name__ == "__main__":
register()
# test call
bpy.ops.object.modal_operator('INVOKE_DEFAULT')
on an object instead of cursor in edit mode I did
mw = context.object.matrix_world
n = f.normal @ mw.inverted()
t = f.calc_tangent_edge_pair().normalized() @ mw.inverted()
and here is an example in object mode on a active object
Align empty to normal with object matrix world rotation?
normal
andtangent
with.normalized()
. $\endgroup$