Property update function not working, help!

Hi!
I’m new to Python and somewhat intermediate in coding in general and I’ve been using Blender for years, on and off.
I’ve been trying to create an addon that would help me with creating and modifying solar systems for worldbuilding and art needs. At first, I tried to do it with geometry nodes and was somewhat successful, but had to do multiple things manually each time I added a new planet or a moon (mainly setting up and copying drivers). That system also moved the geometry of objects, but didn’t allow me to move the objects themselves with their origins, as far as I know - I really tried.
These were great limitations and I decided to tackle the problem with scripting. But I didn’t get very far before I encountered a big problem - any time I change a property, I have to manually press the update button to see the change in the orbit of the body. I need to see the visual change right away when changing the value with the slider in the UI…it is also needed for animating the whole thing.
In the Blender Python API, it says here that you can call a method any time you update the property, once initialized. But it just does not work. I’ve tried different methods, different properties, but nothing whatsoever is called when the change happens. I’m quite confused in general with this API scripting business, but this takes the cake. I couldn’t find an answer online (only supposedly working examples…), so I’m writing here. I’m probably missing something obvious.
Anyway, here’s my entire code (there’s problems, but I just really want to know how to do the update), I’ll appreciate any pointers. Thanks!

bl_info = {
    "name" : "Solar System Creator",
    "author" : "Ondrej Hrdina",
    "version" : (1, 0),
    "blender" : (3, 40, 0),
    "location" : "View3d > Tool",
    "warning" : "hi",
    "wiki_url" : "",
    "category" : "Add Mesh",
}

import bpy
import math

#list of all celestial bodies in the system
Bodies = []

#updates position of all bodies
def update_coordinates(self, context):
    time = bpy.context.scene.time
    for b in Bodies:
        e = b["eccentricity"]
        period = b["period"]
        major = b["semi_major_axis"]
        minor = OrbitalMechanics.minor(e, major, period)
        xy = OrbitalMechanics.xy(e,OrbitalMechanics.e_anomaly(e, period, time, 3), major, minor)
        b.location[0] = xy[0]
        b.location[1] = xy[1]

#button to call the update_coordinates method
class Update(bpy.types.Operator):
    bl_idname = "system.update"
    bl_label = "Update Coordinates"
    
    def execute(self, context):
        update_coordinates(self, context)
        return {'FINISHED'}
            
#button to add a new body to the list
class AddBodyDialogOperator(bpy.types.Operator):
    """Add a celestial empty"""
    bl_idname = "system.add_body_dialog"
    bl_label = "Add Body"
    
    #prompt properties from user
    name : bpy.props.StringProperty(name= "Enter name", default= "Arda")
    mass : bpy.props.FloatProperty(name= "Enter mass in Earth masses", default= 1, min= 0)
    semi_major_axis : bpy.props.FloatProperty(name ="Enter semi-major axis from parent in AU", default= 1, min= 0)
    eccentricity : bpy.props.FloatProperty(name ="Enter eccentricity from parent in AU", default= 0, min= 0, max= 1)
    period : bpy.props.FloatProperty(name ="Period in Earth years", default= 1, min= 0)

    def execute(self, context):
        #add an empty cube
        bpy.ops.object.empty_add(type='SPHERE', align='WORLD')
        body = bpy.context.object
        
        #assign properties
        body.name = self.name
        body['mass'] = self.mass
        body['semi_major_axis'] = self.semi_major_axis
        body['eccentricity'] = self.eccentricity
        body['period'] = self.period
        
        #add the body to the list
        Bodies.append(body)
        return {'FINISHED'}
    
    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)

class OrbitalMechanics():
    
    #method to calculate the x and y coordinates from the eccentric anomaly and the major and minor semi axes
    def xy(e, e_anomaly, major, minor):
        x = (math.cos(e_anomaly) - e)*major
        y = math.sin(e_anomaly)*minor
        
        return [x,y]
    
    #method to iteratively approximate eccentric anomaly from eccentricity and period 
    def e_anomaly(e, period, time, iterations): 
        time = bpy.context.scene.time
        mean_motion = 2*math.pi/period
        mean_anomaly = time*mean_motion
        e_anomaly = mean_anomaly
        i = 0
        while i < iterations:
            e_anomaly = e_anomaly - (((e_anomaly - math.sin(e_anomaly)*e) - mean_anomaly)/(1 - math.cos(e_anomaly)*e))
            i += 1
        return e_anomaly
    
    #method to calculate the semi-minor axis from the semi-major axis and eccentricity
    def minor(e, major, period):
        minor = major*math.sqrt(1 - math.pow(e,2))
        return minor

class MainPanel(bpy.types.Panel):
    bl_label = "Solar System"
    bl_idname = "MainPanel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Solar System'
    
    def draw(self, context):
        layout = self.layout
        layout.operator(AddBodyDialogOperator.bl_idname)
        layout.operator(Update.bl_idname)
        
        row = layout.row()
        layout.prop(bpy.context.scene, '["time"]')
        
#panel to show current body's properties
class BodyDetailPanel(bpy.types.Panel):
    bl_label = "Body Detail"
    bl_idname = "BodyDetailPanel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Solar System'
    bl_parent_id = 'MainPanel'
    
    def draw(self, context):
        if bpy.context.object != None:
            layout = self.layout
                
            layout.prop(bpy.context.object, '["mass"]')
            layout.prop(bpy.context.object, '["semi_major_axis"]')
            layout.prop(bpy.context.object, '["eccentricity"]',slider= True)
            bpy.context.object.id_properties_ui("eccentricity").update(min=0,max=1)
            layout.prop(bpy.context.object, '["period"]')

#scene time to direct all motion
bpy.types.Scene.time = bpy.props.FloatProperty(default= 0, update= update_coordinates)
time = bpy.context.scene.time
bpy.context.scene.time = bpy.context.scene.time

classes = (Update, AddBodyDialogOperator, MainPanel, BodyDetailPanel)
        
def register():
    for cls in classes:
        bpy.utils.register_class(cls)
        
def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)
    
if __name__== "__main__":
    register()
    

You need to use depsgraph_update to auto update:
https://docs.blender.org/api/current/bpy.app.handlers.html

1 Like

Thank you, you’re a lifesaver. I added this line to my code:

bpy.app.handlers.depsgraph_update_pre.append(update_coordinates)

I don’t exactly get what it does or why use this one instead of anything else, but it did solve my issue.

1 Like