I suggest start here: How to create a custom UI?
To extend the Properties Area of the 3D View you can either register a custom panel to add UI elements onto or you can append elements to existing panels.
1. Panel
Blender comes with a few UI templates (Text Editor > Templates > UI ...) . I'd suggest start with UI Panel Simple ui_panel_simple.py
and use it as base to work on. You just have to edit its default variables in order to display the HelloWorldPanel
as part of the properties panel:
- Set
bl_space_type
to 'VIEW_3D'
to make it part of the 3D View
- Set
bl_context
to eg. 'mesh_edit'
or 'objectmode'
to either match Edit or Object Mode
class HelloWorldPanel(bpy.types.Panel):
"""Creates a Sub-Panel in the Property Area of the 3D View"""
bl_label = "My Tool"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "My Tools"
bl_context = "mesh_edit" # "objectmode"
The HelloWorldPanel
comes with a predefined draw
method where you can edit the layout as well as adding UI elements to the panel. More interestingly: You can easily add existing operators like bpy.ops.mesh.primitive_cube_add()
to the panel by calling the operator()
method on the layout element like row.operator("mesh.primitive_cube_add")
without the namespace (bpy.ops*
).
row = layout.row()
# bpy.ops.mesh.primitive_cube_add()
row.operator("mesh.primitive_cube_add")
row = layout.row()
# bpy.ops.object.origin_set()
row.operator("object.origin_set").type="GEOMETRY_ORIGIN"
To pass multiple arguments read: How to pass multiple operator properties via UI layout?
2. Operator
If you want a custom function assigned to a button, you'll have to create a new operator. Blender comes with operator_simple.py
template which is easy to understand. In most cases it's enough adding your code to the execute
method, which will be called when the button is pressed.
def execute(self, context):
my_variable = "Hello World"
print (my_variable) # Prints 'Hello World' to the Console
self.report({'INFO'}, my_variable) # Reports 'Hello World' to the Info Area
return {'FINISHED'} # Return the execution is finished
If you want to call another operator as part of the operation like bpy.ops.mesh.remove_doubles()
which only works on meshes, it's a good idea to limit the scope to mesh objects using the provided poll
method, even if the operator is only accessible in Edit Mode because otherwise the operator is still callable using the Search Menu (F3):
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj is not None and obj.type == 'MESH')
def execute(self, context):
# Your code here
bpy.ops.mesh.remove_doubles(threshold=0.02)
# ...
return {'FINISHED'}
Also read: What do operator methods do? (poll, invoke, execute, draw & modal)
If you want to display the vertex count after mesh.remove_doubles
, you can store the vertex count before calling the operator and then report
the actual result:
def execute(self, context):
obj = context.object
verts_before = len(obj.data.vertices)
bpy.ops.mesh.remove_doubles(threshold=0.02)
verts_after = len(obj.data.vertices)
result = verts_before - verts_after
self.report({'INFO'}, "Verts removed: {}".format(result))
return {'FINISHED'}
3. Registration
Register your classes properly, add your operator(s), labels, and properties to the panel:
import bpy
class MY_OT_custom(bpy.types.Operator):
"""Tooltip"""
bl_idname = "object.simple_operator"
bl_label = "Simple Object Operator"
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj is not None and obj.type == 'MESH')
def execute(self, context):
# Your code here
# ...
bpy.ops.mesh.remove_doubles(threshold=0.02)
return {'FINISHED'}
class MY_PT_custom(bpy.types.Panel):
"""Creates a Sub-Panel in the Property Area of the 3D View"""
bl_label = "My Tool"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "My Tools"
bl_context = "mesh_edit"
def draw(self, context):
obj = context.object
layout = self.layout
row = layout.row()
row.label(text="Active object is: {}".format(obj.name))
row = layout.row()
row.prop(obj, "name")
row = layout.row()
row.label(text="Vertex Count: {}".format(len(obj.data.vertices)))
row = layout.row()
row.operator(MY_OT_custom.bl_idname)
def register():
bpy.utils.register_class(MY_PT_custom)
bpy.utils.register_class(MY_OT_custom)
def unregister():
bpy.utils.unregister_class(MY_OT_custom)
bpy.utils.unregister_class(MY_PT_custom)
if __name__ == "__main__":
register()
Appendix
TLDR; If you want to 'remove doubles' (merge by distance) for multiple meshes in Object Mode for whatever reason, use the bmesh.ops.remove_doubles()
operator as suggested by @batFINGER, it's 3 times faster.
As of 2.8x we can enter Edit Mode for all selected objects simultaneously, which avoids looping over all the objects as well as switching to Edit Mode and Object Mode each iteration so your code can be simplified as follows:
def execute(self, context):
# get all mesh objects in selection
meshes = set(o for o in context.selected_objects if o.type == 'MESH')
# store the vert count on all objects
verts_before = sum([len(o.data.vertices) for o in meshes])
# switch to edit mode
bpy.ops.object.mode_set(mode='EDIT')
# select all vertices
bpy.ops.mesh.select_all(action='SELECT')
# remove the doubles
bpy.ops.mesh.remove_doubles(threshold=0.01)
# switch to object mode
bpy.ops.object.mode_set(mode='OBJECT')
# store the vert count again
verts_after = sum([len(o.data.vertices) for o in meshes])
self.report({'INFO'}, "Verts removed: {}".format(verts_before-verts_after))
return {'FINISHED'}
Although this saves a lot of time, it still takes 0.0814 seconds for 100 cubes with a vertex count of 8080.
def execute(self, context):
# get all mesh objects in selection
meshes = set(o.data for o in context.selected_objects if o.type == 'MESH')
# store the vert count on all objects
verts_before = sum([len(o.vertices) for o in meshes])
# Remove doubles using bmesh based on:
# https://blender.stackexchange.com/a/68133/31447
bm = bmesh.new()
for m in meshes:
bm.from_mesh(m)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=self.merge_dist)
bm.to_mesh(m)
m.update()
bm.clear()
bm.free()
# store the vert count again
verts_after = sum([len(o.vertices) for o in meshes])
self.report({'INFO'}, "Verts removed: {}".format(verts_before-verts_after))
return {'FINISHED'}
Bmesh method takes 0.0290 seconds for 100 cubes with a vertex count of 8080.
Final Panel & Operator
Panel in Object Mode and a proper implementation of the operator (Undo, Merge Distance etc.) in order to remove possible doubles for all objects in selection:
import bpy
import bmesh
class MY_OT_rem_doubles_bmesh(bpy.types.Operator):
"""Remove Doubles on Objects in Selection"""
bl_idname = "object.remove_doubles_bmesh"
bl_label = "Remove Doubles (bmesh)"
bl_options = {'REGISTER', 'UNDO'}
merge_dist: bpy.props.FloatProperty(
name="Merge Distance",
description="Merge Distance",
min=0.0,
step=0.1,
default=0.02
)
@classmethod
def poll(cls, context):
obj = context.active_object
return (obj is not None and obj.type == 'MESH')
def execute(self, context):
meshes = set(o.data for o in context.selected_objects if o.type == 'MESH')
verts_before = sum(len(o.vertices) for o in meshes)
# Remove doubles based on:
# https://blender.stackexchange.com/a/68133/31447
bm = bmesh.new()
for m in meshes:
bm.from_mesh(m)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=self.merge_dist)
bm.to_mesh(m)
m.update()
bm.clear()
bm.free()
verts_after = sum([len(o.vertices) for o in meshes])
self.report({'INFO'}, "Verts removed: {}".format(verts_before-verts_after))
return {'FINISHED'}
class MY_PT_custom(bpy.types.Panel):
"""Creates a Sub-Panel in the Property Area of the 3D View"""
bl_label = "My Tools"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool"
bl_context = "objectmode"
def draw(self, context):
sel_objs = context.selected_objects
sel_vert_count = sum(len(o.data.vertices) for o in sel_objs if o.type == 'MESH')
layout = self.layout
row = layout.row()
row.operator(MY_OT_rem_doubles_bmesh.bl_idname)
layout.separator()
row = layout.row()
row.label(text="{} Objects in Selection".format(len(sel_objs)))
row = layout.row()
row.label(text="Vertex Count: {}".format(sel_vert_count))
def register():
bpy.utils.register_class(MY_OT_rem_doubles_bmesh)
bpy.utils.register_class(MY_PT_custom)
def unregister():
bpy.utils.unregister_class(MY_OT_rem_doubles_bmesh)
bpy.utils.unregister_class(MY_PT_custom)
if __name__ == "__main__":
register()