forked from blender/blender-addons
Move copyright text to SPDX-FileCopyrightText or set to the Blender Foundation so "make check_licenses" now runs without warnings.
361 lines
11 KiB
Python
361 lines
11 KiB
Python
# SPDX-FileCopyrightText: 2017-2023 Blender Foundation
|
|
#
|
|
# 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, 4),
|
|
"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:
|
|
# Add a mirror modifier
|
|
mirror_modifier = bpy.context.object.modifiers.new("", 'MIRROR')
|
|
mirror_modifier.use_axis[0] = X # Choose the axis to use, based on the cut's axis
|
|
mirror_modifier.use_axis[1] = Y
|
|
mirror_modifier.use_axis[2] = Z
|
|
mirror_modifier.use_clip = automirror.Use_Matcap
|
|
mirror_modifier.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()
|