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.

@jimmy.b

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.

https://godotengine.org/qa/22341/surface-normal-to-transform

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:
bpy.context.scene.transform_orientation_slots[0].custom_orientation.matrix

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
bpy.context.scene.transform_orientation_slots[0].custom_orientation.matrix

#somehow feed the transformation matrix into the location/rotation of the 3d Cursor
bpy.context.scene.cursor.location
bpy.context.scene.cursor.rotation

#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)

            bm.verts.ensure_lookup_table()
            bm.edges.ensure_lookup_table()
            bm.faces.ensure_lookup_table()

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

            if sel_count == 0:
                bpy.ops.view3d.snap_cursor_to_center()
                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()
                    else:
                        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)

                else:
                    bpy.ops.view3d.snap_cursor_to_center()

                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
                                break

                    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()
                            avg_e.append(uv)

                        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
                    n.negate()
                    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)
                    else:
                        rot_mtx = rotation_from_vector(n, t_v, rotate90=False)

                    set_cursor(rot_mtx)


            # 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)
                    set_cursor(rot_mtx)

                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:
                        n_v.negate()

                    # 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
                    else:
                        u_v = c1
                    t_v = u_v.cross(n_v).normalized()

                    rot_mtx = rotation_from_vector(n_v, t_v)
                    set_cursor(rot_mtx)

                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))
                            else:
                                t_c = sel_verts[0].co - sel_verts[0].link_edges[0].other_vert(sel_verts[0]).co
                        else:
                            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)
                        set_cursor(rot_mtx)

                elif sel_count == 0:
                    bpy.ops.view3d.snap_cursor_to_center()

                bm.select_flush_mode()
                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()
                bm.from_mesh(hit_obj.data)
                bm.faces.ensure_lookup_table()

                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

            else:
                bpy.ops.view3d.snap_cursor_to_center()


        if not self.cursorOP:
            # RESET OP TRANSFORMS
            bpy.ops.transform.select_orientation(orientation=og_orientation)
            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
        else:
            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)

# NORMAL / SELECTION
        elif em:
            context.object.update_from_editmode()
            sel = [v for v in context.object.data.vertices if v.select]
            if sel:
                try:
                    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()
                    bpy.ops.transform.delete_orientation()
                    restore_transform(og)
                except RuntimeError:
                    print("Fallback: Invalid selection for Orientation - Using Local")
                    # Normal O. with a entire cube selected will fail create_o.
                    bpy.ops.transform.select_orientation(orientation='LOCAL')
                    self.tm = context.object.matrix_world.to_3x3()
            else:
                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
bpy.ops.your.operator(ctx)

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

This is what i have so far

import bpy

bpy.ops.view3d.snap_cursor_to_selected()

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

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

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-

bpy.context.scene.cursor.rotation_
axis_angle
euler
mode
quaternion

“”"

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)

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

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

        print (custom_matrix)

        bpy.ops.view3d.snap_cursor_to_selected()
        loc, rot, scale = custom_matrix.to_4x4().decompose()

        bpy.context.scene.cursor.rotation_quaternion =  (rot)
        bpy.ops.object.mode_set(mode='OBJECT')


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

Thank u @Jimmy_James