Operator Redo when using a custom orientation + bpy.ops.transform.transform

Since the following has not received any attention the past few days, I figured I’d ask here as well: https://devtalk.blender.org/t/unable-to-get-custom-orientation-bpy-ops-transform-to-work-during-operator-redo/6348

Basically the operator adds a new mesh cylinder aligned to the current selection of faces. This works fine on initial invocation. However, if you modify my operators settings in the Redo panel, the alignment is immediately lost. The same code is run during the Redo so I’m at a loss as to why the orientation and transform call does not work.

Is there something I’m missing to properly support Redo?

It looks like some obscure bug in the underlying undo system, but much of it can be circumvented by replacing many of the operator calls with lower level functions. It usually simplifies the execution chain and often also guarantees that Blender doesn’t inadvertently push an undo at stage where it does weird things when operator redo is being run.

An issue with your script is that you’re toggling modes. This is hit or miss with multiple operators and undo.

If you’re trying to align a mesh to a surface while supporting operator redo, it’s best to do a low-level version that doesn’t include the usage of transform orientations. It’s better to create the transform using a matrix.

The rundown of the example below:

  1. Use bmesh to get the reference face’s normal and center point and convert these to a matrix
  2. Add the cylinder in edit mode
  3. Transform the cylinder’s vertices using the new matrix

Example:

import bpy
import bmesh

class MESH_OT_add_cylinder_on_surface(bpy.types.Operator):
    bl_idname = 'mesh.add_cylinder_on_face'
    bl_label = 'Add Cylinder On Face'
    bl_options = {'REGISTER', 'UNDO'}

    verts: bpy.props.IntProperty(min=3, default=32)
    
    @classmethod
    def poll(self, context):
        a = context.area.type == 'VIEW_3D'
        b = context.objects_in_mode
        return a and b

    def execute(self, context):
        bm = bmesh.from_edit_mesh(context.object.data)

        mat = None

        # for purpose of example, get the matrix from a single face.
        # could be expanded to take a normal average of selected faces.
        for face in bm.faces:
            if face.select:
                mat = face.normal.to_track_quat('Z', 'Y').to_matrix().to_4x4()
                mat.translation = face.calc_center_median()
                break

        if mat:
            bpy.ops.mesh.primitive_cylinder_add(vertices=self.verts)
            
            # at this point the new mesh is already selected (and we still have the bmesh)
            for vert in bm.verts:
                if vert.select:
                    # subtract cursor location since the add
                    # primitve operator uses it as mesh origin
                    vert.co = mat @ (vert.co - context.scene.cursor.location)
            bm.normal_update()
        return {'FINISHED'}

def register():
    bpy.utils.register_class(MESH_OT_add_cylinder_on_surface)

if __name__ == '__main__':
    register()

Edit: Simplified a bit

1 Like

Sigh, that’s pretty sucky. I’ll probably file a bug anyhow. I have come across some other recently filed bugs with operators and undo in the tracker too. Such a minefield.

In the meantime, thank you very much for this! I’m going to push my luck and hope that create_orientation remains a ‘hit’ here. I’m keeping that call so I can steal it’s matrix to 1) not have to rewrite the normal averaging myself and 2) remove the need iterate over a lot of faces in python.

Unfortunately I will have to keep the per-vert python iteration + matrix op :frowning: (which is not fun for my poly spheres “primitives” in my real script). Those also require a mode switch so I can take my base cube and sub-d + spherize it + join it back in. That still seems to work with REDO after using your code here though so I’m keeping my fingers crossed…

Thanks again!

If you’re interested I wrote a function for getting average vectors back when exploring ways to extract vertex coordinates as fast as possible. It scales reasonably well with small and dense meshes.

Transforming verts in edit-mode is faster than switching modes. Every time edit mode is toggled Blender needs to transfer between edit-mesh and evaluated mesh.

Some transform numbers on a 50k tri mesh:

  • Iteratively transforming all vertices by on a bmesh in edit-mode: 7 ms
  • Transform by vectorization using bmesh.ops.transform(): 5 ms
  • Toggling to object mode, transforming, toggling back: 27 ms.
  • Transform by vectorization using obj.data.transform: < 1 ms

The last one requires object mode and will transform every vertex.

A lot of the operations you mention, polysphere from a subdivided cube, etc. is possible without ever leaving edit-mode. If you’d like an example on this I’ll be happy to provide one.

Anyway, not trying to prove a point. Just wanted to let you know there are options if performance is an issue :slight_smile:

Well I’m not gonna turn down code :slight_smile: I’d be interested for sure. Really appreciate it and others may stumble across the thread too.

Actually when I was working with your suggestion I had some trouble when the object had non-uniform scale (e.g scaled by 2 along y-axis in object mode etc) and I wasn’t successful in directly addresseing it. I instead had to switch back to obj mode and join again, but kept the manual transform code afterwards. Any suggestions for that scenario - 3dmath continues to be a huge weakness of mine.