# SPDX-License-Identifier: GPL-3.0-or-later ###################################################################################################### # A simple add-on to auto cut in two and mirror an object # # Actually partially uncommented (see further version) # # Author: Lapineige, Bookyakuno # ###################################################################################################### # 2.8 update by Bookyakuno, meta-androcto bl_info = { "name": "Auto Mirror", "description": "Super fast cutting and mirroring for mesh", "author": "Lapineige", "version": (2, 5, 3), "blender": (2, 80, 0), "location": "View 3D > Sidebar > Edit Tab > AutoMirror (panel)", "warning": "", "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/auto_mirror.html", "category": "Mesh", } import bpy from mathutils import Vector import bmesh import bpy import collections import mathutils import math from bpy_extras import view3d_utils from bpy.types import ( Operator, Menu, Panel, AddonPreferences, PropertyGroup, ) from bpy.props import ( BoolProperty, EnumProperty, FloatProperty, IntProperty, PointerProperty, StringProperty, ) # Operator class AlignVertices(Operator): """ Automatically cut an object along an axis """ bl_idname = "object.align_vertices" bl_label = "Align Vertices on 1 Axis" @classmethod def poll(cls, context): obj = context.active_object return obj and obj.type == "MESH" def execute(self, context): automirror = context.scene.automirror bpy.ops.object.mode_set(mode = 'OBJECT') x1,y1,z1 = bpy.context.scene.cursor.location bpy.ops.view3d.snap_cursor_to_selected() x2,y2,z2 = bpy.context.scene.cursor.location bpy.context.scene.cursor.location[0], \ bpy.context.scene.cursor.location[1], \ bpy.context.scene.cursor.location[2] = 0, 0, 0 #Vertices coordinate to 0 (local coordinate, so on the origin) for vert in bpy.context.object.data.vertices: if vert.select: if automirror.axis == 'x': axis = 0 elif automirror.axis == 'y': axis = 1 elif automirror.axis == 'z': axis = 2 vert.co[axis] = 0 bpy.context.scene.cursor.location = x2,y2,z2 bpy.ops.object.origin_set(type='ORIGIN_CURSOR') bpy.context.scene.cursor.location = x1,y1,z1 bpy.ops.object.mode_set(mode = 'EDIT') return {'FINISHED'} class AutoMirror(bpy.types.Operator): """ Automatically cut an object along an axis """ bl_idname = "object.automirror" bl_label = "AutoMirror" bl_options = {'REGISTER'} # 'UNDO' ? @classmethod def poll(cls, context): obj = context.active_object return obj and obj.type == "MESH" def draw(self, context): automirror = context.scene.automirror layout = self.layout if bpy.context.object and bpy.context.object.type == 'MESH': layout.prop(automirror, "axis", text = "Mirror axis") layout.prop(automirror, "orientation", text = "Orientation") layout.prop(automirror, "threshold", text = "Threshold") layout.prop(automirror, "toggle_edit", text = "Toggle edit") layout.prop(automirror, "cut", text = "Cut and mirror") if automirror.cut: layout.prop(automirror, "clipping", text = "Clipping") layout.prop(automirror, "mirror", text = "Apply mirror") else: layout.label(icon = "ERROR", text = "No mesh selected") def get_local_axis_vector(self, context, X, Y, Z, orientation): loc = context.object.location bpy.ops.object.mode_set(mode = "OBJECT") # Needed to avoid to translate vertices v1 = Vector((loc[0],loc[1],loc[2])) bpy.ops.transform.translate(value = (X*orientation, Y*orientation, Z*orientation), constraint_axis = ((X==1), (Y==1), (Z==1)), orient_type = 'LOCAL') v2 = Vector((loc[0],loc[1],loc[2])) bpy.ops.transform.translate(value = (-X*orientation, -Y*orientation, -Z*orientation), constraint_axis = ((X==1), (Y==1), (Z==1)), orient_type = 'LOCAL') bpy.ops.object.mode_set(mode="EDIT") return v2-v1 def execute(self, context): context.active_object.select_set(True) automirror = context.scene.automirror X,Y,Z = 0,0,0 if automirror.axis == 'x': X = 1 elif automirror.axis == 'y': Y = 1 elif automirror.axis == 'z': Z = 1 current_mode = bpy.context.object.mode # Save the current mode if bpy.context.object.mode != "EDIT": bpy.ops.object.mode_set(mode = "EDIT") # Go to edit mode bpy.ops.mesh.select_all(action = 'SELECT') # Select all the vertices if automirror.orientation == 'positive': orientation = 1 else: orientation = -1 cut_normal = self.get_local_axis_vector(context, X, Y, Z, orientation) # Cut the mesh bpy.ops.mesh.bisect( plane_co = ( bpy.context.object.location[0], bpy.context.object.location[1], bpy.context.object.location[2] ), plane_no = cut_normal, use_fill = False, clear_inner = automirror.cut, clear_outer = 0, threshold = automirror.threshold ) bpy.ops.object.align_vertices() # Use to align the vertices on the origin, needed by the "threshold" if not automirror.toggle_edit: bpy.ops.object.mode_set(mode = current_mode) # Reload previous mode if automirror.cut: bpy.ops.object.modifier_add(type = 'MIRROR') # Add a mirror modifier bpy.context.object.modifiers[-1].use_axis[0] = X # Choose the axis to use, based on the cut's axis bpy.context.object.modifiers[-1].use_axis[1] = Y bpy.context.object.modifiers[-1].use_axis[2] = Z bpy.context.object.modifiers[-1].use_clip = automirror.Use_Matcap bpy.context.object.modifiers[-1].show_on_cage = automirror.show_on_cage if automirror.apply_mirror: bpy.ops.object.mode_set(mode = 'OBJECT') bpy.ops.object.modifier_apply(modifier = bpy.context.object.modifiers[-1].name) if automirror.toggle_edit: bpy.ops.object.mode_set(mode = 'EDIT') else: bpy.ops.object.mode_set(mode = current_mode) return {'FINISHED'} # Panel class VIEW3D_PT_BisectMirror(Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_label = "Auto Mirror" bl_category = 'Edit' bl_options = {'DEFAULT_CLOSED'} def draw(self, context): automirror = context.scene.automirror layout = self.layout col = layout.column(align=True) layout = self.layout if bpy.context.object and bpy.context.object.type == 'MESH': layout.operator("object.automirror") layout.prop(automirror, "axis", text = "Mirror Axis", expand=True) layout.prop(automirror, "orientation", text = "Orientation") layout.prop(automirror, "threshold", text = "Threshold") layout.prop(automirror, "toggle_edit", text = "Toggle Edit") layout.prop(automirror, "cut", text = "Cut and Mirror") if bpy.context.scene.automirror.cut: layout.prop(automirror, "Use_Matcap", text = "Use Clip") layout.prop(automirror, "show_on_cage", text = "Editable") layout.prop(automirror, "apply_mirror", text = "Apply Mirror") else: layout.label(icon="ERROR", text = "No mesh selected") # Properties class AutoMirrorProps(PropertyGroup): axis : EnumProperty( items = [("x", "X", "", 1), ("y", "Y", "", 2), ("z", "Z", "", 3)], description="Axis used by the mirror modifier", ) orientation : EnumProperty( items = [("positive", "Positive", "", 1),("negative", "Negative", "", 2)], description="Choose the side along the axis of the editable part (+/- coordinates)", ) threshold : FloatProperty( default= 0.001, min= 0.001, description="Vertices closer than this distance are merged on the loopcut", ) toggle_edit : BoolProperty( default= False, description="If not in edit mode, change mode to edit", ) cut : BoolProperty( default= True, description="If enabled, cut the mesh in two parts and mirror it. If not, just make a loopcut", ) clipping : BoolProperty( default=True, ) Use_Matcap : BoolProperty( default=True, description="Use clipping for the mirror modifier", ) show_on_cage : BoolProperty( default=False, description="Enable to edit the cage (it's the classical modifier's option)", ) apply_mirror : BoolProperty( description="Apply the mirror modifier (useful to symmetrise the mesh)", ) # Add-ons Preferences Update Panel # Define Panel classes for updating panels = ( VIEW3D_PT_BisectMirror, ) def update_panel(self, context): message = ": Updating Panel locations has failed" try: for panel in panels: if "bl_rna" in panel.__dict__: bpy.utils.unregister_class(panel) for panel in panels: panel.bl_category = context.preferences.addons[__name__].preferences.category bpy.utils.register_class(panel) except Exception as e: print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e)) pass class AutoMirrorAddonPreferences(AddonPreferences): # this must match the addon name, use '__package__' # when defining this in a submodule of a python package. bl_idname = __name__ category: StringProperty( name = "Tab Category", description = "Choose a name for the category of the panel", default = "Edit", update = update_panel ) def draw(self, context): layout = self.layout row = layout.row() col = row.column() col.label(text = "Tab Category:") col.prop(self, "category", text = "") # define classes for registration classes = ( VIEW3D_PT_BisectMirror, AutoMirror, AlignVertices, AutoMirrorAddonPreferences, AutoMirrorProps, ) # registering and menu integration def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.automirror = PointerProperty(type = AutoMirrorProps) update_panel(None, bpy.context) # unregistering and removing menus def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) del bpy.types.Scene.automirror if __name__ == "__main__": register()