Add-on to implement Maya-style "grouping" in the transform hierarchy

If this is not the correct place to post this, please let me know and I’ll move it.

Also: this is my first foray into coding in Blender, so I kind of have no idea what I am doing. This may be a mess. It hasn’t been heavily tested or vetted. So if anyone finds any bugs, has any improvements, or just wants to do some finger wagging, I am all ears!

The idea is to implement “grouping” in the outliner in a way that is analogous to how Maya does it. I know that this isn’t real grouping as defined by Blender. In reality, what this is is a shortcut to take a selection, create an empty at the same point in the transform hierarchy, and then to parent all the items in the selection to that new empty. I needed this because I am modeling in Blender, but rendering in Clarisse iFX and need to have a transform hierarchy that encodes my “grouping” intentions in the transform hierarchy and is preserved when saved in an alembic format.

The addon is based on the included Blender addon mesh - Parent_to_empty. I just modified it to work anywhere within the transform hierarchy, and to use world-space coordinates to make sure nothing moves during the parenting operation. I also made some changes to the original addon where it failed to work in world-space coordinates because it only ever worked on objects that originally had no parents.

Enjoy!

Edit: August 19th, 2018. I am fixing line 59 that had some weird, extra formatting characters in it from when I pasted in the original code.


# GPL # Original Author Liero, Modified by bvz2000 #

bl_info = {
           "name": "Maya style grouping",
           "category": "Object"
           }
           
import bpy
from bpy.types import Operator
from bpy.props import (
        StringProperty,
        BoolProperty,
        EnumProperty,
        )


def averageCenter(sel):
    print(len(sel))
    x = sum([obj.matrix_world.to_translation().x for obj in sel]) / len(sel)
    y = sum([obj.matrix_world.to_translation().y for obj in sel]) / len(sel)
    z = sum([obj.matrix_world.to_translation().z for obj in sel]) / len(sel)
    return (x, y, z)


class MayaStyleGrouping(Operator):
    bl_idname = "object.maya_group"
    bl_label = "Maya Style Grouping"
    bl_description = "Parent selected objects to a new Empty, maya style"
    bl_options = {"REGISTER", "UNDO"}

    groupName = StringProperty(
                    name="",
                    default='group',
                    description='Give the empty / group a name'
                    )
    alsoCreateGroup = BoolProperty(
                    name="Create Group",
                    default=False,
                    description="Also add objects to a group"
                    )
    location = EnumProperty(
                    name='',
                    items=[('CURSOR', 'Cursor', 'Cursor'), 
                           ('ACTIVE', 'Active', 'Active'),
                           ('CENTER', 'Center', 'Selection Center'),
                           ('ORIGIN', 'Origin', 'World Origin')],
                    description='Transform Group location',
                    default='CENTER'
                   )
    doRename = BoolProperty(
                    name="Add Prefix",
                    default=False,
                    description="Add prefix to objects name"
                    )

    @classmethod
    def poll(cls, context):
        selectedItems = context.selected_objects
        return len(selectedItems)

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "groupName")
        column = layout.column(align=True)
        column.prop(self, "location")
        column.prop(self, "alsoCreateGroup")
        column.prop(self, "doRename")

    def getParentDepth(self, item):
        parent = item.parent
        depth = 0
        while parent is not None:
            depth += 1
            parent = parent.parent
        return depth

    def execute(self, context):

        # Build a list of the selected objects, the currently selected object,
        # and the scene
        selectedItems = context.selected_objects
        act = context.object
        sce = context.scene

        # Do something here, I don't know what this does
        try:
            bpy.ops.object.mode_set()
        except:
            pass

        # Identify the location for the new empty
        if self.location == 'CURSOR':
            loc = sce.cursor_location
        elif self.location == 'ACTIVE':
            loc = act.location
        elif self.location == 'CENTER':
            loc = averageCenter(selectedItems)
        else:
            loc = (0, 0, 0)
        
        # Find the highest level parent. This is because if more than one object in different
        # hierarchies are selected, the resulting "group" of objects will be
        # parented under an empty that lives under the first, "highest" level
        # parent.
        parentDepthD = dict()
        for item in selectedItems:
            try:
                parentDepthD[self.getParentDepth(item)].append(item)
            except KeyError:
                parentDepthD[self.getParentDepth(item)] = [item]
        keys = list(parentDepthD.keys())
        keys.sort()
        highestLevelParent = parentDepthD[keys[0]][0].parent

        # Create the new empty in its correct location
        bpy.ops.object.add(type='EMPTY', location=(loc))
        newParent = context.object
        newParent.name = self.groupName
        if highestLevelParent is not None:
            savedMatrix = newParent.matrix_world
            newParent.parent = highestLevelParent
            newParent.matrix_world = savedMatrix
        # Uncomment the next two lines if you want your empty to have its name
        # automatically displayed in the 3DView, and it to have xray turned on.
        # I don't like those features so I turn them off by default.
        # newParent.show_name = True
        # newParent.show_x_ray = True

        if self.alsoCreateGroup:
            bpy.ops.group.create(name=self.groupName)
            bpy.ops.group.objects_add_active()

        for item in selectedItems:
            item.select = True
            savedMatrix = item.matrix_world
            item.parent = newParent
            item.matrix_world = savedMatrix
            if self.alsoCreateGroup:
                bpy.ops.group.objects_add_active()
            item.select = False
        for item in selectedItems:
            if self.doRename:
                item.name = self.groupName + '_' + item.name
        return {'FINISHED'}



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


def unregister():
    bpy.utils.unregister_class(MayaStyleGrouping)


# This allows you to run the script directly from blenders text editor
# to test the addon without having to install it.
if __name__ == "__main__":
    register()

1 Like

nice… thank you

Thanks for sharing

Thank you very much, Could you please explain how to add it to blender? I’m kinda new to this. Thank you.

I’m not at my computer (nor will I be for about three weeks), but I think you just:

Save this file somewhere on your drive as “MayaGrouping.py” or something like that.

Then go to preferences, add-ons, and click “add” or something like that.

Then pick the file you saved.

That should be it. If not, try googling it. It shouldn’t be too hard.

Cheers!

Not working
image

Let me take a look later today when I get back to my machine…

Ok, there is a typo in the original script on that line. I will fix it in the original post, but for your purposes, replace this line (on line 59 - the line that is giving you an error):

        return (len(selectedItems) & gt; 0)

with this line:

        return len(selectedItems)

Let me know if that fixes it. (The bug is a formatting bug that was introduced when I uploaded the code - which explains why I haven’t been having any issues with it on my local machine).

Alternatively, you can wait for a few moments and I will post an updated code in the original post above. Then you can just re-download the whole thing

Thanks for finding this error.

Well, tnx for quick reply, but…
image

I’m out again, but that is a basic error.

Make sure that that line lines up with the line above it, and that the leading whitespace (the blanks before the line starts) are all spaces and not tabs.

I’ll look at it again when I get home.

Looking at it again, the code seems right.

The error you are seeing is a classic python error that stems from one of three things. Most likely, in your case, it is because you have a tab character instead of spaces making up the first few characters of the line. Python allows you to use either tabs or spaces to line up the code, but not both. The convention is to use spaces, so if you used a tab, that could trigger this error. (It’s possible that the formatting of this forum might have stuck a tab in there and if you copied the line that that tab got into the code).

The other possibility is that the line does not line up with the line above it. Python requires that every line have a specific amount of indentation. The details are more than I want to get into here, but the short version is that this line has to line up exactly with the line above it (and has to use spaces - not tabs - to do so).

The last possibility is that there is a syntax error on another line that can occasionally trigger this error, but it isn’t as likely.

Let me know if any of this helps. If not, I’ll take another crack at fixing it.