A way to Align the 3dCursor

Is there A way to Align the 3dCursor to a selection in python?.. of course there is, but is there a way of doing so which also takes the rotation in account? Like the interactive 3D cursor placing tools does?
When I have a Face selected, and hit “snap_cursor_to_selected” , shouldn´t it be possible to take the normal in account and rotate the cursor to it?
My ideal scenario would be, that it snaps to the middle/average of a selection (which it does by default)
but then, also rotates the 3dCursor to an average /( or a somehow fitted Plane- I guess it would have to be at least 3 Verts)
Is there a way of doing this in python?

Set the location with bpy.context.scene.cursor.location
Set the rotation with bpy.context.scene.cursor.rotation

I think that his issue is finding the actual matrix for the selection.


You can use a custom Transformation Orientation then use the matrix from it for the cursor, you can then utilize the API calls testure provided above.

Or try something like this.


Thanks Kkar The Idea is pretty neat. Creating them seems pretty doable in python-
But how can I obtain that Matrix from a Custom Transformation orientation?

last I checked you cannot. The workaround in previous years has been to create an empty using the custom transform orientation, copy the matrix, then delete the object. good times!

i would love to be wrong though, so if that is not the case someone should feel free to correct me :slight_smile:

Allow me to correct myself- doing some experimentation in the python console it appears you can get the custom matrix from a custom transform orientation. example:

That is what I used in one of my add-ons.

Thanks for the hints, but i am actually pretty clueless on how to implement them-
in my phantasie it would work something like this:

import bpy

#create a custom orientation from selection, rename it
bpy.ops.transform.create_orientation(name=“3pointplane”, use=True, overwrite=True)

#somehow get the transformation matrix from it

#somehow feed the transformation matrix into the location/rotation of the 3d Cursor

#create a plane aligned to the Cursor
bpy.ops.mesh.primitive_plane_add(size=4, enter_editmode=False, align=‘CURSOR’)
bpy.context.object.display_type = ‘WIRE’

Any tips on how to “glue” this steps together??

There’s an operator in an addon called KeKit that does exactly what you’re asking about. The addon can be found here: https://artbykjell.com/blender.html
and the relevant blenderartists thread here: keKit for Blender (2.8+)

for posterity, I’ll paste the relevant code so you can learn from it (hopefully @Kiellog does not mind, if so let me know):

class VIEW3D_OT_cursor_fit_selected_and_orient(bpy.types.Operator):
    bl_idname = "view3d.cursor_fit_selected_and_orient"
    bl_label = "Cursor snap to selected and orient"
    bl_description = "Snap Cursor to selected + orient to FACE/VERT/EDGE normal. No selection = Cursor reset " \
                     "Note: Works with mouse over on faces in Object mode."
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_options = {'REGISTER'}

    cursorOP = True
    mouse_pos = Vector((0, 0))

    def invoke(self, context, event):
        self.mouse_pos[0] = event.mouse_region_x
        self.mouse_pos[1] = event.mouse_region_y
        return self.execute(context)

    def execute(self, context):
        og_cursor_setting = str(context.scene.cursor.rotation_mode)
        self.cursorOP = bpy.context.scene.kekit.cursorfit

        if not self.cursorOP:
            # GRAB CURRENT ORIENT & PIVOT (to restore at the end)
            og_orientation = str(bpy.context.scene.transform_orientation_slots[0].type)
            og_pivot = str(bpy.context.scene.tool_settings.transform_pivot_point)

        if bpy.context.mode == "EDIT_MESH":
            sel_mode = bpy.context.tool_settings.mesh_select_mode[:]
            obj = bpy.context.edit_object
            obj_mtx = obj.matrix_world.copy()
            bm = bmesh.from_edit_mesh(obj.data)


            vert_mode = True
            sel_verts = [v for v in bm.verts if v.select]
            sel_count = len(sel_verts)

            if sel_count == 0:
                return {'FINISHED'}

            # POLY MODE -----------------------------------------------------------------------
            if sel_mode[2]:
                sel_poly = [p for p in bm.faces if p.select]

                if sel_poly:

                    # sel_islands = get_islands(bm, sel_verts)

                    v_normals = [p.normal for p in sel_poly]
                    v_tan = [p.calc_tangent_edge_pair() for p in sel_poly]
                    face = sel_poly[-1]

                    normal = correct_normal(obj_mtx, sum(v_normals, Vector()) / len(v_normals))
                    tangent = correct_normal(obj_mtx, sum(v_tan, Vector()) / len(v_tan))

                    if len(sel_poly) == 1:
                        pos = obj_mtx @ bm.faces[face.index].calc_center_median()
                        ps = [v.co for v in sel_verts]
                        pos = obj_mtx @ average_vector(ps)

                    # fallback for all faces selected type of scenarios
                    if sum(normal) == 0:
                        normal = Vector((0,0,1))
                        tangent = Vector((1,0,0))

                    rot_mtx = rotation_from_vector(normal, tangent, rw=False)
                    set_cursor(rot_mtx, pos=pos)


                vert_mode = False

            # EDGE MODE -----------------------------------------------------------------------
            if sel_mode[1]:

                loop_mode = False
                line_mode = False

                sel_edges = [e for e in bm.edges if e.select]
                e_count = len(sel_edges)

                if e_count > 1:
                    vps = [e.verts[:] for e in sel_edges]
                    loops = get_loops(vps, legacy=True)
                    if len(loops) >= 1 and loops[0][0] != loops[0][-1]:
                        fl = list(set(flatten(vps)))
                        if len(fl) == len(loops[0]):
                            line_mode = True

                if e_count == 1:

                    ev = sel_edges[0].verts[:]
                    n = Vector((ev[0].normal + ev[1].normal) * .5).normalized()
                    t_v = Vector((ev[0].co - ev[1].co).normalized())
                    vert_mode = False

                elif e_count == 2 or line_mode:
                    shared_face = []
                    for f in sel_edges[0].link_faces:
                        for fe in sel_edges[1].link_faces:
                            if fe == f:
                                shared_face = f

                    ev = sel_edges[0].verts[:]
                    etv = sel_edges[1].verts[:]

                    n = Vector((ev[0].normal + ev[1].normal)*.5).normalized()
                    t_v = Vector((etv[0].normal + etv[1].normal)*.5).normalized()

                    if abs(round(n.dot(t_v),2)) == 1 or shared_face or line_mode:
                        avg_v, avg_e = [], []
                        for e in sel_edges:
                            avg_v.append((e.verts[0].normal + e.verts[1].normal) * .5)
                            uv = Vector(e.verts[0].co - e.verts[1].co).normalized()

                        n = average_vector(avg_v)
                        # I forgot why i needed these checks?
                        if sum(n) == 0:
                            n = avg_v[0]
                        t_v = average_vector(avg_e)
                        if sum(t_v) == 0:
                            t_v = avg_e[0]

                        if shared_face:
                            t_v = avg_e[0]

                        vert_mode = False

                elif e_count > 2:
                    loop_mode = True

                    startv = Vector(sel_edges[0].verts[0].co - sel_edges[0].verts[1].co).normalized()
                    cv1 = Vector(sel_edges[1].verts[0].co - sel_edges[1].verts[1].co).normalized()
                    cdot = abs(round(startv.dot(cv1), 6))

                    for e in sel_edges[2:]:
                        v = Vector(e.verts[0].co - e.verts[1].co).normalized()
                        vdot = abs(round(startv.dot(v), 3))
                        if vdot < cdot:
                            cv1 = v
                            cdot = vdot

                    n = startv
                    t_v = cv1
                    vert_mode = False

                # final pass
                n = correct_normal(obj_mtx, n)
                t_v = correct_normal(obj_mtx, t_v)
                n = n.cross(t_v)

                # vert average fallback
                if sum(t_v) == 0 or sum(n) == 0:
                    vert_mode = True

                if not vert_mode:
                    if loop_mode:
                        rot_mtx = rotation_from_vector(n, t_v, rotate90=True)
                        rot_mtx = rotation_from_vector(n, t_v, rotate90=False)


            # VERT (& GENERAL AVERAGE) MODE -----------------------------------------------------------------------
            if sel_mode[0] or vert_mode:

                if sel_count == 2:
                    n = Vector(sel_verts[0].co - sel_verts[1].co).normalized()
                    v_n = [v.normal for v in sel_verts]
                    t_v = correct_normal(obj_mtx, sum(v_n, Vector()) / len(v_n))
                    n = correct_normal(obj_mtx, n)
                    t_v = t_v.cross(n)

                    rot_mtx = rotation_from_vector(n, t_v)

                elif sel_count == 3:
                    cv = [v.co for v in sel_verts]

                    # make triangle vectors, sort to avoid the hypot.vector
                    h = tri_points_order(cv)
                    tri = sel_verts[h[0]].co, sel_verts[h[1]].co, sel_verts[h[2]].co
                    v1 = Vector((tri[0] - tri[1])).normalized()
                    v2 = Vector((tri[0] - tri[2])).normalized()
                    v1 = correct_normal(obj_mtx, v1)
                    v2 = correct_normal(obj_mtx, v2)
                    n_v = v1.cross(v2)

                    # flipcheck
                    v_n = [v.normal for v in sel_verts]
                    ncheck = correct_normal(obj_mtx, sum(v_n, Vector()) / len(v_n))
                    if ncheck.dot(n_v) < 0:

                    # tangentcheck
                    c1 = n_v.cross(v1).normalized()
                    c2 = n_v.cross(v2).normalized()
                    if c1.dot(n_v) > c2.dot(n_v):
                        u_v = c2
                        u_v = c1
                    t_v = u_v.cross(n_v).normalized()

                    rot_mtx = rotation_from_vector(n_v, t_v)

                elif sel_count != 0:

                    v_n = [v.normal for v in sel_verts]
                    n = correct_normal(obj_mtx, sum(v_n, Vector()) / len(v_n))

                    if sel_count >= 1:
                        if sel_count == 1:
                            if not sel_verts[0].link_edges:
                                # floater vert check -> world rot
                                t_c = Vector((1,0,0))
                                n = Vector((0,0,1))
                                t_c = sel_verts[0].co - sel_verts[0].link_edges[0].other_vert(sel_verts[0]).co
                            t_c = sel_verts[0].co - sel_verts[1].co

                        t_c = correct_normal(obj_mtx, t_c)
                        t_v = n.cross(t_c).normalized()

                        rot_mtx = rotation_from_vector(n, t_v)

                elif sel_count == 0:

                bmesh.update_edit_mesh(obj.data, True)

        # OBJECT MODE -----------------------------------------------------------------------
        elif bpy.context.mode == "OBJECT":
            sel_obj = [o for o in context.selected_objects]
            hit_obj, hit_wloc, hit_normal, hit_face = mouse_raycast(context, self.mouse_pos)

            if hit_normal and hit_obj:
                obj_mtx = hit_obj.matrix_world.copy()

                bm = bmesh.new()

                normal = bm.faces[hit_face].normal
                tangent = bm.faces[hit_face].calc_tangent_edge_pair()
                pos = obj_mtx @ bm.faces[hit_face].calc_center_median()

                rot_mtx = rotation_from_vector(normal, tangent, rw=False)
                rot_mtx = obj_mtx @ rot_mtx
                set_cursor(rot_mtx, pos=pos)

            elif sel_obj and not hit_normal:
                context.scene.cursor.location = sel_obj[0].location
                context.scene.cursor.rotation_euler = sel_obj[0].rotation_euler


        if not self.cursorOP:
            bpy.context.scene.tool_settings.transform_pivot_point = og_pivot

        if og_cursor_setting != "QUATERNION":
            # just gonna go ahead and assume no one uses this as default, for back-compatibility reasons...
            context.scene.cursor.rotation_mode = og_cursor_setting
            context.scene.cursor.rotation_mode = 'XYZ'

        return {'FINISHED'}
1 Like

No problem ;>

That script is a bit old, and I hacked together a lot of needless maths tho ;> Jimmy might want to look at “mouse axis move” (in the kit) for a simpler solution with custom orientations.
excerpt from normal orientation: (youd set the self.tm orientation to the cursor later)
or …use the kit ;D (plug)

        elif em:
            sel = [v for v in context.object.data.vertices if v.select]
            if sel:
                    bpy.ops.transform.create_orientation(name='keTF', use_view=False, use=True, overwrite=True)
                    self.tm = context.scene.transform_orientation_slots[0].custom_orientation.matrix.copy()
                except RuntimeError:
                    print("Fallback: Invalid selection for Orientation - Using Local")
                    # Normal O. with a entire cube selected will fail create_o.
                    self.tm = context.object.matrix_world.to_3x3()
                self.report({"INFO"}, " No elements selected ")
                return {'CANCELLED'}

Wow thanks for the Input, will try to use it :slight_smile:

But I have the problem that I get a Error in the system console:
RuntimeError: Error: Create Orientation’s ‘use’ parameter only valid in a 3DView context

I feel pretty stupid right now, but is there a way to test-run my script from the 3dView?

If you’re trying to run your script from the script editor you can override the context of any other operator: https://docs.blender.org/api/current/bpy.ops.html

this is typically done by creating a copy of the current context and then changing whatever you want in it (it’s just a dictionary).

ctx = bpy.context.copy()
ctx['area'] = your_area

Hi, thanks for all the hints- still i am pretty lost!

This is what i have so far

import bpy


bpy.ops.transform.create_orientation(name=‘3PointPlane’, use_view=False, use=True, overwrite=True)


custom_matrix = bpy.context.scene.transform_orientation_slots[0].custom_orientation.matrix.copy()

print (custom_matrix)


So I have a custom Matrix now-great!!

-but how do I feed it into the cursor rotation now?

Cursor rotation seems to have 4 options-



What would be the right way/syntax to get the Matrix into the cursor rotation?

you could decompose the matrix

loc, rot, scale = my_matrix.decompose()

I get an error

Matrix.decompose(): inappropriate matrix size - expects 4x4 matrix

my matrx looks like this

<Matrix 3x3 (-0.0079, 1.0000, 0.0000)
( 0.0951, 0.0007, -0.9955)
(-0.9954, -0.0078, -0.0951)>

you can convert a 3x3 matrix to a 4x4 matrix with the to_4x4() function

loc, rot, scale = my_matrix.to_4x4().decompose()

Thanks :slight_smile:

I am facing a similar problem as you and in search of the solution I came across this post.

Would it be possible for you to share your solution in full at the end?

I am currently developing a kind of “auto-rig” for rotated geometry and the rotation of the 3D cursor is interesting.

Follow the thread with interest!

Here is what I have so far:

bpy.ops.transform.create_orientation(name='3Points', use_view=False, use=True, overwrite=True)


        custom_matrix = bpy.context.scene.transform_orientation_slots[0].custom_orientation.matrix.copy()

        print (custom_matrix)

        loc, rot, scale = custom_matrix.to_4x4().decompose()

        bpy.context.scene.cursor.rotation_quaternion =  (rot)

        bpy.ops.mesh.primitive_plane_add(size=4, enter_editmode=False, align='CURSOR')type or paste code here
1 Like

Thank u @Jimmy_James