4
$\begingroup$

I'm displaying N-instances of a Collection Property (depending on the amount of selected objects) — the values are floats, default values are 100/N, and I want to be able to recalculate values of other properties when one of the instance is changed so that a sum would be always 100 — basically I want to have something like influence between objects limited by 100(%).

Here's how it looks: in this example two other fields should be 25 and 25

enter image description here

I tried to use the update function and change "other values" (instances that aren't the active) but I guess since every instance uses the same function Blender goes in infinite recursion and crashes. Maybe my approach is wrong and I should do this in a different way?

import bpy
import math

def update_float(self,context):
    other_props = [prop for prop in context.scene.my_custom_set if prop.name != self.name]
    rest = 100 - self.value
    for prop in other_props:
        prop.value = rest / len(context.selected_objects) # crashes blender
    
class SceneSettingItem(bpy.types.PropertyGroup):
    name: bpy.props.StringProperty(name="Obj Name", default="")
    value: bpy.props.FloatProperty(name="Z coord", default=0, max=100, min=0, update=update_float)

class SimpleOperator2(bpy.types.Operator):
    """"""
    bl_idname = "object.simple_operator_sk"
    bl_label = "Collection Property Test 2"
    bl_options = {'REGISTER', 'UNDO'}

    def draw(self, context):
        layout = self.layout

        for my_item in context.scene.my_custom_set:
            row = layout.row()
            row.prop(my_item, "value", text=f"Prop for {my_item.name}")

    def execute(self, context):
        return {'FINISHED'}

    def invoke(self, context, event):
        context.scene.my_custom_set.clear()
        for ob in context.selected_objects:
            my_item = context.scene.my_custom_set.add()
            my_item.name = ob.name
            my_item.value = math.ceil(
                10000 / len(context.selected_objects))/100

        return context.window_manager.invoke_props_dialog(self)


def register(): 
    bpy.utils.register_class(SimpleOperator2)
    bpy.utils.register_class(SceneSettingItem)
    bpy.types.Scene.my_custom_set = bpy.props.CollectionProperty(type=SceneSettingItem)

def unregister():
    del bpy.types.Scene.my_custom_set
    bpy.utils.unregister_class(SimpleOperator2)
    bpy.utils.unregister_class(SceneSettingItem)

if __name__ == "__main__":
    register()

$\endgroup$
6
$\begingroup$

Set the custom property.

Internal "get/set" function of property?

Why does an update function loop when string property is reset?

When a property defined via bpy.props is set to non default, it is saved on the object as a custom property, eg setting ob.foo = 33 is equivalent of ob["foo"] = 33. Setting the property data does not fire the update,

... hence can set the others by custom property.

Have removed name since it is added to a collection item by default

In the method self is the instance of the collection item, can test against this rather than name. self.id_data is the scene, since this is the ID object the collection is defined on.

import bpy
import math

def update_float(self,  context):
    other_props = [prop for prop in self.id_data.my_custom_set if prop != self]
    v = (100 - self.value) / len(other_props)
    for p in other_props:
        p["value"] = v

    
class SceneSettingItem(bpy.types.PropertyGroup):
    value: bpy.props.FloatProperty(name="Z coord", default=0, max=100, min=0, update=update_float)

class SimpleOperator2(bpy.types.Operator):
    """"""
    bl_idname = "object.simple_operator_sk"
    bl_label = "Collection Property Test 2"
    bl_options = {'REGISTER', 'UNDO'}

    def draw(self, context):
        layout = self.layout

        for my_item in context.scene.my_custom_set:
            row = layout.row()
            row.prop(my_item, "value", text=f"Prop for {my_item.name}")

    def execute(self, context):
        return {'FINISHED'}

    def invoke(self, context, event):
        context.scene.my_custom_set.clear()
        for ob in context.selected_objects:
            my_item = context.scene.my_custom_set.add()
            my_item.name = ob.name
            my_item.value = math.ceil(
                10000 / len(context.selected_objects))/100

        return context.window_manager.invoke_props_dialog(self)


def register(): 
    bpy.utils.register_class(SimpleOperator2)
    bpy.utils.register_class(SceneSettingItem)
    bpy.types.Scene.my_custom_set = bpy.props.CollectionProperty(type=SceneSettingItem)

def unregister():
    del bpy.types.Scene.my_custom_set
    bpy.utils.unregister_class(SimpleOperator2)
    bpy.utils.unregister_class(SceneSettingItem)

if __name__ == "__main__":
    register()
    bpy.ops.object.simple_operator_sk('INVOKE_DEFAULT')
    

PS. Will be issues with number of selected objects being one or less, resolve by polling panel and or operator as true only when 2 or more objects are selected.

Poll method for custom panel

$\endgroup$
5
  • 2
    $\begingroup$ To be knowledgeable AND a great teacher is a rare skill :) $\endgroup$
    – Gorgious
    Jun 4 at 15:51
  • 1
    $\begingroup$ Thankyou, appreciated. Added comment to your answer, re this being the long winded version & speculated piccies get the bickies. Please don't feel obliged to delete on my account, ... and ditto back to you :} $\endgroup$
    – batFINGER
    Jun 5 at 7:02
  • $\begingroup$ As always, thank you, great answer. I'm having an issue that maybe should be for a different question: after I press Ok the values in the Adjust Last Operation window reset to default and don't change anymore: is there a trick to get those working..? $\endgroup$ Jun 6 at 6:27
  • 1
    $\begingroup$ Wondered about that never did get around to testing. Added the link re getter setter. In general the properties seen in modal popup are operator props, not scene or object. Would consider doing it differently than a modal popup, big fan of panel popovers in 2.8+. For the most part whatever is required in the execute method of operator could be done via the update method.(or getter setter) The redo last operation is effectively replaced by the panel of scene properties (consider instead using window manager).. The UI can test if the selection has changed and reoffer the populate button. $\endgroup$
    – batFINGER
    Jun 6 at 7:13
  • 1
    $\begingroup$ ** to put any ID object property in a modal popup (or alter via UI for that matter) with UNDO / REDO will likely act similar. The scene's state on undo stack. The operator added the collection to scene. Example of update button blender.stackexchange.com/questions/141179/… Please elaborate more on detail of usage? and how or why a modal popup is required? $\endgroup$
    – batFINGER
    Jun 6 at 7:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.