2
$\begingroup$

Summary and shortened code

Hi,
I'm trying to set up a function which let's me change an EnumProperty value of all selected objects, to that of the active objects same EnumProperty value. Thereafter, the script should execute a function for all selected objects based on their value from the EnumProperty I am however encountering an issue were the enumerator list seems to break in the loop that executes the function based on the shared enumerator property value, generating this error message in the python command console:

"WARN (bpy.rna): c:\b\win64_cmake_vs2017\win64_cmake_vs2017\blender.git\source\blender\python\intern\bpy_rna.c:1476 pyrna_enum_to_py: current value '1' matches no enum in 'ADDON_NAME_PG_Object_Settings', '', 'obj_category'"

The EnumProperty is set up as follows:

def category_enum_callback(scene, context):
    items = [('NONE', "Not set", "")]

    ob = context.object
    #    if is_hair == True:
    if ob is not None:
        if ob.type == 'MESH' or ob.type == 'CURVE':
            items.append(('BIG', "Big", ""))
            items.append(('MED', "Medium", ""))
            items.append(('SML', "Small", ""))

    return items

class ADDON_NAME_PG_Object_Settings(bpy.types.PropertyGroup):
    bl_idname = "PG_MySettings"
    bl_label = "My Settings"

    # apply values to obj_category
    obj_category: bpy.props.EnumProperty(
        name="obj_category",
        description="Apply category to haircard",
        items=category_enum_callback,
        update= update_and_move
        )

Simplified, the function for changing the EnuProperty value of all objects is written as follows:

def update_and_move(self, context):
    C = context.selected_objects
    ref_object = context.object
    coll_match = False
    ref_category: bpy.props.StringProperty()
    ref_category = ref_object.my_tool.obj_category

    for ob in C:
        self.do_not_update=True      
        if ob.my_tool.obj_category != ref_category:
            print("Before: ob category = ", ob.my_tool.obj_category)
            ob.my_tool.obj_category = ref_category
            print("After: ob category = ", ob.my_tool.obj_category)              
        self.do_not_update=False 

The code is then immediately followed by a for ob in C: for loop. Going through all selected objects and moving them to collections of the same name OR creating new collections if there is no collection with a matching name to the objects category.

Usage and testing

The update_and_move function is called either by changing an objects obj_category Property value from the addons UI panel, or when the function is triggered within an operator.

The script works just fine as long as only one object enters the function, or the additional objects entering the update_and_move function already have the same obj_category as the active object. if the category change part of the script is entered, all objects do get updated property values. However, when they afterward enter the following for loop, the EnumProperty lists on all "changed" objects are emptied. Which later produced the aforementioned rna warning.

From what I understand this error could be caused by a known EnumProperty callback bug that is mentioned in the Blender API: https://docs.blender.org/api/master/bpy.props.html#bpy.props.EnumProperty

"There is a known bug with using a callback, Python must keep a reference to the strings returned or Blender will misbehave or even crash." - Blender API

I've found references to this problem in other threads, but with answers that I don't fully understand how to implement. See: https://devtalk.blender.org/t/enumproperty-and-string-encoding/7835

Finally, using pdb, the python debugger. I've noticed that all object categories running into the ob.my_tool.obj_category = ref_category comes out with the exact same id pointer for their property data value. I have also run into this when trying the line bpy.data.objects['object.1'].my_tool.obj_category = bpy.data.objects['object.2'].my_tool.obj_category. Is this an improper way of transferring enumproperty values?

Full Demo Code (Runnable in Blender 2.8)

In the full demo code below the update_and_move function can be tested by in the "object category demo panel").
Plug'n'play Full Demo Code (Runnable in Blender 2.8):

import bpy
from bpy.props import PointerProperty

#     -----     Update Function/Script Function    -----
def update_and_move(self, context):
    # Set original object to ref_object, 
    # its obj_category to ref category
    # & default match type to False

    C = context.selected_objects
    ref_object = context.object
    coll_match = False
    ref_category: bpy.props.StringProperty()
    ref_category = ref_object.my_tool.obj_category
    print("NEW CATEGORY IS:       ", ref_category)

    # Loop Through all selected objects:
    #   if an objects obj_category is not already a match with ref_category,
    #       set their obj_category to ref_category
    print('_____________________________________________________________')
    for ob in C:
        self.do_not_update=True

        if ob.my_tool.obj_category != ref_category:
            print("Before: ob category = ", ob.my_tool.obj_category)
            ob.my_tool.obj_category = ref_category
            print("After: ob category = ", ob.my_tool.obj_category)

        self.do_not_update=False 

    #((THE FUNCTION))
    # Loop Through all selected objects:
    #   For each object, assuming an object has a category that is not none, loop through all collections:
    #       if an objects category matches the name of any collection:
    #           move the object to that collection
    #       If no match is found: 
    #           create a new collection with the same name as the objects category
    print('_____________________________________________________________')
    for ob in C:
        print("Selected object is:       ", ob.name)
        print("Selected object category is:       ", ob.my_tool.obj_category)                  

        #Loop through all collections to find a coll with a matching name to ob.
        for coll in bpy.data.collections:

            if ob.my_tool.obj_category == 'NONE':                           
                print('object ', ob.name, ' has no category: break loop') 
                break                                                     
            else:                                                          
                print("Current Collection is:       ", coll.name)

                if ob.my_tool.obj_category == coll.name:
                    print('ob category is: ', ob.my_tool.obj_category, ' & coll.name is ', coll.name, ". It is a match!")
                    bpy.data.collections[ob.users_collection[0].name].objects.unlink(ob)
                    bpy.data.collections[coll.name].objects.link(ob)
                    coll_match = True
                    print('_____________________________________________________________')
                    break                          
                if not coll_match:
                    print('ob category is: ', ob.my_tool.obj_category, ' & coll.name is ', coll.name,". No match")
                    print('_____________________________________________________________')


        #If no match is found, create new collection with obj_category name        
        else:            
            print('No match in loop, creating new collection "', ob.my_tool.obj_category, '"')      
            view_layer = bpy.context.view_layer

            newcoll = bpy.data.collections.new(ob.my_tool.obj_category)
            bpy.context.scene.collection.children.link(newcoll)

            layer_collection = view_layer.layer_collection.children[newcoll.name]
            view_layer.active_layer_collection = layer_collection

            # Unlink/ objects from previous collection and link to the new collection                
            bpy.data.collections[ob.users_collection[0].name].objects.unlink(ob)
            bpy.data.collections[newcoll.name].objects.link(ob)
            print('_____________________________________________________________')

    #Make the original active object the selected object
    bpy.context.view_layer.objects.active = ref_object
    ref_object.select_set(state=True)


#     -----     Callback    -----
def category_enum_callback(scene, context):
    items = [('NONE', "Not set", "")]

    ob = context.object
    #    if is_hair == True:
    if ob is not None:
        if ob.type == 'MESH' or ob.type == 'CURVE':
            items.append(('BIG', "Big", ""))
            items.append(('MED', "Medium", ""))
            items.append(('SML', "Small", ""))

    return items

#     -----     PROPERTY GROUP    -----
class ADDON_NAME_PG_Object_Settings(bpy.types.PropertyGroup):
    bl_idname = "PG_MySettings"
    bl_label = "My Settings"

    # apply values to obj_category
    obj_category: bpy.props.EnumProperty(
        name="obj_category",
        description="Apply category to haircard",
        items=category_enum_callback,
        update= update_and_move
        )

#     -----     PROPERTY GROUP    -----
class ADDON_NAME_PT_My_Panel(bpy.types.Panel):
    bl_idname = "ADDON_NAME_PT_my_panel"
    bl_label = "object category demo panel"
    bl_category = "object category demo panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    @classmethod
    def poll(self,context):
        return context.object is not None

    def draw(self, context):
        layout = self.layout
        object = context.object
        mytool = object.my_tool

        ob = context.object            
        layout.row().label(text='Choose object Category') 
        layout.prop(mytool, 'obj_category', text="")

#     -----     REGISTER    -----
classes = (
ADDON_NAME_PG_Object_Settings,
ADDON_NAME_PT_My_Panel,
)

#register, unregister = bpy.utils.register_classes_factory(classes)
def register():
    from bpy.utils import register_class
    for c in classes:
        register_class(c)

    bpy.types.Object.my_tool = PointerProperty(type=ADDON_NAME_PG_Object_Settings)

def unregister():
    from bpy.utils import unregister_class
    for c in reversed(classes):
        unregister_class(c)


if __name__ == "__main__":
    register()

Finally, this is my first time posting on StackExchange as well as first time coding in both blender and using python alltoghether. if you have any input on my code (structure, solution), or manner of posting a question. Feel free to chime in and tell me.

$\endgroup$
10
  • $\begingroup$ How is self.do_not_update supposed to work. Maybe make it a bool on the propertygroup? and set ob.my_tool..do_not_update Suggest contemplating making this an operator instead of an enum property update method. $\endgroup$ – batFINGER Mar 14 '20 at 13:43
  • $\begingroup$ @batFINGER self.do_not_update disables the calling of the EnumPropertys update function. This means it prevents update_and_move to run whenever any objects my_tool.obj_category is changed. Without this, the update_and_move function would be called infinitely within the first for loop of its own function, which serves to change the other selected objects category to that of the active object (ref_category). I believe this part of the script is fine. What seems to be the problem is the retrieving/transferring of the EnumProperties items within the item callback. $\endgroup$ – kaktjuven Mar 15 '20 at 2:11
  • $\begingroup$ (Part.2) What I understand is that EnumProperty item lists shouldn't be retrieved from within the EnumPropertys callback each time, but should be stored "elsewhere", something I just don't really know how to do... I'm unsure if an operator version would really avoid the error with value-transfer of EnumP:s. The full version of my script enables the user to add their own categories, making EnumProperties the only solution I can think of. Thank you for taking the time to look at the problem. I'll keep going at it! $\endgroup$ – kaktjuven Mar 15 '20 at 2:12
  • 1
    $\begingroup$ An example is merge vertices operator blender.stackexchange.com/questions/159300/… or as a menu in UI blender.stackexchange.com/a/102157/15543 IMO this gives the functionality desired. Updated example here blender.stackexchange.com/a/122595/15543 Has both a dynamic enum. Selecting is equiv of update. Any docs on do_not_update or is it simply not wired in in question code? since self.do_not_update will throw an unknown property error if not set first. self in the loop is always the $\endgroup$ – batFINGER Mar 15 '20 at 4:07
  • $\begingroup$ prop group of context object. $\endgroup$ – batFINGER Mar 15 '20 at 4:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.