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$ Commented Apr 29, 2021 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$ Commented Apr 30, 2021 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$ Commented Apr 30, 2021 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$ Commented Apr 30, 2021 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$ Commented May 2, 2021 at 18:47

2 Answers 2

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$ Commented May 1, 2021 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$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.