Add-On / bpy.props not changing in sync with object properties

Hello there,

I’m new on Add-On development on Blender and on my first steps I would like to mimic the Item transform Panel and simply act on Location. I made a piece of code, but it only works one way. While the Add-On panel update the object location, changing the object location in its properties doesn’t change the Add-On bpy.props.FloatVectorProperty.
I’m quite sure that is a basic and trivial error or misunderstanding but I would like to know if there is a simple way to manage a such simple stuff?

Here the simple code I wrote,

import bpy

bl_info = {
	"name": "Test Add-On",
	"blender": (4, 1, 1),
	"category": "Object",
}



# Update function
def update_o(self, context):
	if context.scene.o is None:
		return
	context.scene.o.location = context.scene.o_loc
	

# Create Panel into 3DView
class mOPanel(bpy.types.Panel):
	bl_label = "oTest"
	bl_idname = "OBJECT_PT_oTest"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'UI'
	bl_category = "object"
	bl_version = "0.0.1"

	def draw(self, context):
		layout = self.layout
		
		row = layout.row()
		row.prop(context.scene, "o", text='Object')
		if context.scene.o is not None:
			row = layout.row()
			row.prop(context.scene, "o_loc", )
		row = layout.separator(type='LINE')
		row = layout.label(text=f"v{self.bl_version}")



# Registering and Unregistering Add-On
def register():
	bpy.types.Scene.o = bpy.props.PointerProperty(type=bpy.types.Object)
	bpy.types.Scene.o_loc = bpy.props.FloatVectorProperty(name="Location", min=-10.0, max=10.0, step=0.5, default=(0.0 ,0.0 ,0.0), options={'ANIMATABLE'}, update=update_o)
	bpy.utils.register_class(mOPanel)


def unregister():
	del bpy.types.Scene.o
	del bpy.types.Scene.o_loc
	bpy.utils.unregister_class(mOPanel)


# Start Add-On
if __name__ == "__main__":
	register()

If anyone have an idea how to get that simple thinks working both way I take it! I did try get and set options on bpy.props.FloatVectorProperty but I couldn’t get that work either, maybe I missed something somewhere…
Thanks

Because your property is not connected to the location in any way. get and set callbacks is the way to do it. get will ensure your property to always show the value from the object’s location, set will ensure that new value set to your property updates object’s location.

1 Like

Thanks a lot @Andrej. I thought the good way was setter / getter but I haven’t succeed yesterday when I tried.
The API documentation on this is really not exhaustive. If you do know how to set this up I really appreciate a piece of code :grin:

https://docs.blender.org/api/current/bpy.props.html#getter-setter-example

I think you want to use the msgbus system as object location is a builtin property that AFAIK you can’t override.

https://docs.blender.org/api/current/bpy.msgbus.html#example-use

However be careful because it’s prone to cause an infinite loop of updates, object location updates the scene property which updates object location, …

@Andrej , Thanks, but I already read that example and I could get it work in the add-on context.
While o_loc exist in the context.scene Blender console returns me that the key doesn’t exist

import bpy

bl_info = {
	"name": "Test Add-On",
	"blender": (4, 1, 1),
	"category": "Object",
}

# Update functions
def get_o(self):
	return self["o_loc"]

def set_o(self,value):
	self["o_loc"] = value


# Create Panel into 3DView
class mOPanel(bpy.types.Panel):
	bl_label = "oTest"
	bl_idname = "OBJECT_PT_oTest"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'UI'
	bl_category = "object"
	bl_version = "0.0.1"

	def draw(self, context):
		layout = self.layout
		
		row = layout.row()
		row.prop(context.scene, "o", text='Object')
		if context.scene.o is not None:
			row = layout.row()
			# layout.use_property_split = True
			# layout.use_property_decorate = True

			row.prop(context.scene, "o_loc")

		row = layout.separator(type='LINE')
		row = layout.label(text=f"v{self.bl_version}")


# Registering and Unregistering Add-On
def register():
	bpy.types.Scene.o = bpy.props.PointerProperty(type=bpy.types.Object)
	bpy.types.Scene.o_loc = bpy.props.FloatVectorProperty(name="Location", min=-10.0, max=10.0, step=0.5, default=(0.0 ,0.0 ,0.0), options={'ANIMATABLE'}, get=get_o, set=set_o) #update=update_o, 
	bpy.utils.register_class(mOPanel)

def unregister():
	del bpy.types.Scene.o
	del bpy.types.Scene.o_loc
	bpy.utils.unregister_class(mOPanel)


# Start Add-On
if __name__ == "__main__":
	register()

@Gorgious , the problem on message bus is that it is not flag when you are moving the object in Viewport from what I read in the API

For the people who are also stuck on those kind of problem.

When defining a get/set callback for a property, the self argument in those callbacks refers to the owner of the property (in this case, the Scene), not the panel or a separate variable.
The getters and setters should directly access the currently referenced object (self.o) and return or modify its location.

Here the working code:

import bpy

bl_info = {
	"name": "Test Add-On",
	"blender": (4, 1, 1),
	"category": "Object",
}



# Getter and Setter functions for FloatVectorProperty
def get_o(self):
    obj = self.o
    if obj:
        return obj.location
    return (0.0, 0.0, 0.0)

def set_o(self, value):
    obj = self.o
    if obj:
        obj.location = value



# Create Panel into 3DView
class mOPanel(bpy.types.Panel):
	bl_label = "oTest"
	bl_idname = "OBJECT_PT_oTest"
	bl_space_type = 'VIEW_3D'
	bl_region_type = 'UI'
	bl_category = "object"
	bl_version = "0.0.1"

	def draw(self, context):
		layout = self.layout
		
		row = layout.row()
		row.prop(context.scene, "o", text='Object')

		obj = context.scene.o
		if obj is not None:
			row = layout.row()
			row.prop(context.scene, "o_loc")

		row = layout.separator(type='LINE')
		row = layout.label(text=f"v{self.bl_version}")

# Registering and Unregistering Add-On
def register():
	bpy.types.Scene.o = bpy.props.PointerProperty(type=bpy.types.Object)
	bpy.types.Scene.o_loc = bpy.props.FloatVectorProperty(
		name="Location", 
		min=-10.0, 
		max=10.0, 
		step=0.5, 
		default=(0.0 ,0.0 ,0.0), 
		options={'ANIMATABLE'}, 
		get=get_o, 
		set=set_o
		)
	bpy.utils.register_class(mOPanel)


def unregister():
	del bpy.types.Scene.o
	del bpy.types.Scene.o_loc
	bpy.utils.unregister_class(mOPanel)


# Start Add-On
if __name__ == "__main__":
	register()

I’d maybe add a clause like if if obj and obj.location != value: obj.location = value as reassigning the same value would cause a re-calculation of depsgraph when nothing should have changed. Just my 2 cents.

1 Like

A good one, thanks for having pointed this!

I believe, depsgraph is updated either way, even if set_o will be just a pass. But yes, it will help skipping msgbus updates.

import bpy
from mathutils import Vector


def get_o(self):
    obj = bpy.context.object
    if obj:
        return obj.location.x
    return 0.0


def set_o(self, value):
    obj = bpy.context.object
    if obj and obj.location.x != value:
        print("set")
        obj.location.x = value


class Panel_(bpy.types.Panel):
    bl_label = "oTest"
    bl_idname = "OBJECT_PT_oTest"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "object"
    bl_version = "0.0.1"

    def draw(self, context):
        self.layout.prop(context.scene, "o_loc")


def generate_callback(callback_str):
    def callback(*args, **kwargs):
        print(callback_str, args, kwargs)

    return callback


# Registering and Unregistering Add-On
def register():
    bpy.types.Scene.o_loc = bpy.props.FloatProperty(
        name="Location",
        default=0.0,
        get=get_o,
        set=set_o,
    )
    bpy.utils.register_class(Panel_)
    bpy.app.handlers.depsgraph_update_post.append(generate_callback("depsgraph_update_post"))
    bpy.app.handlers.depsgraph_update_pre.append(generate_callback("depsgraph_update_pre"))

    cube = bpy.data.objects["Cube"]
    bpy.msgbus.subscribe_rna(
        key=cube.path_resolve("location"), owner=None, args=(), notify=lambda *args: print("Cube's location changed!")
    )


if __name__ == "__main__":
    register()
1 Like