object_print3d_utils: replace f-strings by str.format() for I18n
[blender-addons.git] / mesh_auto_mirror.py
blob114ee9d310eedf9646158a56d087863cda1f76b0
1 # SPDX-License-Identifier: GPL-3.0-or-later
3 ######################################################################################################
4 # A simple add-on to auto cut in two and mirror an object #
5 # Actually partially uncommented (see further version) #
6 # Author: Lapineige, Bookyakuno #
7 ######################################################################################################
8 # 2.8 update by Bookyakuno, meta-androcto
10 bl_info = {
11 "name": "Auto Mirror",
12 "description": "Super fast cutting and mirroring for mesh",
13 "author": "Lapineige",
14 "version": (2, 5, 3),
15 "blender": (2, 80, 0),
16 "location": "View 3D > Sidebar > Edit Tab > AutoMirror (panel)",
17 "warning": "",
18 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/auto_mirror.html",
19 "category": "Mesh",
23 import bpy
24 from mathutils import Vector
26 import bmesh
27 import bpy
28 import collections
29 import mathutils
30 import math
31 from bpy_extras import view3d_utils
32 from bpy.types import (
33 Operator,
34 Menu,
35 Panel,
36 AddonPreferences,
37 PropertyGroup,
39 from bpy.props import (
40 BoolProperty,
41 EnumProperty,
42 FloatProperty,
43 IntProperty,
44 PointerProperty,
45 StringProperty,
49 # Operator
51 class AlignVertices(Operator):
53 """ Automatically cut an object along an axis """
55 bl_idname = "object.align_vertices"
56 bl_label = "Align Vertices on 1 Axis"
58 @classmethod
59 def poll(cls, context):
60 obj = context.active_object
61 return obj and obj.type == "MESH"
63 def execute(self, context):
64 automirror = context.scene.automirror
66 bpy.ops.object.mode_set(mode = 'OBJECT')
68 x1,y1,z1 = bpy.context.scene.cursor.location
69 bpy.ops.view3d.snap_cursor_to_selected()
71 x2,y2,z2 = bpy.context.scene.cursor.location
73 bpy.context.scene.cursor.location[0], \
74 bpy.context.scene.cursor.location[1], \
75 bpy.context.scene.cursor.location[2] = 0, 0, 0
77 #Vertices coordinate to 0 (local coordinate, so on the origin)
78 for vert in bpy.context.object.data.vertices:
79 if vert.select:
80 if automirror.axis == 'x':
81 axis = 0
82 elif automirror.axis == 'y':
83 axis = 1
84 elif automirror.axis == 'z':
85 axis = 2
86 vert.co[axis] = 0
88 bpy.context.scene.cursor.location = x2,y2,z2
90 bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
92 bpy.context.scene.cursor.location = x1,y1,z1
94 bpy.ops.object.mode_set(mode = 'EDIT')
95 return {'FINISHED'}
98 class AutoMirror(bpy.types.Operator):
99 """ Automatically cut an object along an axis """
100 bl_idname = "object.automirror"
101 bl_label = "AutoMirror"
102 bl_options = {'REGISTER'} # 'UNDO' ?
104 @classmethod
105 def poll(cls, context):
106 obj = context.active_object
107 return obj and obj.type == "MESH"
109 def draw(self, context):
110 automirror = context.scene.automirror
112 layout = self.layout
113 if bpy.context.object and bpy.context.object.type == 'MESH':
114 layout.prop(automirror, "axis", text = "Mirror axis")
115 layout.prop(automirror, "orientation", text = "Orientation")
116 layout.prop(automirror, "threshold", text = "Threshold")
117 layout.prop(automirror, "toggle_edit", text = "Toggle edit")
118 layout.prop(automirror, "cut", text = "Cut and mirror")
119 if automirror.cut:
120 layout.prop(automirror, "clipping", text = "Clipping")
121 layout.prop(automirror, "mirror", text = "Apply mirror")
123 else:
124 layout.label(icon = "ERROR", text = "No mesh selected")
126 def get_local_axis_vector(self, context, X, Y, Z, orientation):
127 loc = context.object.location
128 bpy.ops.object.mode_set(mode = "OBJECT") # Needed to avoid to translate vertices
130 v1 = Vector((loc[0],loc[1],loc[2]))
131 bpy.ops.transform.translate(value = (X*orientation, Y*orientation, Z*orientation),
132 constraint_axis = ((X==1), (Y==1), (Z==1)),
133 orient_type = 'LOCAL')
134 v2 = Vector((loc[0],loc[1],loc[2]))
135 bpy.ops.transform.translate(value = (-X*orientation, -Y*orientation, -Z*orientation),
136 constraint_axis = ((X==1), (Y==1), (Z==1)),
137 orient_type = 'LOCAL')
139 bpy.ops.object.mode_set(mode="EDIT")
140 return v2-v1
142 def execute(self, context):
143 context.active_object.select_set(True)
145 automirror = context.scene.automirror
147 X,Y,Z = 0,0,0
149 if automirror.axis == 'x':
150 X = 1
151 elif automirror.axis == 'y':
152 Y = 1
153 elif automirror.axis == 'z':
154 Z = 1
156 current_mode = bpy.context.object.mode # Save the current mode
158 if bpy.context.object.mode != "EDIT":
159 bpy.ops.object.mode_set(mode = "EDIT") # Go to edit mode
161 bpy.ops.mesh.select_all(action = 'SELECT') # Select all the vertices
163 if automirror.orientation == 'positive':
164 orientation = 1
165 else:
166 orientation = -1
168 cut_normal = self.get_local_axis_vector(context, X, Y, Z, orientation)
170 # Cut the mesh
171 bpy.ops.mesh.bisect(
172 plane_co = (
173 bpy.context.object.location[0],
174 bpy.context.object.location[1],
175 bpy.context.object.location[2]
177 plane_no = cut_normal,
178 use_fill = False,
179 clear_inner = automirror.cut,
180 clear_outer = 0,
181 threshold = automirror.threshold
184 bpy.ops.object.align_vertices() # Use to align the vertices on the origin, needed by the "threshold"
186 if not automirror.toggle_edit:
187 bpy.ops.object.mode_set(mode = current_mode) # Reload previous mode
189 if automirror.cut:
190 bpy.ops.object.modifier_add(type = 'MIRROR') # Add a mirror modifier
191 bpy.context.object.modifiers[-1].use_axis[0] = X # Choose the axis to use, based on the cut's axis
192 bpy.context.object.modifiers[-1].use_axis[1] = Y
193 bpy.context.object.modifiers[-1].use_axis[2] = Z
194 bpy.context.object.modifiers[-1].use_clip = automirror.Use_Matcap
195 bpy.context.object.modifiers[-1].show_on_cage = automirror.show_on_cage
196 if automirror.apply_mirror:
197 bpy.ops.object.mode_set(mode = 'OBJECT')
198 bpy.ops.object.modifier_apply(modifier = bpy.context.object.modifiers[-1].name)
199 if automirror.toggle_edit:
200 bpy.ops.object.mode_set(mode = 'EDIT')
201 else:
202 bpy.ops.object.mode_set(mode = current_mode)
204 return {'FINISHED'}
207 # Panel
209 class VIEW3D_PT_BisectMirror(Panel):
210 bl_space_type = 'VIEW_3D'
211 bl_region_type = 'UI'
212 bl_label = "Auto Mirror"
213 bl_category = 'Edit'
214 bl_options = {'DEFAULT_CLOSED'}
216 def draw(self, context):
217 automirror = context.scene.automirror
219 layout = self.layout
220 col = layout.column(align=True)
222 layout = self.layout
224 if bpy.context.object and bpy.context.object.type == 'MESH':
225 layout.operator("object.automirror")
226 layout.prop(automirror, "axis", text = "Mirror Axis", expand=True)
227 layout.prop(automirror, "orientation", text = "Orientation")
228 layout.prop(automirror, "threshold", text = "Threshold")
229 layout.prop(automirror, "toggle_edit", text = "Toggle Edit")
230 layout.prop(automirror, "cut", text = "Cut and Mirror")
231 if bpy.context.scene.automirror.cut:
232 layout.prop(automirror, "Use_Matcap", text = "Use Clip")
233 layout.prop(automirror, "show_on_cage", text = "Editable")
234 layout.prop(automirror, "apply_mirror", text = "Apply Mirror")
236 else:
237 layout.label(icon="ERROR", text = "No mesh selected")
239 # Properties
240 class AutoMirrorProps(PropertyGroup):
241 axis : EnumProperty(
242 items = [("x", "X", "", 1),
243 ("y", "Y", "", 2),
244 ("z", "Z", "", 3)],
245 description="Axis used by the mirror modifier",
248 orientation : EnumProperty(
249 items = [("positive", "Positive", "", 1),("negative", "Negative", "", 2)],
250 description="Choose the side along the axis of the editable part (+/- coordinates)",
253 threshold : FloatProperty(
254 default= 0.001, min= 0.001,
255 description="Vertices closer than this distance are merged on the loopcut",
258 toggle_edit : BoolProperty(
259 default= False,
260 description="If not in edit mode, change mode to edit",
263 cut : BoolProperty(
264 default= True,
265 description="If enabled, cut the mesh in two parts and mirror it. If not, just make a loopcut",
268 clipping : BoolProperty(
269 default=True,
272 Use_Matcap : BoolProperty(
273 default=True,
274 description="Use clipping for the mirror modifier",
277 show_on_cage : BoolProperty(
278 default=False,
279 description="Enable to edit the cage (it's the classical modifier's option)",
282 apply_mirror : BoolProperty(
283 description="Apply the mirror modifier (useful to symmetrise the mesh)",
287 # Add-ons Preferences Update Panel
289 # Define Panel classes for updating
290 panels = (
291 VIEW3D_PT_BisectMirror,
295 def update_panel(self, context):
296 message = ": Updating Panel locations has failed"
297 try:
298 for panel in panels:
299 if "bl_rna" in panel.__dict__:
300 bpy.utils.unregister_class(panel)
302 for panel in panels:
303 panel.bl_category = context.preferences.addons[__name__].preferences.category
304 bpy.utils.register_class(panel)
306 except Exception as e:
307 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
308 pass
311 class AutoMirrorAddonPreferences(AddonPreferences):
312 # this must match the addon name, use '__package__'
313 # when defining this in a submodule of a python package.
314 bl_idname = __name__
316 category: StringProperty(
317 name = "Tab Category",
318 description = "Choose a name for the category of the panel",
319 default = "Edit",
320 update = update_panel
323 def draw(self, context):
324 layout = self.layout
326 row = layout.row()
327 col = row.column()
328 col.label(text = "Tab Category:")
329 col.prop(self, "category", text = "")
331 # define classes for registration
332 classes = (
333 VIEW3D_PT_BisectMirror,
334 AutoMirror,
335 AlignVertices,
336 AutoMirrorAddonPreferences,
337 AutoMirrorProps,
341 # registering and menu integration
342 def register():
343 for cls in classes:
344 bpy.utils.register_class(cls)
346 bpy.types.Scene.automirror = PointerProperty(type = AutoMirrorProps)
347 update_panel(None, bpy.context)
349 # unregistering and removing menus
350 def unregister():
351 for cls in reversed(classes):
352 bpy.utils.unregister_class(cls)
354 del bpy.types.Scene.automirror
356 if __name__ == "__main__":
357 register()