Custom CollectionProperty different from builtin collections?

Hi,

It has been a year since I started learning python scripting for Blender. Recently I’ve noticed that the builtin collections such as data.objects, scene.objects, and many more are different from the ones we create with CollectionProperty. Maybe they are entirely different data structures, but I think there must be a way to manually add similarly functionality to our own properties. For example,


bpy.types.Object.my_coll = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)

creates a simple collection property (PropertyGrpup directly used here for simplicity, but subclassing it makes no difference). It lacks “active” subproperty (for example I want to keep track of active item in the collection), a “new” method, etc. Of course I can use following workarounds for them:

  • Use other integer property to keep track of active item
bpy.types.Object.my_coll_active = bpy.types.IntProperty()

We could use it with template_list, for example. But my_coll.active would be much more intuitive than having 2 different properties.

  • Use add() method to add new items instead of new(). But add() lacks arguments that we need to explicitly set which becomes cumbersome if the item type has update() callback set

class MyItemType(bpy.types.PropertyGroup):
    prop1 = bpy.props.StringProperty(update=prop12_combined_update)
    prop2 = bpy.props.IntProperty(update=prop12_combined_update)

bpy.types.Object.my_items_coll = bpy.props.CollectionProperty(type=MyItemType)

…and later…


newitem = bpy.context.object.my_items_coll.add()
newitem.prop1 = "blah blah"
newitem.prop2 = 42

So here, the prop12_combined_update() is called twice, which is bad if prop12_combined_update() is time-consuming. I think with new() it could be prevented (callback isn’t called, but we could manually call it later)

So my question is: Is there a way to add these functionality (and maybe other that are provided by built-in collections) from Python script?
(FYKI I’ve seen this example by Campbell, but that did not work, and I don’t even know if that is related to what I am looking for)

Looking forward for quick reply on my first post! :eyebrowlift:

I believe all that already works. I use that same style of property management in many of my scripts/AddOns.

It really is such a low overhead to create an Empty and add your collection to it. The collection has to be stored somewhere when the user issues a File/Save. It might as well be stored with an object that the end user can locate and delete, if neccessary.

Here is a code section from my RE:Lay AddOn (which is available, in full, by visiting the link in my signature).


############################################################################
# Parameter Definitions
############################################################################
def updateRELayParameter(self,context):
    # This def gets called when one of the tagged properties changes state.
    global isBusy
        
    if isBusy == False:
        if context != None:
            passedScene = context.scene
            cf = passedScene.frame_current
            print("")
            print("updateRELayParameter on frame #" + str(cf))
            reviewRELay(passedScene)
            
class cls_RELay(bpy.types.PropertyGroup):
    # The properties for this class which is referenced as an 'entry' below.
    target_name = bpy.props.StringProperty(name="Target", description="Type the name of the object that will inherit this objects motion here.")
    apply_to_delta = bpy.props.BoolProperty(name="ApplyToDelta", description="When active, results are applied to the delta coordinates instead.", default=True, options={'ANIMATABLE'}, subtype='NONE', update=updateRELayParameter)
    offset = bpy.props.FloatProperty(name="Offset", description="Number of frames to delay transformation.", default=0.0, min=-1800.0, max=1800, step=3, precision=2, options={'ANIMATABLE'}, subtype='TIME', unit='TIME', update=updateRELayParameter)    
    stretch = bpy.props.FloatProperty(name="Stretch", description="Stretch time to slow it down.", default=1.0, min=0.001, max=10.0, step=3, precision=2, options={'ANIMATABLE'}, subtype='FACTOR', unit='NONE', update=updateRELayParameter)

    axis_types = [
                ("0","None","None"),
                ("1","X","X"),
                ("2","Y","Y"),
                ("3","Z","Z"),
                ("4","XY","XY"),
                ("5","YZ","YZ"),
                ("6","XZ","XZ"),
                ("7","XYZ","XYZ"),
                ("8","ZXY","ZXY"),
                ("9","XZY","XZY"),
                ]
    axis_loc = EnumProperty(name="AxisLoc", description="The axis that will be relayed to the target.", default="7", items=axis_types, update=updateRELayParameter)
    axis_rot = EnumProperty(name="AxisRot", description="The axis that will be relayed to the target.", default="7", items=axis_types, update=updateRELayParameter)
    axis_scale = EnumProperty(name="AxisScale", description="The axis that will be relayed to the target.", default="7", items=axis_types, update=updateRELayParameter)

bpy.utils.register_class(cls_RELay)

# Add these properties to every object in the entire Blender system (muha-haa!!)
bpy.types.Object.Relay_List_Index = bpy.props.IntProperty(min= 0,default= 0)
bpy.types.Object.Relay_List = bpy.props.CollectionProperty(type=cls_RELay)

As far as the update getting called multiple times, I really did not notice any performance hit. But if you know one update will be CPU intensive, you can create an alternate update just for that property and route all others to a general/pass-through update.

Then all we have to do to add a new collection is…


def relay_new_source(lock, passedSourceName, passedSleepTime):
    ob_source = fetchIfObject(passedSourceName)
    if ob_source !=None:
            ob_source.show_name = True
            ob_source.hide_render = True  
                                                
            # Populate the new entry in the collection list.
            collection = ob_source.Relay_List
            collection.add() 
            l = len(collection)
            collection[-1].name= (str(l)+ ENTRY_NAME)

And in the draw() routine you can display such properties by fetching an entry from the collection. If the user changes them, the update() will fire.



                                if ob.Relay_List:
                                    # Display self created properties.
                                    try:
                                        entry = ob.Relay_List[ob.Relay_List_Index]
                                    except:
                                        entry = None
                                    if entry != None:
                                        box1.prop(entry, "name", text = "Name")
                                        box = layout.box()  #Separator 
                                        box1 = layout.box() 
                                        row1 = box1.row()
                                        row1.label(" Target:", icon='CURSOR')
                                        
                                        # Let's let the icon offer a little feedback.
                                        if fetchIfObject(entry.target_name) == None:
                                            box1.prop(entry, "target_name", icon='QUESTION')
                                        else:
                                            box1.prop(entry, "target_name", icon='OBJECT_DATAMODE')
        
                                        box1.prop(entry, "offset")
                                        box1.prop(entry, "stretch")
                                        layout.separator()

Also, by animating the Relay_List_Index you can cause sets of properties to change over time.

Thanks Atom for the quick reply! :slight_smile:

I believe all that already works. I use that same style of property management in many of my scripts/AddOns.

Yes, it works. But I was wondering if there is an alternative - semantically better - way of doing the same thing. That is, instead of


bpy.types.Object.Relay_List_Index = bpy.props.IntProperty(min= 0,default= 0)
bpy.types.Object.Relay_List = bpy.props.CollectionProperty(type=cls_RELay)

and later accessing them as

context.object.Relay_List[context.object.Relay_List_Index]

we could access them something like

context.object.Relay_List.active

. The collection has to be stored somewhere when the user issues a File/Save. It might as well be stored with an object that the end user can locate and delete, if neccessary.

Yes, I understand. Currently I’m also doing the same thing:

bpy.types.Object.my_items_coll = bpy.props.CollectionProperty(type=MyItemType)

so that it gets stored in all the “Object” objects. So here is my followup question: Is it possible to create our own classes such as bpy.types.MyClass and store my collection there? This is because, for example, I do not want my property to be available for a particular type of Blender object (Object, Armature, Empty, or any other Blender object). Maybe I want it only when the name of that Blender object starts with “my_” (may it be any Blender object), or, for example, it is parent of 2 or more objects.
PS: Sorry I’m talking more about semantics in the coding level which may not be visible to the end-user, but I’m feeling frustrated for a bulky script (module) which do not require much UI, and thinking of script clean-up. I’ve searched much for a way to add custom functionality to CollectionProperty (such as new(), active, etc), but haven’t found it yet. :frowning: I am ready to do any sorts of inheritance and meta-programming in Python script, but don’t want to rewrite everything in C.

The problem I see with…


context.object.Relay_List.active

Is that you are accessing the context which does not exist when rendering. The context is always None when rendering. Even if you could get it to work, come render time your script would fail. Or you would be forced to deal with that issue in some way or another.

You can, of course, ignore objects by type. I filter my objects by a name prefix as well.

so that it gets stored in all the “Object” objects.

Only if that object has been populated with data. Sure the definition exists for all objects, but the collections remain empty until you populate them. In my RE:ticular script, an alternate display algorithm for the standard particle system, I used the above technique to manage hundreds and even thousands of objects. Each new object my script created and managed inherited the list and index, but I did not notice a significant performance hit or file size bloat.

Custom properties, on a per-object basis, do exists but I am not sure if you can store a collection of classes within them?

Ah, I see. So what I can understand is, we cannot add our own custom datablock from within Python. But then, we can’t also use UILayout.template_ID, for example. (Because our active item is not structured as coll.active_item). I think I’ll have to stick with the current approach (having a separate property to keep track of active item in the collection and rerouting my update callback).

Sounds to me like combination of Collection Property and Property Group:
http://www.blender.org/documentation/blender_python_api_2_65_3/bpy.props.html#propertygroup-example

as we can only create Float, Int, Bool and String props, it is impossible to have an .active property, as it would have to be an ID prop (which isn’t available in python) :frowning:

Thanks CoDEmanX for clarifying this :slight_smile: Hoping for ability to do this in future Blender releases! :yes:

that would be indeed great, with current API you can’t keep track of objects (only by name via StringProperty, but if it’s changed then it loses the connection)