Proper way to create/remove objects in response to property changes

I have created an add-on:

This add-on needs to create/remove objects in some situations:

  • When the user enables/disables the “Rigid Body” checkbox.

  • When switching into or out of Edit mode.

  • When the user changes the “Type” enum.

Right now I simply make those changes inside the “update” callback for the properties, and everything works perfectly.

However, the Blender developers have said that I should not create or remove objects in the “update” callback for properties:

https://developer.blender.org/T81345#1028956

So how am I supposed to create/remove the objects when the user changes the “Rigid Body” or “Type” properties?

They suggested using a modal operator or app handler, but those are always running, which seems bad for performance since my code only needs to run when the property changes.

Definitely don’t use modals. Not only do they get called on every input and timer event, they also potentially consume events other modals might be listening to as well as prevent blender from auto-saving.

You could always just fake the drop-down box to point to operators instead of properties. The ui is flexible enough to emulate operators looking like properties. For eg. blender does this for the addon expand arrows in the addons tab of preferences.

Or just make a button and have the user click it after selecting from the drop-down list.

1 Like

Thanks for the tip, that gives me even more reason to avoid them.

That’s a quite bad user experience: the user would need to click that button whenever switching out of Edit mode, or when changing the settings. That’s quite tedious and poor design, so I would really prefer to avoid that, if possible.

I had thought about that, though it feels wrong. And there’s three problems with that:

  1. The CHECKBOX_DEHLT and CHECKBOX_HLT icons do not look the same as the regular checkbox.

  2. Using a menu with operators does not look or act the same as the enum popup. And it doesn’t respect the use_property_split layout either.

  3. How would that work with switching out of Edit mode? They only said that update callbacks are bad, but I assume that using bpy.msgbus.subscribe_rna is also bad.

Eh, it beats an unsafe alternative especially after the latest changes to undo. You could always hope for Bastien one day making changes to bmain safe outside main execution.

Does this discrepancy disturb you? There are other ways to show toggles. An operator optionally takes a depress argument.

Use operators and place them using layout.operator_menu_enum().

It’s not clear what exactly you’re expecting to happen when switching modes. Is it about deleting objects? Is deleting really the best way to go about?

Perhaps, though even after extensive testing I have not run into any problems: no errors, no crashes, no issues with undo/redo. Of course that might change in the future, but I think you’re underestimating how horrible a button would be for my add-on’s user experience.

Yes, because the checkbox is displayed right next to other checkboxes, so the discrepancy is very ugly and noticeable. In particular the “Armature -> Enable rigid bodies” checkbox.

It is not a minor difference, the checkboxes are completely different, including the positioning and color!

Blender Screenshot 001

If this was native Blender UI it would not be acceptable, why should it be acceptable in an add-on?

Yes, I know. Even when using operator_menu_enum, it does not look the same, it does not behave the same, and it does not work with use_property_split. I tried all the different operator and menu layouts, none of them work properly:

Blender Screenshot 002

(The first one is the prop, the second one is the operator_menu_enum)

Yes, it is mandatory for it to create/delete objects when switching modes, otherwise it breaks the functionality. There is no way around this.

This must be done when switching modes, because in Edit mode the user might create new bones, remove bones, change bone parents, or move bones. And when switching out of Edit mode, the add-on must update the rigid bodies based on those changes (which involves creating/removing rigid body objects, and aligning the rigid bodies to match the bones).


Not every add-on fits into a nice neat “click button to do thing” workflow. I find it very bizarre that Blender provides update callbacks, but you’re not allowed to actually do anything inside of them.

I suggested adding a new Python API that would run the callback during the main event loop (when it is safe to create/remove objects), but they don’t seem interested in that either, which is also very bizarre…

I keep getting the impression that the Blender developers aren’t interested in complex add-ons, which is strange because add-ons are one of the major benefits Blender has over other 3D software. Add-ons like HardOps and BoxCutter are absolutely incredible, we need more and better add-ons, which can’t happen with a crippled add-on system.

(This isn’t directed at you, I’m just venting. I’ve spent hundreds of hours creating this add-on which adds in a useful feature to Blender and saves users a huge amount of time… but then I get told essentially “you can’t do that, and no we won’t provide an alternative”. I don’t mind being told that my implementation is wrong, but my goals are not wrong, and I don’t like them being dismissed, especially since I pay money to the Blender Institute.)

2 Likes

No need to worry about UI so much, the addon is already well designed if there are a few flaws in the UI is no problem since it works.

You can use a toggle button instead.

import bpy

class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "scene.simple_operator"
    bl_label = "Simple Object Operator"
    bl_options = {"INTERNAL"} # this operator is hidden from user
    # (this is undocumented btw - just checked it)
    def execute(self, context):
        context.scene.is_on = not context.scene.is_on
        print('property changed to', context.scene.is_on)
        return {'FINISHED'}

class HelloWorldPanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Hello World Panel"
    bl_idname = "OBJECT_PT_hello"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"
    def draw(self, context):
        layout = self.layout
        # https://docs.blender.org/api/current/bpy.types.UILayout.html#bpy.types.UILayout.operator
        if not context.scene.is_on:
            layout.operator("scene.simple_operator", text="STOPPED")
        else:
            layout.operator("scene.simple_operator", text="RUNNING", depress=True)

def register():
    bpy.utils.register_class(HelloWorldPanel)
    bpy.types.Scene.is_on = bpy.props.BoolProperty()
    bpy.utils.register_class(SimpleOperator)

def unregister():
    bpy.utils.unregister_class(HelloWorldPanel)
    del bpy.types.Scene.is_on
    bpy.utils.unregister_class(SimpleOperator)

if __name__ == "__main__":
    try:unregister()
    except:pass
    register()

So I had to completely rewrite the addon, but I came up with this solution:

  1. All major changes (object creation/deletion) have been moved into a custom operator.

  2. When a property is changed, it uses the update callback to mark the armature as “dirty”. It also registers a timer which will run during the next main event loop.

  3. When the timer runs, it runs the custom operator on all of the dirty armatures.

This means that the update callbacks don’t do anything (other than marking the armature as dirty), and all of the major work is delayed until the next main event loop.

So the end result is that the user does NOT need to click a button (the changes happen automatically), and I also don’t need to use fake checkboxes or menus (which doesn’t work very well, as explained before).

This is completely acceptable to me, and according to the docs this should be a supported way of using timers:

https://docs.blender.org/api/current/bpy.app.timers.html#use-a-timer-to-react-to-events-in-another-thread

Glad you managed to solve your issue!

1 Like

Thanks for starting this discussion!

So I’ve read through your bug report.
I had posted a similar one in September, where Blender crashes, when you undo after appending an object in response to an enum property change.
This seems to be a fairly common and popular use case for addons bringing in external assets to the current scene/blend file utilizing layout.template_icon_view() as the interface for the enum.

As of 2.83 this is a major issue leading to a Blender crash, and we are not supposed to use operator calls in update callbacks.

Ok, so today I’ve submitted a follow-up report, where I have a put the object-append-logic into a function instead of an operator.
It still crashes blender after undoing however. Apparently because we aren’t supposed to do any database manipulation in response to property changes.
If we are doing database manipulation, we should do it in an operator.

Getting frustrated, I share your sentiment, that this is not well communicated by the devs, that it’s a flaw of the API, and that it’s absurd to not provide a good way to do this. “You shouldn’t do this” is just unacceptable.

I’ve tried your timer based approach, but it is still crashing after undoing, maybe because I’m appending, instead of creating/removing objects?
If you could take a look at my example, I’d very much appreciate your input!:
append_obj_undo_crash_from_update_callback_of_enum_prop.zip

In fact, I have a second much simpler example just with the timer, and without the enum, that also crashes:
append_obj_undo_crash_from_timer.zip
So I should probably report that separately:
edit: https://developer.blender.org/T82379

3 Likes

@MACHIN3 Since I’m a small time developer, I don’t think they will listen to me. But if a big developer like you talks about it, maybe they will listen. Their attitude toward addon developers is really absurd, and I think this will cause a lot of user backlash in the 2.91 release.

Their attitude is so bad that it’s making me feel like not creating addons anymore, and recommending people to not use Blender… I almost never encounter attitudes as bad as that (in other open source projects), and it’s even worse since I pay money to Blender.

I also get crashes even when using an operator (with bl_options = {'UNDO'}). I think it’s related to this: https://developer.blender.org/T77557

There is a patch which fixes it, but it hasn’t been reviewed since October 12th: https://developer.blender.org/D9166

1 Like

I’m not sure if this appropriate use of the API or a good workaround for all cases, but we prevent crashes like this by adding a call to bpy.ops.ed.undo_push() after linking an object.

Adding this in the append_obj_undo_crash_from_timer.zip example seems to prevent the crash when using undo:

def append_object_function(context):
    print("appending object")
    
    blendpath = os.path.join(os.path.dirname(bpy.data.filepath), 'obj.blend')
    objname = 'Cube'

    # append obj
    with bpy.data.libraries.load(blendpath, link=False, relative=False) as (data_from, data_to):
        getattr(data_to, 'objects').append(objname)
    obj = getattr(data_to, 'objects')[0]

    # link the obj to the current scene      
    context.scene.collection.objects.link(obj)
    bpy.ops.ed.undo_push()
1 Like

It’s funny you say this, because I was specifically told to do this in the past, ha.
But it’s also still crashing even if I remove it.
Also, if the op is called a timer it looks like the undo push isn’t actually happening.

Indeed it does! I’ve been trying these earlier too, just not with the timer. Thanks!

Probably not the best choice though:
probably_not_the_best_choice

P.S. Here is my code which marks the current armature as dirty. It registers a timer for the next tick, which will run the update operator on all the dirty armatures.

It’s designed so that it will have a maximum of 1 timer at once, and the timer will always run as soon as possible.

1 Like

Yep, I had taken a look already. Seems like a good approach!

At this point I’m just waiting to see https://developer.blender.org/T77557 get fixed. I don’t have high hopes of this happening any time soon given that this bug is confirmed but open since early June but hasn’t made it into any 2.83 or 2.90 release. So it probably won’t make it into 2.91 either.

This “nerfing” of update callbacks without a proper alternative is really disappointing. IMO it doesn’t really matter, that it “worked by accident” in the past. People have been using them to do more or less complex things for ever, because they are convenient and useful and were AFAICT super stable. May as well support them properly, or provide an alternative.

1 Like