5
$\begingroup$

I would like to make a custom button on the N-panel.

I already went through blender's templates and amended the ui button script to 2.8 but I don't understand where and how I should implement my code into the template.

Also I would love to be able to remove doubles and display the current vertex count next to it before and after the operation:

import bpy
context = bpy.context
scene = context.scene

bpy.ops.object.select_all(action='SELECT')

for ob in scene.objects[:]:
    bpy.ops.object.editmode_toggle()
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.remove_doubles(threshold=0.02)
    bpy.ops.object.editmode_toggle()

Q: How can I add a custom button with that functionality on the N-panel?

$\endgroup$
6
  • 2
    $\begingroup$ Related re remove doubles multiple mesh objects. blender.stackexchange.com/a/68133/15543 $\endgroup$
    – batFINGER
    Feb 24, 2020 at 16:25
  • $\begingroup$ Still having issues with showing the vert count $\endgroup$
    – four two
    Feb 25, 2020 at 21:23
  • $\begingroup$ @brockmann THANKS A LOT! works like a charm $\endgroup$
    – four two
    Feb 26, 2020 at 9:15
  • $\begingroup$ @brockmann, i think so.. it does what i want it to do what's wrong with it? $\endgroup$
    – four two
    Feb 26, 2020 at 11:33
  • 1
    $\begingroup$ Added a "final version". I guess your version is even 2 or 3 times slower than my optimized version of your code. Just ask if there is something you do not understand. ...And please don't use this code in your question... $\endgroup$
    – brockmann
    Feb 26, 2020 at 17:48

1 Answer 1

12
$\begingroup$

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.

bpy.ops.mesh.remove_doubles()

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.

bmesh.ops.remove_doubles()

    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()
$\endgroup$
7
  • $\begingroup$ I see what you did there... thanks a lot for the detailed appendix :) I have one question: merge_dist: bpy.props.FloatProperty.. how come it moved into the class and why you need the name, description, min, step ? @brockmann $\endgroup$
    – four two
    Feb 27, 2020 at 9:14
  • $\begingroup$ The reason is simple: Adding a property to the operator makes it accessible in the UI (Operator Panel or F6), see: i.stack.imgur.com/VYr8C.png Adding min, max, step etc. to the FloatProperty just avoids the user is doing any mistakes, it's an intentional limit. Imagine a script adding subsurf modifiers to each object and the user accidentally sets the subsurf value to 100 ... @fourtwo $\endgroup$
    – brockmann
    Feb 27, 2020 at 9:32
  • $\begingroup$ I understand, but there is no input from the user in this script @brockmann $\endgroup$
    – four two
    Feb 27, 2020 at 11:39
  • $\begingroup$ That's just the way blender works and I added that for completeness because usually you want interaction. Also it's nice to have it there in case... if you don't like it for whatever reason just throw it away and replace self.merge_dist with a float value like 0.01: bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.01), easy but not that user friendly IMHO @fourtwo $\endgroup$
    – brockmann
    Feb 27, 2020 at 12:23
  • $\begingroup$ no it's good to know, thanks again ;) $\endgroup$
    – four two
    Feb 27, 2020 at 12:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .