how to set object properties in real-time from custom panel

Hi,

A script I wrote back in 2.55 is now broken in 2.57b and I’m stumped on how to proceed. The script set a custom panel and in the draw() method tries to set some properties of various objects when a slider is changed

e.g.

def draw(self, context):
     pose_bones = context.active_object.pose.bones
     pose_bones['Shoulder.L'].constraints["Copy Rotation"].influence = context.scene.ik_arm_l

Now with 2.57b I get the error:

“AttributeError: Writing to ID classes in this context is not allowed: avatar, Object datablock, error setting CopyRotationConstraint.influence”

Any ideas on what is wrong or how you are supposed to manipulate object properties from custom panel (there doesn’t appear to be an event mechanism so I’m implementing changes to sliders etc in the draw() method)

Here is a full example, it creates an integer property in the scene and defines a panel with a slider that can modify the property. All this is fine. What I’m after is to connect a change in the slider with a real-time update of several scene and object parameters. It used to be that you could do this in the draw() method, but now this returns an the error “Writing to ID classes in this context is not allowed”:

import bpy
from bpy.props import *

bpy.types.Scene.FS = IntProperty(name = "Frame Start", min=0, max=10)

class UIPanel(bpy.types.Panel):
    bl_label = "Property test"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
 
    def draw(self, context):
        layout = self.layout
        scn = context.scene
        layout.prop(scn, 'FS', slider=True)
        print(scn['FS'])
        
        context.scene.frame_start=scn['FS']
        
bpy.utils.register_class(UIPanel)

I guess executing code in the draw method was a hack but there doesn’t seem to be any other good way to do this. One alternative is to create a button that makes the changes but this would make an awful gui - move some sliders, click button to see effect, move more, click again … ugh, not exactly real-time.

Do we really have to resort to doing our own event loop in another thread? i.e launch a thread on initialisation with an infinite loop checking the values of the properties, if it detects a change then it acts to implement those changes.

If you are starting in 3D view, as most users do, you can switch modes during execution and restore without any obvious change to user perspective.

For example, I often toggle (switch to) edit mode, make changes to a mesh, and then (toggle back) return to 3D mode.

You could explicitly set modes, not sure you can capture original mode and restore, that’s why I use toggle.

Example:


            bpy.ops.object.editmode_toggle()
            bpy.ops.mesh.spin(steps=(self.base_faces*4), dupli=False, degrees=360, center=(0,0,0), axis=(0,0,1))
            bpy.ops.mesh.select_all(action='SELECT')
            bpy.ops.object.editmode_toggle()

Sorry - my mistake, thought this was an inline tool operation, not a toolbar “where am I now” situation. From what I understand you will have to know the “context” and perhaps that can be “assumed” if your option/action is present in a particular tool window.

I may be Over-my-head; but, using the toolbar typically updates when modified. If you are using a different “window”, again, that’s over-my head.

wrong window? :stuck_out_tongue: or have I completely missed your answer?

It was more I completely missed you question.

Wrong window - meaning “state”, toggle changes state for “reference”. You can specify reference; which view, mode.


        scene = context.scene

When you create an object - or reference it (sorry for all those that know this) - you can determine “window” or change it. ergo toggle…

Sorry for ramble, but seemed that “check your stats” works in this case.

It is already interactive, if you set it up correctly.

You are trying to map your custom property to frame_start. There is no need to do that. Simply use frame_start to begin with.


import bpy
from bpy.props import *

bpy.types.Scene.FS = IntProperty(name = "Frame Start", min=0, max=10)

class UIPanel(bpy.types.Panel):
    bl_label = "Property test"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
 
    def draw(self, context):
        layout = self.layout
        scn = context.scene
        layout.prop(scn, 'frame_start', slider=True)
        #print(scn['FS'])
        
        #context.scene.frame_start=scn['FS']
        
bpy.utils.register_class(UIPanel)

Now you can interactively change the frame_start without any more code.

no no no … I was just using frame_start as an example. I know I could manipulate it directly. Thanks for the reply though.

The actual program manipulates a whole lot of custom values which in turn manipulate a bunch of shape keys on multiple meshes, modifies the geometry of several bones, turns on and off IK controls and sets various animation parameters. Basically I need to execute arbitrary code in response to slider changes. I’m surprised this is so difficult.

Establishing a separate thread to look for changes seems plausible if clumsy, but is causing other headaches at the moment (scene not substantiated, threads that don’t die and so on).

There’s been hints in some posts that an event mechanism is on the TODO list, any idea if this is true and what the time frame is?

I have exactly the same question. All i want to do is to execute some code in case a property is changed like e.g. the change of the location.x value on a panel moves the object in the 3D-View. I want to have the same behaviour with my own properties.

hmmm. interesting topic :slight_smile:
the documented IntProperty doesnt show any UI either…

heh! was just looking through your scripting examples for inspiration :stuck_out_tongue:

sadly none of this cool stuff you are trying to do here is covered there yet :frowning:

It looks like layout can not display any of these dynamic properties unless you modify.

…/.blender/scripts/modules/bpy_types.py
to include your new property. (Which seems very hackish and forces you to update bpy.types.py every time you pull down a new build of Blender).

Even if you get it working, as the docs mention, there is not way to save the data with your BLEND file.

The only event generator I have been able to get to work is the PLAY button. The play button will generate an event that will trigger your code. It really is not that bad to just click the play button and let it run. That is how all my frameChange event code work.

I also wrote a threading “poll” system, as you mention in your second post. That approach will work as well and not require the play button to constantly run, but you must take care in polling values from BPY or you can throw exceptions.

Feel free to click on the link in my signature. It leads to AddOns that demonstrate both approaches to adding interactivity to a BLENDER scene. Event change and threaded polling.

Here is something that works but is an overkill - implement a separate thread to respond to the UI. This means there will be a separate thread eating CPU cycles just waiting for the user to change the the sliders. Again the code snippet does something trivial just to test it.

import threading, time
import bpy
from bpy.props import *

def do_something(scene):
    # just a trivial example to see its working
    print(scene.FS)
    scene.frame_start=scene.FS

class respond_to_ui(threading.Thread):
    
    def __init__(self, scene):
        threading.Thread.__init__(self)
        self.scene = scene
    
    def run(self):
        # in reality this would be an infinite loop. Do 10s for testing
        for ii in range(100):
            time.sleep(0.1)
            # only do something if there is a change
            if self.scene.FS!=self.scene.FS_:
                do_something(self.scene)
                self.scene.FS_=self.scene.FS
                

bpy.types.Scene.FS = IntProperty(name = "Frame Start", min=0, max=10)
bpy.types.Scene.FS_ = IntProperty()

class UIPanel(bpy.types.Panel):
    bl_label = "Realtime property test"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout
        layout.prop(context.scene, 'FS', slider=True)
        
    
if __name__ == "__main__" :    
    bpy.utils.register_class(UIPanel)
    
    # launch separate thread, need to pass it the scene or it wont see it
    r = respond_to_ui(bpy.context.scene)
    r.start()
   

The script will only run for around 10s since I’m only testing. Unfortunately, repeatedly restarting the script tends to segfault blender or give obscure “hardware errors”. If you wait a bit between testing it’s a bit more stable, maybe it takes a while for garbage collection to clean up old threads or something. A more serious implementation would require some basic thread management so multiple watcher threads aren’t started by accident.

@Atom: I haven’t looked at your code yet but I see what you mean about throwing exceptions! Is Blender not thread safe?

Saving property values would not be a problem as you could simply store the value in an ID property as part of the fset function.

The play or execute button seems to be the only official solution but it makes user-scripted panels secondhand citizens. I have a humanoid model and around 50 or so sliders that modify shape and mesh of my model in complicated ways that can’t just be set up as exposing an existing property in the UI. I also have some animation shortcuts like IK controls in the panel which affect multiple constraints, this is a nice way of doing things. Having to press a button after each slider change is very clumsy and does’t let you test things out (can’t hit esc to not commit to a change) so it makes modelling with the UI sliders almost unusable.

An alternative would be to implement yet-another-UI as 3D objects in the model itself. This seems to be what is done with IK switches for instance. Seems silly and inconsistent when we have the ability to customize the blender UI itself, and have a nice language like python to do it in.

@Atom: had a look at frame_change.py, nice. You have a more sophisticated use of threads but I don’t think it’s necessary for my application - I don’t need to know when the watcher thread is busy for instance.

How have you found the stability of blender when it’s running a separate thread like that?

A more refined threading example for the record. Hopefully obsolete soon, see next message.

import threading, time
import bpy
from bpy.props import *

def do_something():
    # just a trivial example to see its working
    global SCENE, ACTIVE_OBJECT
    
    # only do something if there is a change
    if SCENE.FS!=SCENE.FS_:
        SCENE.FS_= SCENE.FS
        SCENE.frame_start=SCENE.FS
        print(ACTIVE_OBJECT, SCENE.FS)

class uiwatcher(threading.Thread):
    
    def __init__(self):
        threading.Thread.__init__(self)
        self.daemon = True # so Blender can quit cleanly
        self.name='uiwatcher'
        self.active = True
    
    def run(self):
        while self.active:
            time.sleep(0.1)
            try:
                # call functions here
                do_something()
            except Exception as detail:
                print("UI watcher exception:", detail)
                

bpy.types.Scene.FS = IntProperty(name = "Frame Start", min=0, max=10)
bpy.types.Scene.FS_ = IntProperty() # store old prop to monitor for changes

class UIPanel(bpy.types.Panel):
    bl_label = "Realtime property"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        
        # make sure active_object is up to date
        global ACTIVE_OBJECT
        if ACTIVE_OBJECT!=context.active_object:
            ACTIVE_OBJECT=context.active_object
            
        layout = self.layout
        if 'uiwatcher' in [t.name for t in threading.enumerate() ]:
            state = 'alive'
        else:
            state = 'dead'
        layout.label("UI watcher is %s"%state)
        layout.prop(context.scene, 'FS', slider=True)

# bpy.context not available in separate thread 
# so store what we need in global variables
SCENE = bpy.context.scene
ACTIVE_OBJECT = bpy.context.active_object

if __name__ == "__main__" :    
    bpy.utils.register_class(UIPanel)
     
    # shut down any existing threads
    for t in threading.enumerate():
        if t.name == 'uiwatcher':
            t.active = False
    
    r = uiwatcher()
    r.start()

Woohoo! Just committed to blender CVS:

Revision: 37260
http://projects.blender.org/scm/viewvc.php?view=rev&root=bf-blender&revision=37260
Author: campbellbarton
Date: 2011-06-06 17:50:20 +0000 (Mon, 06 Jun 2011)
Log Message:

Support for update callbacks in python defined RNA properties as discussed last meeting.
This means script authors can perform actions using these callbacks rather then on drawing which puts blender in a readonly state.

Simple example:

import bpy
def up_func(self, context):
print(“test”)

bpy.types.Scene.testprop = bpy.props.FloatProperty(update=up_func)
bpy.context.scene.testprop = 11

prints -> test

see http://lists.blender.org/pipermail/bf-blender-cvs/2011-June/036463.html. Awesome.

This is huge, and precisely what I was looking for in my current script project. Thank you so much for pointing this commit out! I’ll have to test it out to see if it works for me. Heading over to builder.blender.org immediately!

Atom, take note of this, as it may make your frame change event unnecessary!

Alas, it does not seem to work as a frame change event. While it is true, that when you click on the custom property and change the value you do get mouse events routed to a def, the system does not seem to generate events if you simply animate the parameter and press play.

Feel free to take a look at my setup to see what I am doing wrong?

I am using trunk r37385.

Attachments

25_new_event_system.blend (62.7 KB)

No, Atom, I don’t think you’re missing anything. I did a test of my own today, and came to the same conclusion. The new callback functionality for property changes is very nice, but it doesn’t appear to solve all issues.

Do you have any information on whether anything is planned for introducing something like frame change event functionality? Have you ever mentioned the issue to Campbell? Or is it intentional that this functionality does not exist? I’m just thinking that, if Campbell has just recently committed this update, maybe he would be receptive to looking into the frame change issue now.

The whole thing is just so frustrating.

I’ve looked at your frame change script of course, but I’m sure we’d all like a much cleaner solution.