1
$\begingroup$

I have created a panel in the Properties area in which the user can specify various parameters and then press a button to create the object. I am trying to write a Python code to allow the user to modify an object that was previously created using either the same panel or a new modify object panel. Two parts of this task are giving me trouble:

  1. Identifying the object that has been selected. The problem is that if I click on the screen so that no object is selected, when I do bpy.context.active_object, I still get the most recent object selected. Also, if I deselect all the objects using bpy.context.active_object.select_set(False), I still get the last object selected as the active object. If I can determine whether or not an object is selected, I can determine whether or not to edit an object or create a new object.
  2. If an object has been selected and is going to be modified, I would like the geometric properties of that object to appear in the panel. I can't retrieve the properties of a selected object in a draw method, so I am thinking that a modal operator may be the answer. I am including my code with a simple operator and a panel. After I create the operator, the modal operator will respond the next time I click on the object, but the code returns 'FINISHED', so it only detects selecting the object once and then, even if I modify the scene properties, they are displayed in the panel.

Here is my code:

import bpy
from bpy.props import FloatProperty

bpy.types.Scene.length = bpy.props.FloatProperty(name = "length",default = 2)
bpy.types.Scene.height = bpy.props.FloatProperty(name = "height",default = 1)
bpy.types.Scene.width = bpy.props.FloatProperty(name = "width",default = 1)

class DrawBlock(bpy.types.Operator):
    """A rectangular prism"""
    bl_idname = "myops.draw_block"
    bl_label = "Edit Block" 
    bl_description = "Draw a rectangular prism"
    bl_options = {'REGISTER', 'UNDO'}
    
    height: FloatProperty (name="height")
    length: FloatProperty (name="length")
    width: FloatProperty (name="width")

    def new_block(self,context):
        width = bpy.context.scene.width
        height = bpy.context.scene.height
        length = bpy.context.scene.length

        bpy.ops.mesh.primitive_cube_add()
        obj=bpy.context.active_object
        obj.dimensions=(width,length,height)
        obj.location.z=obj.location.z+height/2
        obj.name="Block"

    def execute(self,context):
        obj = bpy.context.active_object
        if obj:
            w = bpy.context.scene.width
            h = bpy.context.scene.height
            length = bpy.context.scene.length
            obj.dimensions=(w,length,h)
        else:
            self.new_block(context)
        return {'FINISHED'}

    def modal(self, context, event):

        obj=bpy.context.active_object
        if obj:
            print("object selected: ",obj)
            bpy.context.scene.width=obj.dimensions.x
            bpy.context.scene.length=obj.dimensions.y
            bpy.context.scene.height=obj.dimensions.z
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            print("cancelled operation")
            return {'CANCELLED'}

        else:
            return {'PASS_THROUGH'}

        return {'RUNNING_MODAL'}
            
    def invoke(self,context,event):
        print("inboke block")
        self.new_block(context)
        context.window_manager.modal_handler_add(self)
        return{'RUNNING_MODAL'}
    
class SCENE_PT_Build(bpy.types.Panel):
    """Creates a Panel in the scene context of the properties editor"""
    bl_label = "Build Menu"
    bl_idname = "SCENE_PT_build"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        obj = context.active_object
        row=layout.row()
        row.label(text="block geometry:")        
        row=layout.row()
        row.prop(scene, "length")
        row=layout.row()
        row.prop(scene, "width")
        row = layout.row()
        row.prop(scene, "height")
        row=layout.row()
        button_text="New Block"
        if obj:
            buttom_text="Edit Block"
        row.operator("myops.draw_block",text=button_text)

def register():
   bpy.utils.register_class(SCENE_PT_Build)
   bpy.utils.register_class(DrawBlock) 
   
def unregister():
   bpy.utils.unregister_class(DrawBlock)
   bpy.utils.unregister_class(SCENE_PT_Build) 
   
if __name__ == "__main__":
    register()

I am not sure why the line obj=context.object does not seem to get any objects. I think that a similar line in my real code does.

I am happy to learn about more effective ways of accomplishing this. I have tried using the adjust last operation menu, but I think I have too many parameters for that to be worthwhile.

$\endgroup$
5
  • $\begingroup$ I can try to look more later, but there is definitely a difference between "active" and "selected"...you want to get the selected objects, which is bpy.context.selected_objects. There are times when you don't want the active item to be selected...for instance if you want to subtract a vertex group from another, you set the 1st active, then select it, then set the 2nd active, and deselect it. If active was the same as selected, that would be impossible. Or if you wanted to select a collection and then deselect one object in the collection. $\endgroup$ Apr 29 at 21:33
  • $\begingroup$ Cool! That works. Thanks for the explanation. I would like to be able to put the parameters of the selected object in the properties listed in the panel. I possibly could use an enum property to select the object and use an update function, but selecting the object in the 3d view seems preferable. $\endgroup$
    – SJK
    Apr 30 at 2:11
  • 1
    $\begingroup$ if you want it to auto-update on selection, that's harder. Here's an answer that describes how to overload the built in selection function: blender.stackexchange.com/questions/31351/… $\endgroup$ Apr 30 at 4:14
  • $\begingroup$ This will also depend on the mesh of the object. eg there is no way to give the plane a z dimension. $\endgroup$
    – batFINGER
    Apr 30 at 8:52
  • $\begingroup$ Can anyone tell me why the SelectionOperator in the answer below does not run when the code is an addon? From what I can tell, neither the invoke nor the execute methods are executed when I click in the 3d view. Probably there is simple solution, but so far I I have not found it. $\endgroup$
    – SJK
    May 2 at 18:47
2
$\begingroup$

Ok, I confirmed what I commented above. You need to get the selected object(s), not the active objects.

So context.selected_objects is a list - you either need to always operate on just the first selected object, or all of them. If you just want to assume the first selected one, just replace this:

obj = context.active_object

with:

obj = None
if len(context.selected_objects) > 0:
    obj = context.selected_objects[0]

in every function where you're trying to get the active object. Otherwise run a loop over the context.selected_objects to affect all of them.

Also there's a bug here:

if obj:
    buttom_text="Edit Block"

should be button_text instead of buttom_text.

Also your invoke function is what gets called when you push the button, so that's ALWAYS creating a new block. If you want your Edit Block to work you need to change the invoke function.

Hope this is helpful!

$\endgroup$
1
  • $\begingroup$ Yes, yout answer is helpful. $\endgroup$
    – SJK
    May 1 at 22:28
0
$\begingroup$

I hope it is OK if I post a complete code that answers the question. I could not have figured it out without the previous answer and comments, but having a working code to experiment with is very helpful when trying to learn something new.

import bpy
from bpy.props import FloatProperty,BoolProperty, IntVectorProperty

last_selection = []

bpy.types.Scene.length = bpy.props.FloatProperty(name = "length",default = 2)
bpy.types.Scene.height = bpy.props.FloatProperty(name = "height",default = 1)
bpy.types.Scene.width = bpy.props.FloatProperty(name = "width",default = 1)

class DrawBlock(bpy.types.Operator):
    """A rectangular prism"""
    bl_idname = "myops.draw_block"
    bl_label = "Edit Block" 
    bl_description = "Draw a rectangular prism"
    bl_options = {'REGISTER', 'UNDO'}

    def new_block(self,context):
        width = bpy.context.scene.width
        height = bpy.context.scene.height
        length = bpy.context.scene.length

        bpy.ops.mesh.primitive_cube_add()
        obj=bpy.context.active_object
        obj.dimensions=(length,width,height)
        obj.location.z=obj.location.z+height/2

    def execute(self,context):
        obj = bpy.context.active_object
        if obj:
            w = bpy.context.scene.width
            h = bpy.context.scene.height
            length = bpy.context.scene.length
            obj.dimensions=(length,w,h)
        else:
            self.new_block(context)
        return {'FINISHED'}
    
class SCENE_PT_Build(bpy.types.Panel):
    """Creates a Panel in the scene context of the properties editor"""
    bl_label = "Build Menu"
    bl_idname = "SCENE_PT_build"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "scene"

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        objlist=bpy.context.selected_objects
        obj=[]
        for obj in objlist:
           if "Cube" in obj.name:
               break
        row=layout.row()
        row.label(text="block geometry:")        
        row=layout.row()
        row.prop(scene, "length")
        row=layout.row()
        row.prop(scene, "width")
        row = layout.row()
        row.prop(scene, "height")
        row=layout.row()
        button_text="New Block"
        if obj:
            button_text="Edit Block"
        row.operator("myops.draw_block",text=button_text)
        

# This section is mostly copied from #https://blender.stackexchange.com/questions/31351/update-stringproperty-on-object-selection-change

class SelectionOperator(bpy.types.Operator):
    """ Costum selection
    """
    bl_idname = "view3d.select_costum" 
    bl_label = "costum selection"

    extend : BoolProperty(default = False)
    deselect : BoolProperty(default = False)
    toggle : BoolProperty(default = True)
    center : BoolProperty(default = False)
    enumerate : BoolProperty(default = False)
    object : BoolProperty(default = False)
    location : IntVectorProperty(default = (0,0),subtype ='XYZ', size = 2)

    def execute(self, context):
        #select the object
        bpy.ops.view3d.select(extend=self.extend, deselect=self.deselect, deselect_all=True, toggle=self.toggle, center=self.center, enumerate=self.enumerate, object=self.object, location=(self.location[0] , self.location[1] ))
        #change the property
        global last_selection
        obj=bpy.context.active_object
        if obj != last_selection:
               last_selection = obj
               bpy.ops.object.select_all(action='DESELECT')
               obj.select_set(True)
               bpy.context.scene.length = obj.dimensions.x
               bpy.context.scene.width = obj.dimensions.y
               bpy.context.scene.height = obj.dimensions.z

        return {'FINISHED'}

    def invoke(self, context, event):
        if context.space_data.type == 'VIEW_3D':
            self.location[0] = event.mouse_region_x
            self.location[1]  = event.mouse_region_y
            return self.execute(context)
        else:
                self.report({'WARNING'}, "Active space must be a View3d")
                return {'CANCELLED'}

def replace_shortkey( old_op_name, new_op_name) :

        wm = bpy.context.window_manager
        keyconfig = wm.keyconfigs.active
        keymap = keyconfig.keymaps['3D View']
        items = keymap.keymap_items

        item = items.get(old_op_name, None)
        while item :

                props = item.properties

                extend    = props.extend.real
                deselect  = props.deselect.real
                toggle    = props.toggle.real
                center    = props.center.real
                enumerate = props.enumerate.real
                object    = props.object.real

                item.idname = new_op_name

                props.extend    = extend 
                props.deselect  = deselect
                props.toggle    = toggle
                props.center    = center
                props.enumerate = enumerate
                props.object    = object

                item = items.get( old_op_name, None)

def register():
   bpy.utils.register_class(SelectionOperator)
   bpy.utils.register_class(SCENE_PT_Build)
   bpy.utils.register_class(DrawBlock) 
   replace_shortkey( 'view3d.select', SelectionOperator.bl_idname )
   
def unregister():
   replace_shortkey(SelectionOperator.bl_idname, 'view3d.select')
   bpy.utils.unregister_class(DrawBlock)
   bpy.utils.unregister_class(SCENE_PT_Build)
   bpy.utils.unregister_class(SelectionOperator)

   
if __name__ == "__main__":
    register()
    bpy.ops.mesh.primitive_cube_add()
    obj=bpy.context.active_object
    obj.dimensions=[2,4,6]
    obj.location=[4,2,0]
    bpy.ops.mesh.primitive_cube_add()

IF the user clicks on one of the cubes, the dimensions of the cube show up in the panel and the object can be modified.

$\endgroup$

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.