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.
self.do_not_update
supposed to work. Maybe make it a bool on the propertygroup? and setob.my_tool..do_not_update
Suggest contemplating making this an operator instead of an enum property update method. $\endgroup$self.do_not_update
disables the calling of the EnumPropertys update function. This means it preventsupdate_and_move
to run whenever any objectsmy_tool.obj_category
is changed. Without this, theupdate_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$do_not_update
or is it simply not wired in in question code? sinceself.do_not_update
will throw an unknown property error if not set first.self
in the loop is always the $\endgroup$