diff options
28 files changed, 11090 insertions, 0 deletions
diff --git a/mesh_extra_tools/__init__.py b/mesh_extra_tools/__init__.py new file mode 100644 index 00000000..90f5c674 --- /dev/null +++ b/mesh_extra_tools/__init__.py @@ -0,0 +1,762 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# Contributed to by: +# meta-androcto, Hidesato Ikeya, zmj100, luxuy_BlenderCN, TrumanBlending, PKHG, # +# Oscurart, Greg, Stanislav Blinov, komi3D, BlenderLab, Paul Marshall (brikbot), # +# metalliandy, macouno, CoDEmanX, dustractor, Liero, lijenstina, Germano Cavalcante # + +bl_info = { + "name": "Edit Tools 2", + "author": "meta-androcto", + "version": (0, 3, 1), + "blender": (2, 77, 0), + "location": "View3D > Toolshelf > Tools and Specials (W-key)", + "description": "Extra mesh edit tools - modifying meshes and selection", + "warning": "", + "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/Extra_Tools", + "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/", + "category": "Mesh"} + + +# Import From Files +if "bpy" in locals(): + import importlib + importlib.reload(face_inset_fillet) + importlib.reload(mesh_filletplus) + importlib.reload(mesh_vertex_chamfer) + importlib.reload(mesh_mextrude_plus) + importlib.reload(mesh_offset_edges) + importlib.reload(pkhg_faces) + importlib.reload(mesh_edge_roundifier) + importlib.reload(mesh_cut_faces) + importlib.reload(split_solidify) + importlib.reload(mesh_to_wall) + importlib.reload(mesh_edges_length) + importlib.reload(random_vertices) + importlib.reload(mesh_fastloop) + importlib.reload(mesh_edgetools) + importlib.reload(mesh_pen_tool) + importlib.reload(vfe_specials) + importlib.reload(mesh_help) + importlib.reload(mesh_select_by_direction) + importlib.reload(mesh_select_by_edge_length) + importlib.reload(mesh_select_by_pi) + importlib.reload(mesh_select_by_type) + importlib.reload(mesh_select_connected_faces) + importlib.reload(mesh_index_select) + importlib.reload(mesh_selection_topokit) + importlib.reload(mesh_info_select) + importlib.reload(mesh_extrude_and_reshape) + +else: + from . import face_inset_fillet + from . import mesh_filletplus + from . import mesh_vertex_chamfer + from . import mesh_mextrude_plus + from . import mesh_offset_edges + from . import pkhg_faces + from . import mesh_edge_roundifier + from . import mesh_cut_faces + from . import split_solidify + from . import mesh_to_wall + from . import mesh_edges_length + from . import random_vertices + from . import mesh_fastloop + from . import mesh_edgetools + from . import mesh_pen_tool + from . import vfe_specials + from . import mesh_help + from . import mesh_extrude_and_reshape + + from .mesh_select_tools import mesh_select_by_direction + from .mesh_select_tools import mesh_select_by_edge_length + from .mesh_select_tools import mesh_select_by_pi + from .mesh_select_tools import mesh_select_by_type + from .mesh_select_tools import mesh_select_connected_faces + from .mesh_select_tools import mesh_index_select + from .mesh_select_tools import mesh_selection_topokit + from .mesh_select_tools import mesh_info_select + +import bpy +from bpy.types import ( + Menu, + Panel, + PropertyGroup, + AddonPreferences, + ) +from bpy.props import ( + BoolProperty, + BoolVectorProperty, + IntVectorProperty, + PointerProperty, + ) + + +# ------ MENUS ------ # + +# Define the "Extras" menu +class VIEW3D_MT_edit_mesh_extras(Menu): + bl_idname = "VIEW3D_MT_edit_mesh_extras" + bl_label = "Edit Tools" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + mode = context.tool_settings.mesh_select_mode + + if mode[0]: + split = layout.split() + col = split.column() + + col.label(text="Vertex", icon="VERTEXSEL") + col.separator() + + col.operator("mesh.vertex_chamfer", text="Vertex Chamfer") + col.operator("mesh.random_vertices", text="Random Vertices") + + col = split.column() + col.label(text="Utilities", icon="SCRIPTWIN") + col.separator() + + col.operator("object_ot.fastloop", text="Fast loop") + col.operator("mesh.flip_normals", text="Normals Flip") + col.operator("mesh.remove_doubles", text="Remove Doubles") + col.operator("mesh.subdivide", text="Subdivide") + col.operator("mesh.dissolve_limited", text="Dissolve Limited") + + elif mode[1]: + split = layout.split() + col = split.column() + col.label(text="Edge", icon="EDGESEL") + col.separator() + + col.operator("mesh.fillet_plus", text="Edge Fillet Plus") + col.operator("mesh.offset_edges", text="Offset Edges") + col.operator("mesh.edge_roundifier", text="Edge Roundify") + col.operator("object.mesh_edge_length_set", text="Set Edge Length") + col.operator("bpt.mesh_to_wall", text="Edge(s) to Wall") + + col = split.column() + col.label(text="Utilities", icon="SCRIPTWIN") + col.separator() + + col.operator("object_ot.fastloop", text="Fast loop") + col.operator("mesh.flip_normals", text="Normals Flip") + col.operator("mesh.remove_doubles", text="Remove Doubles") + + col.operator("mesh.subdivide", text="Subdivide") + col.operator("mesh.dissolve_limited", text="Dissolve Limited") + + elif mode[2]: + split = layout.split() + col = split.column() + col.label(text="Face", icon="FACESEL") + col.separator() + + col.operator("object.mextrude", text="Multi Extrude") + col.operator("mesh.face_inset_fillet", text="Face Inset Fillet") + col.operator("mesh.extrude_reshape", text="Push/Pull") + col.operator("mesh.add_faces_to_object", text="PKHG Faces") + col.operator("mesh.ext_cut_faces", text="Cut Faces") + col.operator("mesh.split_solidify", text="Split Solidify") + + col = split.column() + col.label(text="Utilities", icon="SCRIPTWIN") + col.separator() + + col.operator("object_ot.fastloop", text="Fast loop") + col.operator("mesh.flip_normals", text="Normals Flip") + col.operator("mesh.remove_doubles", text="Remove Doubles") + col.operator("mesh.subdivide", text="Subdivide") + col.operator("mesh.dissolve_limited", text="Dissolve Limited") + + +class EditToolsPanel(Panel): + bl_label = "Mesh Edit Tools" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_context = "mesh_edit" + bl_category = "Tools" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + scene = context.scene + VERTDROP = scene.mesh_extra_tools.UiTabDrop[0] + EDGEDROP = scene.mesh_extra_tools.UiTabDrop[1] + FACEDROP = scene.mesh_extra_tools.UiTabDrop[2] + UTILSDROP = scene.mesh_extra_tools.UiTabDrop[3] + # Change icons depending on the bool state (complient with the rest of the UI) + icon_active_0 = "TRIA_RIGHT" if not VERTDROP else "TRIA_DOWN" + icon_active_1 = "TRIA_RIGHT" if not EDGEDROP else "TRIA_DOWN" + icon_active_2 = "TRIA_RIGHT" if not FACEDROP else "TRIA_DOWN" + icon_active_3 = "TRIA_RIGHT" if not UTILSDROP else "TRIA_DOWN" + + layout = self.layout + + # Vert options + box1 = self.layout.box() + col = box1.column(align=True) + row = col.row(align=True) + row.prop(scene.mesh_extra_tools, "UiTabDrop", text="Vertex", index=0, icon=icon_active_0) + if not VERTDROP: + row.menu("mesh.vert_select_tools", icon="RESTRICT_SELECT_OFF", text="") + row.menu("VIEW3D_MT_Select_Vert", icon="VERTEXSEL", text="") + else: + layout = self.layout + + row = layout.row() + row.label(text="Vertex Tools:", icon="VERTEXSEL") + + row = layout.split(0.8, align=True) + row.operator("mesh.vertex_chamfer", text="Chamfer") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_vertex_chamfer" + + row = layout.split(0.8, align=True) + row.operator("mesh.random_vertices", text="Random Vertices") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "random_vertices" + + # Edge options + box1 = self.layout.box() + col = box1.column(align=True) + row = col.row(align=True) + row.prop(scene.mesh_extra_tools, "UiTabDrop", text="Edge", index=1, icon=icon_active_1) + + if not EDGEDROP: + row.menu("mesh.edge_select_tools", icon="RESTRICT_SELECT_OFF", text="") + row.menu("VIEW3D_MT_Select_Edge", icon="EDGESEL", text="") + else: + layout = self.layout + + row = layout.row() + row.label(text="Edge Tools:", icon="EDGESEL") + row.menu("VIEW3D_MT_edit_mesh_edgetools", icon="GRID") + + row = layout.split(0.8, align=True) + row.operator("mesh.fillet_plus", text="Fillet plus") + + prop = row.operator("mesh.extra_tools_help", icon="LAYER_USED") + prop.help_ids = "mesh_filletplus" + prop.popup_size = 400 + + row = layout.split(0.8, align=True) + row.operator("mesh.offset_edges", text="Offset Edges") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_offset_edges" + + row = layout.split(0.8, align=True) + row.operator("mesh.edge_roundifier", text="Roundify") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_edge_roundifier" + + row = layout.split(0.8, align=True) + row.operator("object.mesh_edge_length_set", text="Set Edge Length") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_edges_length" + + row = layout.split(0.8, align=True) + row.operator("bpt.mesh_to_wall", text="Edge(s) to Wall") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_to_wall" + + # Face options + box1 = self.layout.box() + col = box1.column(align=True) + row = col.row(align=True) + row.prop(scene.mesh_extra_tools, "UiTabDrop", text="Face", index=2, icon=icon_active_2) + + if not FACEDROP: + row.menu("mesh.face_select_tools", icon="RESTRICT_SELECT_OFF", text="") + row.menu("VIEW3D_MT_Select_Face", icon="FACESEL", text="") + else: + layout = self.layout + + row = layout.row() + row.label(text="Face Tools:", icon="FACESEL") + + row = layout.split(0.8, align=True) + row.operator("object.mextrude", text="Multi Extrude") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_mextrude_plus" + + row = layout.split(0.8, align=True) + row.operator("mesh.extrude_reshape", text="Push/Pull") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_extrude_and_reshape" + + row = layout.split(0.8, align=True) + row.operator("mesh.face_inset_fillet", text="Inset Fillet") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "face_inset_fillet" + + row = layout.split(0.8, align=True) + row.operator("mesh.ext_cut_faces", text="Cut Faces") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_cut_faces" + + row = layout.split(0.8, align=True) + row.operator("mesh.split_solidify", text="Split Solidify") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "split_solidify" + + # Utils options + box1 = self.layout.box() + col = box1.column(align=True) + row = col.row(align=True) + row.prop(scene.mesh_extra_tools, "UiTabDrop", text="Utils", index=3, icon=icon_active_3) + + if not UTILSDROP: + row.menu("mesh.utils specials", icon="SOLO_OFF", text="") + row.menu("VIEW3D_MT_Edit_MultiMET", icon="LOOPSEL", text="") + else: + layout = self.layout + + row = layout.row() + row.label(text="Utilities:") + + row = layout.row() + row = layout.split(0.8, align=True) + row.operator("object_ot.fastloop", text="Fast Loop") + + prop = row.operator("mesh.extra_tools_help", icon="LAYER_USED") + prop.help_ids = "mesh_fastloop" + prop.popup_size = 400 + + row = layout.row() + row.operator("mesh.flip_normals", text="Normals Flip") + + row = layout.row() + row.operator("mesh.remove_doubles", text="Remove Doubles") + + row = layout.row() + row.operator("mesh.subdivide", text="Subdivide") + + row = layout.row() + row.operator("mesh.dissolve_limited", text="Dissolve Limited") + + row = layout.row(align=True) + row.operator("mesh.select_vert_edge_face_index", + icon="VERTEXSEL", text="Vert Index").select_type = 'VERT' + row.operator("mesh.select_vert_edge_face_index", + icon="EDGESEL", text="Edge Index").select_type = 'EDGE' + row.operator("mesh.select_vert_edge_face_index", + icon="FACESEL", text="Face Index").select_type = 'FACE' + + +# ********** Edit Multiselect ********** +class VIEW3D_MT_Edit_MultiMET(Menu): + bl_label = "Multi Select" + bl_description = "Multi Select Modes" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + prop = layout.operator("wm.context_set_value", + text="Vertex Select", + icon='VERTEXSEL') + prop.value = "(True, False, False)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Edge Select", + icon='EDGESEL') + prop.value = "(False, True, False)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Face Select", + icon='FACESEL') + prop.value = "(False, False, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + layout.separator() + + prop = layout.operator("wm.context_set_value", + text="Vertex and Edge Select", + icon='EDITMODE_HLT') + prop.value = "(True, True, False)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Vertex and Face Select", + icon='ORTHO') + prop.value = "(True, False, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Edge and Face Select", + icon='SNAP_FACE') + prop.value = "(False, True, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Vertex, Edge and Face Select", + icon='SNAP_VOLUME') + prop.value = "(True, True, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + +# Select Tools +class VIEW3D_MT_Select_Vert(Menu): + bl_label = "Select Vert" + bl_description = "Vertex Selection Modes" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + prop = layout.operator("wm.context_set_value", + text="Vertex Select", + icon='VERTEXSEL') + prop.value = "(True, False, False)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Vertex and Edge Select", + icon='EDITMODE_HLT') + prop.value = "(True, True, False)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Vertex and Face Select", + icon='ORTHO') + prop.value = "(True, False, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + +class VIEW3D_MT_Select_Edge(Menu): + bl_label = "Select Edge" + bl_description = "Edge Selection Modes" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + prop = layout.operator("wm.context_set_value", + text="Edge Select", + icon='EDGESEL') + prop.value = "(False, True, False)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Vertex and Edge Select", + icon='EDITMODE_HLT') + prop.value = "(True, True, False)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Edge and Face Select", + icon='SNAP_FACE') + prop.value = "(False, True, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + +class VIEW3D_MT_Select_Face(Menu): + bl_label = "Select Face" + bl_description = "Face Selection Modes" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + prop = layout.operator("wm.context_set_value", + text="Face Select", + icon='FACESEL') + prop.value = "(False, False, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Vertex and Face Select", + icon='ORTHO') + prop.value = "(True, False, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + prop = layout.operator("wm.context_set_value", + text="Edge and Face Select", + icon='SNAP_FACE') + prop.value = "(False, True, True)" + prop.data_path = "tool_settings.mesh_select_mode" + + +class VIEW3D_MT_selectface_edit_mesh_add(Menu): + bl_label = "Select by Face" + bl_idname = "mesh.face_select_tools" + bl_description = "Face Selection Tools" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.label(text="Face Selection Tools", icon="RESTRICT_SELECT_OFF") + layout.separator() + + layout.operator("mesh.select_all").action = 'TOGGLE' + layout.operator("mesh.select_all", text="Inverse").action = 'INVERT' + layout.operator("mesh.ext_deselect_boundary", text="Deselect Boundary") + layout.separator() + + layout.operator("data.facetype_select", text="Triangles").face_type = "3" + layout.operator("data.facetype_select", text="Quads").face_type = "4" + layout.operator("data.facetype_select", text="Ngons").face_type = "5" + layout.separator() + + layout.operator("mesh.select_vert_edge_face_index", + text="By Face Index").select_type = 'FACE' + layout.operator("mesh.select_by_direction", text="By Direction") + layout.operator("mesh.select_by_pi", text="By Pi or e") + layout.operator("mesh.select_connected_faces", text="By Connected Faces") + layout.operator("mesh.conway", text="By Conway's game of life") + layout.separator() + + layout.operator("mesh.e2e_efe", text="Neighbors by Face") + layout.operator("mesh.f2f_fvnef", text="Neighbors by Vert not Edge") + + +class VIEW3D_MT_selectedge_edit_mesh_add(Menu): + bl_label = "Select by Edge" + bl_idname = "mesh.edge_select_tools" + bl_description = "Edge Selection Tools" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.label(text="Edge Selection Tools", icon="RESTRICT_SELECT_OFF") + layout.separator() + + layout.operator("mesh.select_all").action = 'TOGGLE' + layout.operator("mesh.select_all", text="Inverse").action = 'INVERT' + layout.separator() + + layout.operator("mesh.select_vert_edge_face_index", + text="By Edge Index").select_type = 'EDGE' + layout.operator("mesh.select_by_direction", text="By Direction") + layout.operator("mesh.select_by_pi", text="By Pi or e") + layout.operator("mesh.select_by_edge_length", text="By Edge Length") + layout.separator() + + layout.operator("mesh.e2e_eve", text="Neighbors by Vertex") + layout.operator("mesh.e2e_evfe", text="Neighbors by Vertex and Face") + layout.operator("mesh.e2e_efnve", text="Lateral Neighbors") + layout.operator("mesh.e2e_evnfe", text="Longitudinal Edges") + + +class VIEW3D_MT_selectvert_edit_mesh_add(Menu): + bl_label = "Select by Vert" + bl_idname = "mesh.vert_select_tools" + bl_description = "Vertex Selection Tools" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.label(text="Vertex Selection Tools", icon="RESTRICT_SELECT_OFF") + layout.separator() + + layout.operator("mesh.select_all").action = 'TOGGLE' + layout.operator("mesh.select_all", text="Inverse").action = 'INVERT' + layout.separator() + + layout.operator("mesh.select_vert_edge_face_index", + text="By Vert Index").select_type = 'VERT' + layout.operator("mesh.select_by_direction", text="By Direction") + layout.operator("mesh.select_by_pi", text="By Pi or e") + layout.separator() + + layout.operator("mesh.v2v_by_edge", text="Neighbors by Edge") + layout.operator("mesh.e2e_eve", text="Neighbors by Vertex") + layout.operator("mesh.e2e_efe", text="Neighbors by Face") + layout.operator("mesh.v2v_facewise", text="Neighbors by Face - Edge") + + +class VIEW3D_MT_utils_specials(Menu): + bl_label = "Specials Menu" + bl_idname = "mesh.utils specials" + bl_description = "Utils Quick Specials" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.label(text="Fast Specials") + layout.separator() + + layout.menu("VIEW3D_MT_edit_mesh_clean") + layout.separator() + + layout.operator("mesh.subdivide", text="Subdivide").smoothness = 0.0 + layout.operator("mesh.merge", text="Merge...") + layout.operator("mesh.remove_doubles") + layout.operator("mesh.inset") + layout.operator("mesh.bevel", text="Bevel") + layout.operator("mesh.bridge_edge_loops") + layout.separator() + + layout.operator("mesh.normals_make_consistent", + text="Recalculate Outside").inside = False + layout.operator("mesh.normals_make_consistent", + text="Recalculate Inside").inside = True + layout.operator("mesh.flip_normals") + + +# Define the "Extras" Menu append +class VIEW3D_MT_edit_mesh_all(Menu): + bl_idname = "VIEW3D_MT_edit_mesh_all" + bl_label = "Mesh Edit Tools" + + def draw(self, context): + layout = self.layout + + layout.menu("VIEW3D_MT_edit_mesh_extras") + layout.menu("VIEW3D_MT_edit_mesh_edgetools") + + +def menu_func(self, context): + self.layout.menu("VIEW3D_MT_edit_mesh_extras") + self.layout.menu("VIEW3D_MT_edit_mesh_edgetools") + + +# Define "Select" Menu append +def menu_select(self, context): + if context.tool_settings.mesh_select_mode[2]: + self.layout.menu("mesh.face_select_tools", icon="FACESEL") + if context.tool_settings.mesh_select_mode[1]: + self.layout.menu("mesh.edge_select_tools", icon="EDGESEL") + if context.tool_settings.mesh_select_mode[0]: + self.layout.menu("mesh.vert_select_tools", icon="VERTEXSEL") + + +# Scene Properties +class MeshExtraToolsSceneProps(PropertyGroup): + # Define the UI drop down prop + UiTabDrop = BoolVectorProperty( + name="Tab", + description="Expand/Collapse UI elements", + default=(False,) * 4, + size=4, + ) + + +# Add-on Preferences +class mesh_extra_tools_pref(AddonPreferences): + bl_idname = __name__ + + show_info = BoolProperty( + name="Info", + default=False, + description="Some general information about the add-on", + ) + show_shortcuts = BoolProperty( + name="Hot Keys", + default=False, + description="List of the shortcuts used for the included various tools", + ) + + def draw(self, context): + layout = self.layout + box = layout.box() + + box.prop(self, "show_info", icon="INFO") + if self.show_info: + box.label(text="Collection of various extra Mesh Edit Functions", + icon="LAYER_ACTIVE") + box.label("The majority of the tools can be found in" + "Mesh Edit Mode Toolshelf or W key Specials Menu", + icon="LAYER_USED") + box.label("The Pen tool is a separate Panel in the Toolshelf", + icon="LAYER_USED") + box.label("The Face Extrude tool is only available in Object Mode " + "as a separate panel in the Toolshelf", + icon="LAYER_USED") + box.label("Face Info / Select is a separate Panel located in Properties > Data Editor", + icon="LAYER_USED") + + box.prop(self, "show_shortcuts", icon="KEYINGSET") + if self.show_shortcuts: + col = box.column() + col.label(text="Double Right Click in Edit mode in the 3D Viewport", + icon="LAYER_ACTIVE") + col.label("Used for quick access to the Vertex, Edge and Face context menus", + icon="LAYER_USED") + col.separator() + col.label(text="W-key in Edit Mode in the 3D Viewport", + icon="LAYER_ACTIVE") + col.label("Tools are grouped into menus prepended to the Specials Menu", + icon="LAYER_USED") + col.separator() + col.label(text="Ctrl+D in Edit Mode in the 3D Viewport", + icon="LAYER_ACTIVE") + col.label("Used by the Pen Tool to start drawing. When activated:", + icon="LAYER_USED") + col.label("Shift + Mouse Move is used to draw along the X axis", + icon="LAYER_USED") + col.label("Alt + Mouse Move is used to draw along the Y axis", + icon="LAYER_USED") + col.separator() + col.label(text="Note: when using Fast Loop operator, press Esc twice to finish", + icon="LAYER_ACTIVE") + + +def register(): + mesh_pen_tool.register() + vfe_specials.register() + mesh_extrude_and_reshape.register() + bpy.utils.register_module(__name__) + + # Register Scene Properties + bpy.types.Scene.mesh_extra_tools = PointerProperty( + type=MeshExtraToolsSceneProps + ) + # Used in mesh_selection_topokit to store cache selection data + bpy.types.Object.tkkey = IntVectorProperty(size=4) + + # Add "Extras" menu to the "W-key Specials" menu + bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func) + bpy.types.VIEW3D_MT_select_edit_mesh.prepend(menu_select) + + try: + bpy.types.VIEW3D_MT_Select_Edit_Mesh.prepend(menu_select) + except: + pass + + +def unregister(): + mesh_pen_tool.unregister() + vfe_specials.unregister() + mesh_extrude_and_reshape.unregister() + + del bpy.types.Scene.mesh_extra_tools + del bpy.types.Object.tkkey + + bpy.utils.unregister_module(__name__) + + # Remove "Extras" menu from the "" menu. + bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func) + bpy.types.VIEW3D_MT_select_edit_mesh.remove(menu_select) + + try: + bpy.types.VIEW3D_MT_Select_Edit_Mesh.remove(menu_select) + except: + pass + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/face_inset_fillet.py b/mesh_extra_tools/face_inset_fillet.py new file mode 100644 index 00000000..fc758b7c --- /dev/null +++ b/mesh_extra_tools/face_inset_fillet.py @@ -0,0 +1,323 @@ +# -*- coding: utf-8 -*- + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# based completely on addon by zmj100 +# added some distance limits to prevent overlap - max12345 + + +import bpy +import bmesh +from bpy.types import Operator +from bpy.props import ( + FloatProperty, + IntProperty, + BoolProperty, + EnumProperty, + ) +from math import tan, cos, degrees, radians, sin +from mathutils import Matrix + + +def edit_mode_out(): + bpy.ops.object.mode_set(mode='OBJECT') + + +def edit_mode_in(): + bpy.ops.object.mode_set(mode='EDIT') + + +def angle_rotation(rp, q, axis, angle): + # returns the vector made by the rotation of the vector q + # rp by angle around axis and then adds rp + + return (Matrix.Rotation(angle, 3, axis) * (q - rp)) + rp + + +def face_inset_fillet(bme, face_index_list, inset_amount, distance, + number_of_sides, out, radius, type_enum, kp): + list_del = [] + + for faceindex in face_index_list: + + bme.faces.ensure_lookup_table() + # loops through the faces... + f = bme.faces[faceindex] + f.select_set(0) + list_del.append(f) + f.normal_update() + vertex_index_list = [v.index for v in f.verts] + dict_0 = {} + orientation_vertex_list = [] + n = len(vertex_index_list) + for i in range(n): + # loops through the vertices + dict_0[i] = [] + bme.verts.ensure_lookup_table() + p = (bme.verts[vertex_index_list[i]].co).copy() + p1 = (bme.verts[vertex_index_list[(i - 1) % n]].co).copy() + p2 = (bme.verts[vertex_index_list[(i + 1) % n]].co).copy() + # copies some vert coordinates, always the 3 around i + dict_0[i].append(bme.verts[vertex_index_list[i]]) + # appends the bmesh vert of the appropriate index to the dict + vec1 = p - p1 + vec2 = p - p2 + # vectors for the other corner points to the cornerpoint + # corresponding to i / p + angle = vec1.angle(vec2) + + adj = inset_amount / tan(angle * 0.5) + h = (adj ** 2 + inset_amount ** 2) ** 0.5 + if round(degrees(angle)) == 180 or round(degrees(angle)) == 0.0: + # if the corner is a straight line... + # I think this creates some new points... + if out is True: + val = ((f.normal).normalized() * inset_amount) + else: + val = -((f.normal).normalized() * inset_amount) + p6 = angle_rotation(p, p + val, vec1, radians(90)) + else: + # if the corner is an actual corner + val = ((f.normal).normalized() * h) + if out is True: + # this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik... + p6 = angle_rotation(p, p + val, -(p - (vec2.normalized() * adj)), -radians(90)) + else: + p6 = angle_rotation(p, p - val, + ((p - (vec1.normalized() * adj)) - (p - (vec2.normalized() * adj))), + -radians(90)) + + orientation_vertex_list.append(p6) + + new_inner_face = [] + orientation_vertex_list_length = len(orientation_vertex_list) + ovll = orientation_vertex_list_length + + for j in range(ovll): + q = orientation_vertex_list[j] + q1 = orientation_vertex_list[(j - 1) % ovll] + q2 = orientation_vertex_list[(j + 1) % ovll] + # again, these are just vectors between somewhat displaced corner vertices + vec1_ = q - q1 + vec2_ = q - q2 + ang_ = vec1_.angle(vec2_) + + # the angle between them + if round(degrees(ang_)) == 180 or round(degrees(ang_)) == 0.0: + # again... if it's really a line... + v = bme.verts.new(q) + new_inner_face.append(v) + dict_0[j].append(v) + else: + # s.a. + if radius is False: + h_ = distance * (1 / cos(ang_ * 0.5)) + d = distance + elif radius is True: + h_ = distance / sin(ang_ * 0.5) + d = distance / tan(ang_ * 0.5) + # max(d) is vec1_.magnitude * 0.5 + # or vec2_.magnitude * 0.5 respectively + + # only functional difference v + if d > vec1_.magnitude * 0.5: + d = vec1_.magnitude * 0.5 + + if d > vec2_.magnitude * 0.5: + d = vec2_.magnitude * 0.5 + # only functional difference ^ + + q3 = q - (vec1_.normalized() * d) + q4 = q - (vec2_.normalized() * d) + # these are new verts somewhat offset from the corners + rp_ = q - ((q - ((q3 + q4) * 0.5)).normalized() * h_) + # reference point inside the curvature + axis_ = vec1_.cross(vec2_) + # this should really be just the face normal + vec3_ = rp_ - q3 + vec4_ = rp_ - q4 + rot_ang = vec3_.angle(vec4_) + cornerverts = [] + + for o in range(number_of_sides + 1): + # this calculates the actual new vertices + q5 = angle_rotation(rp_, q4, axis_, rot_ang * o / number_of_sides) + v = bme.verts.new(q5) + + # creates new bmesh vertices from it + bme.verts.index_update() + + dict_0[j].append(v) + cornerverts.append(v) + + cornerverts.reverse() + new_inner_face.extend(cornerverts) + + if out is False: + f = bme.faces.new(new_inner_face) + f.select_set(True) + elif out is True and kp is True: + f = bme.faces.new(new_inner_face) + f.select_set(True) + + n2_ = len(dict_0) + # these are the new side faces, those that don't depend on cornertype + for o in range(n2_): + list_a = dict_0[o] + list_b = dict_0[(o + 1) % n2_] + bme.faces.new([list_a[0], list_b[0], list_b[-1], list_a[1]]) + bme.faces.index_update() + # cornertype 1 - ngon faces + if type_enum == 'opt0': + for k in dict_0: + if len(dict_0[k]) > 2: + bme.faces.new(dict_0[k]) + bme.faces.index_update() + # cornertype 2 - triangulated faces + if type_enum == 'opt1': + for k_ in dict_0: + q_ = dict_0[k_][0] + dict_0[k_].pop(0) + n3_ = len(dict_0[k_]) + for kk in range(n3_ - 1): + bme.faces.new([dict_0[k_][kk], dict_0[k_][(kk + 1) % n3_], q_]) + bme.faces.index_update() + + del_ = [bme.faces.remove(f) for f in list_del] + if del_: + del del_ + + +# Operator + +class MESH_OT_face_inset_fillet(Operator): + bl_idname = "mesh.face_inset_fillet" + bl_label = "Face Inset Fillet" + bl_description = ("Inset selected and Fillet (make round) the corners of\n" + "of the newly created Faces") + bl_options = {"REGISTER", "UNDO"} + + # inset amount + inset_amount = FloatProperty( + name="Inset amount", + description="Define the size of the Inset relative to the selection", + default=0.04, + min=0, max=100.0, + step=1, + precision=3 + ) + # number of sides + number_of_sides = IntProperty( + name="Number of sides", + description="Define the roundness of the corners by specifying\n" + "the subdivision count", + default=4, + min=1, max=100, + step=1 + ) + distance = FloatProperty( + name="", + description="Use distance or radius for corners' size calculation", + default=0.04, + min=0.00001, max=100.0, + step=1, + precision=3 + ) + out = BoolProperty( + name="Outside", + description="Inset the Faces outwards in relation to the selection\n" + "Note: depending on the geometry, can give unsatisfactory results", + default=False + ) + radius = BoolProperty( + name="Radius", + description="Use radius for corners' size calculation", + default=False + ) + type_enum = EnumProperty( + items=(('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"), + ('opt1', "Triangle", "Triangulate corners")), + name="Corner Type", + default="opt0" + ) + kp = BoolProperty( + name="Keep faces", + description="Do not delete the inside Faces\n" + "Only available if the Out option is checked", + default=False + ) + + def draw(self, context): + layout = self.layout + + layout.label("Corner Type:") + + row = layout.row() + row.prop(self, "type_enum", text="") + + row = layout.row(align=True) + row.prop(self, "out") + + if self.out is True: + row.prop(self, "kp") + + row = layout.row() + row.prop(self, "inset_amount") + + row = layout.row() + row.prop(self, "number_of_sides") + + row = layout.row() + row.prop(self, "radius") + + row = layout.row() + dist_rad = "Radius" if self.radius else "Distance" + row.prop(self, "distance", text=dist_rad) + + def execute(self, context): + # this really just prepares everything for the main function + inset_amount = self.inset_amount + number_of_sides = self.number_of_sides + distance = self.distance + out = self.out + radius = self.radius + type_enum = self.type_enum + kp = self.kp + + edit_mode_out() + ob_act = context.active_object + bme = bmesh.new() + bme.from_mesh(ob_act.data) + # this + face_index_list = [f.index for f in bme.faces if f.select and f.is_valid] + + if len(face_index_list) == 0: + self.report({'WARNING'}, "No suitable Face selection found. Operation cancelled") + edit_mode_in() + return {'CANCELLED'} + + elif len(face_index_list) != 0: + face_inset_fillet(bme, face_index_list, + inset_amount, distance, number_of_sides, + out, radius, type_enum, kp) + + bme.to_mesh(ob_act.data) + edit_mode_in() + + return {'FINISHED'} diff --git a/mesh_extra_tools/mesh_cut_faces.py b/mesh_extra_tools/mesh_cut_faces.py new file mode 100644 index 00000000..6674053e --- /dev/null +++ b/mesh_extra_tools/mesh_cut_faces.py @@ -0,0 +1,265 @@ +# gpl author: Stanislav Blinov + +bl_info = { + "name": "Cut Faces", + "author": "Stanislav Blinov", + "version": (1, 0, 0), + "blender": (2, 72, 0), + "description": "Cut Faces and Deselect Boundary operators", + "category": "Mesh", } + +import bpy +import bmesh + +from bpy.types import Operator +from bpy.props import ( + BoolProperty, + IntProperty, + EnumProperty, + ) + + +def bmesh_from_object(object): + mesh = object.data + if object.mode == 'EDIT': + bm = bmesh.from_edit_mesh(mesh) + else: + bm = bmesh.new() + bm.from_mesh(mesh) + return bm + + +def bmesh_release(bm, object): + mesh = object.data + bm.select_flush_mode() + if object.mode == 'EDIT': + bmesh.update_edit_mesh(mesh, True) + else: + bm.to_mesh(mesh) + bm.free() + + +def calc_face(face, keep_caps=True): + + assert face.tag + + def radial_loops(loop): + next = loop.link_loop_radial_next + while next != loop: + result, next = next, next.link_loop_radial_next + yield result + + result = [] + + face.tag = False + selected = [] + to_select = [] + for loop in face.loops: + self_selected = False + # Iterate over selected adjacent faces + for radial_loop in filter(lambda l: l.face.select, radial_loops(loop)): + # Tag the edge if no other face done so already + if not loop.edge.tag: + loop.edge.tag = True + self_selected = True + + adjacent_face = radial_loop.face + # Only walk adjacent face if current face tagged the edge + if adjacent_face.tag and self_selected: + result += calc_face(adjacent_face, keep_caps) + + if loop.edge.tag: + (selected, to_select)[self_selected].append(loop) + + for loop in to_select: + result.append(loop.edge) + selected.append(loop) + + # Select opposite edge in quads + if keep_caps and len(selected) == 1 and len(face.verts) == 4: + result.append(selected[0].link_loop_next.link_loop_next.edge) + + return result + + +def get_edge_rings(bm, keep_caps=True): + + def tag_face(face): + if face.select: + face.tag = True + for edge in face.edges: + edge.tag = False + return face.select + + # fetch selected faces while setting up tags + selected_faces = [f for f in bm.faces if tag_face(f)] + + edges = [] + + try: + # generate a list of edges to select: + # traversing only tagged faces, since calc_face can walk and untag islands + for face in filter(lambda f: f.tag, selected_faces): edges += calc_face(face, keep_caps) + finally: + # housekeeping: clear tags + for face in selected_faces: + face.tag = False + for edge in face.edges: + edge.tag = False + + return edges + + +class MESH_xOT_deselect_boundary(Operator): + bl_idname = "mesh.ext_deselect_boundary" + bl_label = "Deselect Boundary" + bl_description = ("Deselect boundary edges of selected faces\n" + "Note: if all Faces are selected there is no boundary,\n" + "so the tool will not have results") + bl_options = {'REGISTER', 'UNDO'} + + keep_cap_edges = BoolProperty( + name="Keep Cap Edges", + description="Keep quad strip cap edges selected", + default=False + ) + + @classmethod + def poll(cls, context): + active_object = context.active_object + return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT' + + def execute(self, context): + object = context.active_object + bm = bmesh_from_object(object) + + try: + edges = get_edge_rings(bm, keep_caps=self.keep_cap_edges) + if not edges: + self.report({'WARNING'}, "No suitable Face selection found. Operation cancelled") + return {'CANCELLED'} + + bpy.ops.mesh.select_all(action='DESELECT') + bm.select_mode = {'EDGE'} + + for edge in edges: + edge.select = True + context.tool_settings.mesh_select_mode[:] = False, True, False + + finally: + bmesh_release(bm, object) + + return {'FINISHED'} + + +class MESH_xOT_cut_faces(Operator): + bl_idname = "mesh.ext_cut_faces" + bl_label = "Cut Faces" + bl_description = "Cut selected faces, connected through their adjacent edges" + bl_options = {'REGISTER', 'UNDO'} + + # from bmesh_operators.h + SUBD_INNERVERT = 0 + SUBD_PATH = 1 + SUBD_FAN = 2 + SUBD_STRAIGHT_CUT = 3 + + num_cuts = IntProperty( + name="Number of Cuts", + default=1, + min=1, + max=100, + subtype='UNSIGNED' + ) + use_single_edge = BoolProperty( + name="Quad/Tri Mode", + description="Cut boundary faces", + default=False + ) + corner_type = EnumProperty( + items=[('SUBD_INNERVERT', "Inner Vert", ""), + ('SUBD_PATH', "Path", ""), + ('SUBD_FAN', "Fan", ""), + ('SUBD_STRAIGHT_CUT', "Straight Cut", ""), + ], + name="Quad Corner Type", + description="How to subdivide quad corners", + default='SUBD_STRAIGHT_CUT' + ) + use_grid_fill = BoolProperty( + name="Use Grid Fill", + description="Fill fully enclosed faces with a grid", + default=True + ) + + @classmethod + def poll(cls, context): + active_object = context.active_object + return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT' + + def draw(self, context): + layout = self.layout + + layout.label("Number of Cuts:") + layout.prop(self, "num_cuts", text="") + + layout.prop(self, "use_single_edge") + layout.prop(self, "use_grid_fill") + + layout.label("Quad Corner Type:") + layout.prop(self, "corner_type", text="") + + def cut_edges(self, context): + object = context.active_object + bm = bmesh_from_object(object) + + try: + edges = get_edge_rings(bm, keep_caps=True) + if not edges: + self.report({'WARNING'}, "No suitable Face selection found. Operation cancelled") + return False + + result = bmesh.ops.subdivide_edges( + bm, + edges=edges, + cuts=int(self.num_cuts), + use_grid_fill=bool(self.use_grid_fill), + use_single_edge=bool(self.use_single_edge), + quad_corner_type=eval("self." + self.corner_type)) + + bpy.ops.mesh.select_all(action='DESELECT') + bm.select_mode = {'EDGE'} + + inner = result['geom_inner'] + for edge in filter(lambda e: isinstance(e, bmesh.types.BMEdge), inner): + edge.select = True + + finally: + bmesh_release(bm, object) + + return True + + def execute(self, context): + + if not self.cut_edges(context): + return {'CANCELLED'} + + context.tool_settings.mesh_select_mode[:] = False, True, False + # Try to select all possible loops + bpy.ops.mesh.loop_multi_select(ring=False) + + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(MESH_xOT_deselect_boundary) + bpy.utils.register_class(MESH_xOT_cut_faces) + + +def unregister(): + bpy.utils.unregister_class(MESH_xOT_deselect_boundary) + bpy.utils.unregister_class(MESH_xOT_cut_faces) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_edge_roundifier.py b/mesh_extra_tools/mesh_edge_roundifier.py new file mode 100644 index 00000000..52f7cadf --- /dev/null +++ b/mesh_extra_tools/mesh_edge_roundifier.py @@ -0,0 +1,1315 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +bl_info = { + "name": "Edge Roundifier", + "category": "Mesh", + "author": "Piotr Komisarczyk (komi3D), PKHG", + "version": (1, 0, 0), + "blender": (2, 7, 3), + "location": "SPACE > Edge Roundifier or CTRL-E > Edge Roundifier or Tools > Addons > Edge Roundifier", + "description": "Mesh editing script allowing edge rounding", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh" +} + +import bpy +import bmesh +from bpy.types import Operator +from bpy.props import ( + BoolProperty, + FloatProperty, + EnumProperty, + IntProperty, + ) + +from math import sqrt, acos, pi, radians, degrees, sin +from mathutils import Vector, Euler, Quaternion + +# CONSTANTS +two_pi = 2 * pi +XY = "XY" +XZ = "XZ" +YZ = "YZ" +SPIN_END_THRESHOLD = 0.001 +LINE_TOLERANCE = 0.0001 +d_XABS_YABS = False +d_Edge_Info = False +d_Plane = False +d_Radius_Angle = False +d_Roots = False +d_RefObject = False +d_LineAB = False +d_Selected_edges = False +d_Rotate_Around_Spin_Center = False + +# Enable debug prints +DEBUG = False + + +# for debugging PKHG # +def debugPrintNew(debugs, *text): + if DEBUG and debugs: + tmp = [el for el in text] + for row in tmp: + print(row) + + +# Geometry and math calcualtion methods # + +class CalculationHelper: + + def __init__(self): + """ + Constructor + """ + + def getLineCoefficientsPerpendicularToVectorInPoint(self, point, vector, plane): + x, y, z = point + xVector, yVector, zVector = vector + destinationPoint = (x + yVector, y - xVector, z) + if plane == 'YZ': + destinationPoint = (x, y + zVector, z - yVector) + if plane == 'XZ': + destinationPoint = (x + zVector, y, z - xVector) + return self.getCoefficientsForLineThrough2Points(point, destinationPoint, plane) + + def getQuadraticRoots(self, coef): + if len(coef) != 3: + return NaN # TODO lijenstina: Is this a valid return? + else: + a, b, c = coef + delta = b ** 2 - 4 * a * c + if delta == 0: + x = -b / (2 * a) + return (x, x) + elif delta < 0: + return None + else: + x1 = (-b - sqrt(delta)) / (2 * a) + x2 = (-b + sqrt(delta)) / (2 * a) + return (x1, x2) + + def getCoefficientsForLineThrough2Points(self, point1, point2, plane): + x1, y1, z1 = point1 + x2, y2, z2 = point2 + + # mapping x1,x2, y1,y2 to proper values based on plane + if plane == YZ: + x1 = y1 + x2 = y2 + y1 = z1 + y2 = z2 + if plane == XZ: + y1 = z1 + y2 = z2 + + # Further calculations the same as for XY plane + xabs = abs(x2 - x1) + yabs = abs(y2 - y1) + debugPrintNew(d_XABS_YABS, "XABS = " + str(xabs) + " YABS = " + str(yabs)) + + if xabs <= LINE_TOLERANCE: + return None # this means line x = edgeCenterX + if yabs <= LINE_TOLERANCE: + A = 0 + B = y1 + return A, B + A = (y2 - y1) / (x2 - x1) + B = y1 - (A * x1) + return (A, B) + + def getLineCircleIntersections(self, lineAB, circleMidPoint, radius): + # (x - a)**2 + (y - b)**2 = r**2 - circle equation + # y = A*x + B - line equation + # f * x**2 + g * x + h = 0 - quadratic equation + A, B = lineAB + a, b = circleMidPoint + f = 1 + (A ** 2) + g = -2 * a + 2 * A * B - 2 * A * b + h = (B ** 2) - 2 * b * B - (radius ** 2) + (a ** 2) + (b ** 2) + coef = [f, g, h] + roots = self.getQuadraticRoots(coef) + if roots is not None: + x1 = roots[0] + x2 = roots[1] + point1 = [x1, A * x1 + B] + point2 = [x2, A * x2 + B] + return [point1, point2] + else: + return None + + def getLineCircleIntersectionsWhenXPerpendicular(self, edgeCenter, circleMidPoint, radius, plane): + # (x - a)**2 + (y - b)**2 = r**2 - circle equation + # x = xValue - line equation + # f * x**2 + g * x + h = 0 - quadratic equation + xValue = edgeCenter[0] + if plane == YZ: + xValue = edgeCenter[1] + if plane == XZ: + xValue = edgeCenter[0] + + a, b = circleMidPoint + f = 1 + g = -2 * b + h = (a ** 2) + (b ** 2) + (xValue ** 2) - 2 * a * xValue - (radius ** 2) + coef = [f, g, h] + roots = self.getQuadraticRoots(coef) + if roots is not None: + y1 = roots[0] + y2 = roots[1] + point1 = [xValue, y1] + point2 = [xValue, y2] + return [point1, point2] + else: + return None + + # point1 is the point near 90 deg angle + def getAngle(self, point1, point2, point3): + distance1 = (Vector(point1) - Vector(point2)).length + distance2 = (Vector(point2) - Vector(point3)).length + cos = distance1 / distance2 + + if abs(cos) > 1: # prevents Domain Error + cos = round(cos) + + alpha = acos(cos) + return (alpha, degrees(alpha)) + + # get two of three coordinates used for further calculation of spin center + # PKHG>nice if rescriction to these 3 types or planes is to be done + # komi3D> from 0.0.2 there is a restriction. In future I would like Edge Roundifier to work on + # komi3D> Normal and View coordinate systems. That would be great + def getCircleMidPointOnPlane(self, V1, plane): + X = V1[0] + Y = V1[1] + if plane == 'XZ': + X = V1[0] + Y = V1[2] + elif plane == 'YZ': + X = V1[1] + Y = V1[2] + return [X, Y] + + def getEdgeReference(self, edge, edgeCenter, plane): + vert1 = edge.verts[1].co + V = vert1 - edgeCenter + orthoVector = Vector((V[1], -V[0], V[2])) + if plane == 'XZ': + orthoVector = Vector((V[2], V[1], -V[0])) + elif plane == 'YZ': + orthoVector = Vector((V[0], V[2], -V[1])) + refPoint = edgeCenter + orthoVector + return refPoint + + +# SELECTION METHODS # + +class SelectionHelper: + + def selectVertexInMesh(self, mesh, vertex): + bpy.ops.object.mode_set(mode="OBJECT") + for v in mesh.vertices: + if v.co == vertex: + v.select = True + break + + bpy.ops.object.mode_set(mode="EDIT") + + def getSelectedVertex(self, mesh): + bpy.ops.object.mode_set(mode="OBJECT") + for v in mesh.vertices: + if v.select is True: + bpy.ops.object.mode_set(mode="EDIT") + return v + + bpy.ops.object.mode_set(mode="EDIT") + return None + + def refreshMesh(self, bm, mesh): + bpy.ops.object.mode_set(mode='OBJECT') + bm.to_mesh(mesh) + bpy.ops.object.mode_set(mode='EDIT') + + +# Operator + +class EdgeRoundifier(Operator): + bl_idname = "mesh.edge_roundifier" + bl_label = "Edge Roundifier" + bl_description = "Mesh modeling tool for building arcs on selected Edges" + bl_options = {'REGISTER', 'UNDO', 'PRESET'} + + threshold = 0.0005 + obj = None + + edgeScaleFactor = FloatProperty( + name="", + description="Set the Factor of scaling", + default=1.0, + min=0.00001, max=100000.0, + step=0.5, + precision=5 + ) + r = FloatProperty( + name="", + description="User Defined arc steepness by a Radius\n" + "Enabled only if Entry mode is set to Radius\n", + default=1, + min=0.00001, max=1000.0, + step=0.1, + precision=3 + ) + a = FloatProperty( + name="", + description="User defined arc steepness calculated from an Angle\n" + "Enabled only if Entry mode is set to Angle and\n" + "Angle presets is set Other", + default=180.0, + min=0.1, max=180.0, + step=0.5, + precision=1 + ) + n = IntProperty( + name="", + description="Arc subdivision level", + default=4, + min=1, max=100, + step=1 + ) + flip = BoolProperty( + name="Flip", + description="If True, flip the side of the selected edges where the arcs are drawn", + default=False + ) + invertAngle = BoolProperty( + name="Invert", + description="If True, uses an inverted angle to draw the arc (360 degrees - angle)", + default=False + ) + fullCircles = BoolProperty( + name="Circles", + description="If True, uses an angle of 360 degrees to draw the arcs", + default=False + ) + bothSides = BoolProperty( + name="Both sides", + description="If True, draw arcs on both sides of the selected edges", + default=False + ) + drawArcCenters = BoolProperty( + name="Centers", + description="If True, draws a vertex for each spin center", + default=False + ) + removeEdges = BoolProperty( + name="Edges", + description="If True removes the Original selected edges", + default=False + ) + removeScaledEdges = BoolProperty( + name="Scaled edges", + description="If True removes the Scaled edges (not part of the arcs)", + default=False + ) + connectArcWithEdge = BoolProperty( + name="Arc - Edge", + description="Connect Arcs to Edges", + default=False + ) + connectArcs = BoolProperty( + name="Arcs", + description="Connect subsequent Arcs", + default=False + ) + connectScaledAndBase = BoolProperty( + name="Scaled - Base Edge", + description="Connect Scaled to Base Edge", + default=False + ) + connectArcsFlip = BoolProperty( + name="Flip Arcs", + description="Flip the connection of subsequent Arcs", + default=False + ) + connectArcWithEdgeFlip = BoolProperty( + name="Flip Arc - Edge", + description="Flip the connection of the Arcs to Edges", + default=False + ) + axisAngle = FloatProperty( + name="", + description="Rotate Arc around the perpendicular axis", + default=0.0, + min=-180.0, max=180.0, + step=0.5, + precision=1 + ) + edgeAngle = FloatProperty( + name="", + description="Rotate Arc around the Edge (Edge acts like as the axis)", + default=0.0, + min=-180.0, max=180.0, + step=0.5, + precision=1 + ) + offset = FloatProperty( + name="", + description="Offset Arc perpendicular the Edge", + default=0.0, + min=-1000000.0, max=1000000.0, + step=0.1, + precision=5 + ) + offset2 = FloatProperty( + name="", + description="Offset Arc in parallel to the Edge", + default=0.0, + min=-1000000.0, max=1000000.0, + step=0.1, + precision=5 + ) + ellipticFactor = FloatProperty( + name="", + description="Make Arc elliptic", + default=0.0, + min=-1000000.0, max=1000000.0, + step=0.1, + precision=5 + ) + workModeItems = [("Normal", "Normal", ""), ("Reset", "Reset", "")] + workMode = EnumProperty( + items=workModeItems, + name="", + default='Normal', + description="Normal work with the current given paramaters set by the user\n" + "Reset - changes back the parameters to their default values" + ) + entryModeItems = [("Radius", "Radius", ""), ("Angle", "Angle", "")] + entryMode = EnumProperty( + items=entryModeItems, + name="", + default='Angle', + description="Entry mode switch between Angle and Radius\n" + "If Angle is selected, arc radius is calculated from it" + ) + rotateCenterItems = [("Spin", "Spin", ""), ("V1", "V1", ""), + ("Edge", "Edge", ""), ("V2", "V2", "")] + rotateCenter = EnumProperty( + items=rotateCenterItems, + name="", + default='Edge', + description="Rotate center for spin axis rotate" + ) + arcModeItems = [("FullEdgeArc", "Full", "Full"), ('HalfEdgeArc', "Half", "Half")] + arcMode = EnumProperty( + items=arcModeItems, + name="", + default='FullEdgeArc', + description="Arc mode - switch between Full and Half arcs" + ) + angleItems = [('Other', "Other", "User defined angle"), ('180', "180", "HemiCircle (2 sides)"), + ('120', "120", "TriangleCircle (3 sides)"), ('90', "90", "QuadCircle (4 sides)"), + ('72', "72", "PentagonCircle (5 sides)"), ('60', "60", "HexagonCircle (6 sides)"), + ('45', "45", "OctagonCircle (8 sides)"), ('30', "30", "DodecagonCircle (12 sides)")] + angleEnum = EnumProperty( + items=angleItems, + name="", + default='180', + description="Presets prepare standard angles and calculate proper ray" + ) + refItems = [('ORG', "Origin", "Use Origin Location"), ('CUR', "3D Cursor", "Use 3DCursor Location"), + ('EDG', "Edge", "Use Individual Edge Reference")] + referenceLocation = EnumProperty( + items=refItems, + name="", + default='ORG', + description="Reference location used to calculate initial centers of drawn arcs" + ) + planeItems = [(XY, "XY", "XY Plane (Z=0)"), + (YZ, "YZ", "YZ Plane (X=0)"), + (XZ, "XZ", "XZ Plane (Y=0)")] + planeEnum = EnumProperty( + items=planeItems, + name="", + default='XY', + description="Plane used to calculate spin plane of drawn arcs" + ) + edgeScaleCenterItems = [('V1', "V1", "v1 - First Edge's Vertex"), + ('CENTER', "Center", "Center of the Edge"), + ('V2', "V2", "v2 - Second Edge's Vertex")] + edgeScaleCenterEnum = EnumProperty( + items=edgeScaleCenterItems, + name="Edge scale center", + default='CENTER', + description="Center used for scaling the initial edge" + ) + + calc = CalculationHelper() + sel = SelectionHelper() + + @classmethod + def poll(cls, context): + return ((context.scene.objects.active.type == 'MESH') and + (context.scene.objects.active.mode == 'EDIT')) + + def prepareMesh(self, context): + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + + mesh = context.scene.objects.active.data + bm = bmesh.new() + bm.from_mesh(mesh) + + edges = [ele for ele in bm.edges if ele.select] + return edges, mesh, bm + + def prepareParameters(self): + parameters = {"a": "a"} + parameters["arcMode"] = self.arcMode + parameters["edgeScaleFactor"] = self.edgeScaleFactor + parameters["edgeScaleCenterEnum"] = self.edgeScaleCenterEnum + parameters["plane"] = self.planeEnum + parameters["radius"] = self.r + parameters["angle"] = self.a + parameters["segments"] = self.n + parameters["fullCircles"] = self.fullCircles + parameters["invertAngle"] = self.invertAngle + parameters["bothSides"] = self.bothSides + parameters["angleEnum"] = self.angleEnum + parameters["entryMode"] = self.entryMode + parameters["workMode"] = self.workMode + parameters["refObject"] = self.referenceLocation + parameters["flip"] = self.flip + parameters["drawArcCenters"] = self.drawArcCenters + parameters["removeEdges"] = self.removeEdges + parameters["removeScaledEdges"] = self.removeScaledEdges + parameters["connectArcWithEdge"] = self.connectArcWithEdge + parameters["connectScaledAndBase"] = self.connectScaledAndBase + parameters["connectArcs"] = self.connectArcs + parameters["connectArcsFlip"] = self.connectArcsFlip + parameters["connectArcWithEdgeFlip"] = self.connectArcWithEdgeFlip + parameters["axisAngle"] = self.axisAngle + parameters["edgeAngle"] = self.edgeAngle + parameters["offset"] = self.offset + parameters["offset2"] = self.offset2 + parameters["ellipticFactor"] = self.ellipticFactor + parameters["rotateCenter"] = self.rotateCenter + return parameters + + def draw(self, context): + layout = self.layout + box = layout.box() + uiPercentage = 0.333 + + self.addEnumParameterToUI(box, False, uiPercentage, 'Mode:', 'workMode') + self.addEnumParameterToUI(box, False, uiPercentage, 'Plane:', 'planeEnum') + self.addEnumParameterToUI(box, False, uiPercentage, 'Reference:', 'referenceLocation') + + box = layout.box() + self.addEnumParameterToUI(box, False, uiPercentage, 'Scale base:', 'edgeScaleCenterEnum') + self.addParameterToUI(box, False, uiPercentage, 'Scale factor:', 'edgeScaleFactor') + + box = layout.box() + self.addEnumParameterToUI(box, False, uiPercentage, 'Entry mode:', 'entryMode') + + row = box.row(align=False) + row.prop(self, 'angleEnum', expand=True, text="Angle presets") + + disable_a = bool(self.entryMode == 'Angle' and self.angleEnum == 'Other') + disable_r = bool(self.entryMode == 'Radius') + + self.addParameterToUI(box, False, uiPercentage, 'Angle:', 'a', disable_a) + self.addParameterToUI(box, False, uiPercentage, 'Radius:', 'r', disable_r) + self.addParameterToUI(box, False, uiPercentage, 'Segments:', 'n') + + box = layout.box() + self.addCheckboxToUI(box, True, 'Options:', 'flip', 'invertAngle') + self.addCheckboxToUI(box, True, '', 'bothSides', 'fullCircles') + self.addCheckboxToUI(box, True, '', 'drawArcCenters') + + box = layout.box() + self.addCheckboxToUI(box, True, 'Remove:', 'removeEdges', 'removeScaledEdges') + + box = layout.box() + self.addCheckboxToUI(box, True, 'Connect:', 'connectArcs', 'connectArcsFlip') + self.addCheckboxToUI(box, True, '', 'connectArcWithEdge', 'connectArcWithEdgeFlip') + self.addCheckboxToUI(box, True, '', 'connectScaledAndBase') + + box = layout.box() + self.addParameterToUI(box, False, uiPercentage, 'Orhto offset:', 'offset') + self.addParameterToUI(box, False, uiPercentage, 'Parallel offset:', 'offset2') + + box = layout.box() + self.addParameterToUI(box, False, uiPercentage, 'Edge rotate :', 'edgeAngle') + self.addEnumParameterToUI(box, False, uiPercentage, 'Axis rotate center:', 'rotateCenter') + self.addParameterToUI(box, False, uiPercentage, 'Axis rotate:', 'axisAngle') + + box = layout.box() + self.addParameterToUI(box, False, uiPercentage, 'Elliptic factor:', 'ellipticFactor') + + def addParameterToUI(self, layout, alignment, percent, label, properties, disable=True): + row = layout.row(align=alignment) + split = row.split(percentage=percent) + col = split.column() + + col.label(label) + col2 = split.column() + row = col2.row(align=alignment) + row.enabled = disable + row.prop(self, properties) + + def addCheckboxToUI(self, layout, alignment, label, property1, property2=None): + if label not in (""): + row = layout.row() + row.label(label) + row2 = layout.row(align=alignment) + if property2: + split = row2.split(percentage=0.5) + split.prop(self, property1, toggle=True) + split.prop(self, property2, toggle=True) + else: + row2.prop(self, property1, toggle=True) + layout.separator() + + def addEnumParameterToUI(self, layout, alignment, percent, label, properties): + row = layout.row(align=alignment) + split = row.split(percentage=percent) + col = split.column() + + col.label(label) + col2 = split.column() + row = col2.row(align=alignment) + row.prop(self, properties, expand=True, text="a") + + def execute(self, context): + + edges, mesh, bm = self.prepareMesh(context) + parameters = self.prepareParameters() + + self.resetValues(parameters["workMode"]) + + self.obj = context.scene.objects.active + scaledEdges = self.scaleDuplicatedEdges(bm, edges, parameters) + + if len(scaledEdges) > 0: + self.roundifyEdges(scaledEdges, parameters, bm, mesh) + + if parameters["connectScaledAndBase"]: + self.connectScaledEdgesWithBaseEdge(scaledEdges, edges, bm, mesh) + + self.sel.refreshMesh(bm, mesh) + self.selectEdgesAfterRoundifier(context, scaledEdges) + else: + debugPrintNew(True, "No edges selected!") + + if parameters["removeEdges"]: + bmesh.ops.delete(bm, geom=edges, context=2) + + if parameters["removeScaledEdges"] and self.edgeScaleFactor != 1.0: + bmesh.ops.delete(bm, geom=scaledEdges, context=2) + + bpy.ops.object.mode_set(mode='OBJECT') + bm.to_mesh(mesh) + bpy.ops.object.mode_set(mode='EDIT') + + bm.free() + return {'FINISHED'} + + def resetValues(self, workMode): + if workMode == "Reset": + self.setAllParamsToDefaults() + + def setAllParamsToDefaults(self): + try: + self.edgeScaleFactor = 1.0 + self.r = 1 + self.a = 180.0 + self.n = 4 + self.flip = False + self.invertAngle = False + self.fullCircles = False + self.bothSides = False + self.drawArcCenters = False + self.removeEdges = False + self.removeScaledEdges = False + + self.connectArcWithEdge = False + self.connectArcs = False + self.connectScaledAndBase = False + self.connectArcsFlip = False + self.connectArcWithEdgeFlip = False + + self.axisAngle = 0.0 + self.edgeAngle = 0.0 + self.offset = 0.0 + self.offset2 = 0.0 + self.ellipticFactor = 0.0 + + self.workMode = 'Normal' + self.entryMode = 'Angle' + self.angleEnum = '180' + self.referenceLocation = 'ORG' + self.planeEnum = 'XY' + self.edgeScaleCenterEnum = 'CENTER' + self.rotateCenter = 'Edge' + + self.report({'INFO'}, "The parameters have been reset to default values") + except Exception as e: + self.report({'WARNING'}, "The parameters could not be reset") + debugPrintNew(True, "\n[setAllParamsToDefaults]\n parameter reset error\n" + e) + + def scaleDuplicatedEdges(self, bm, edges, parameters): + scaleCenter = parameters["edgeScaleCenterEnum"] + factor = parameters["edgeScaleFactor"] + # this code is based on Zeffi's answer to my question + duplicateEdges = [] + if factor == 1: + duplicateEdges = edges + else: + for e in edges: + v1 = e.verts[0].co + v2 = e.verts[1].co + origin = None + if scaleCenter == 'CENTER': + origin = (v1 + v2) * 0.5 + elif scaleCenter == 'V1': + origin = v1 + elif scaleCenter == 'V2': + origin = v2 + + bmv1 = bm.verts.new(((v1 - origin) * factor) + origin) + bmv2 = bm.verts.new(((v2 - origin) * factor) + origin) + bme = bm.edges.new([bmv1, bmv2]) + duplicateEdges.append(bme) + return duplicateEdges + + def roundifyEdges(self, edges, parameters, bm, mesh): + arcs = [] + for e in edges: + arcVerts = self.roundify(e, parameters, bm, mesh) + arcs.append(arcVerts) + + if parameters["connectArcs"]: + self.connectArcsTogether(arcs, bm, mesh, parameters) + + def getNormalizedEdgeVector(self, edge): + V1 = edge.verts[0].co + V2 = edge.verts[1].co + edgeVector = V2 - V1 + normEdge = edgeVector.normalized() + return normEdge + + def getEdgePerpendicularVector(self, edge, plane): + normEdge = self.getNormalizedEdgeVector(edge) + + edgePerpendicularVector = Vector((normEdge[1], -normEdge[0], 0)) + if plane == YZ: + edgePerpendicularVector = Vector((0, normEdge[2], -normEdge[1])) + if plane == XZ: + edgePerpendicularVector = Vector((normEdge[2], 0, -normEdge[0])) + return edgePerpendicularVector + + def getEdgeInfo(self, edge): + V1 = edge.verts[0].co + V2 = edge.verts[1].co + edgeVector = V2 - V1 + edgeLength = edgeVector.length + edgeCenter = (V2 + V1) * 0.5 + return V1, V2, edgeVector, edgeLength, edgeCenter + + def roundify(self, edge, parameters, bm, mesh): + V1, V2, edgeVector, edgeLength, edgeCenter = self.getEdgeInfo(edge) + if self.skipThisEdge(V1, V2, parameters["plane"]): + return + + roundifyParams = None + arcVerts = None + roundifyParams = self.calculateRoundifyParams(edge, parameters, bm, mesh) + if roundifyParams is None: + return + + arcVerts = self.spinAndPostprocess(edge, parameters, bm, mesh, edgeCenter, roundifyParams) + return arcVerts + + def spinAndPostprocess(self, edge, parameters, bm, mesh, edgeCenter, roundifyParams): + spinnedVerts, roundifyParamsUpdated = self.drawSpin(edge, edgeCenter, + roundifyParams, parameters, bm, mesh) + postProcessedArcVerts = self.arcPostprocessing(edge, parameters, bm, mesh, + roundifyParamsUpdated, spinnedVerts, edgeCenter) + return postProcessedArcVerts + + def rotateArcAroundEdge(self, bm, mesh, arcVerts, parameters): + angle = parameters["edgeAngle"] + if angle != 0: + self.arc_rotator(arcVerts, angle, parameters) + + # arc_rotator method was created by PKHG, I (komi3D) adjusted it to fit the rest + def arc_rotator(self, arcVerts, extra_rotation, parameters): + bpy.ops.object.mode_set(mode='OBJECT') + old_location = self.obj.location.copy() + bpy.ops.transform.translate(value=- old_location, constraint_axis=(False, False, False), + constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', + proportional_edit_falloff='SMOOTH', proportional_size=1) + bpy.ops.object.mode_set(mode='EDIT') + adjust_matrix = self.obj.matrix_parent_inverse + bm = bmesh.from_edit_mesh(self.obj.data) + lastVert = len(arcVerts) - 1 + if parameters["drawArcCenters"]: + lastVert = lastVert - 1 # center gets added as last vert of arc + v0_old = adjust_matrix * arcVerts[0].co.copy() + + # PKHG>INFO move if necessary v0 to origin such that the axis gos through origin and v1 + if v0_old != Vector((0, 0, 0)): + for i, ele in enumerate(arcVerts): + arcVerts[i].co += - v0_old + + axis = arcVerts[0].co - arcVerts[lastVert].co + a_mat = Quaternion(axis, radians(extra_rotation)).normalized().to_matrix() + + for ele in arcVerts: + ele.co = a_mat * ele.co + + # PKHG>INFO move back if needed + if v0_old != Vector((0, 0, 0)): + for i, ele in enumerate(arcVerts): + arcVerts[i].co += + v0_old + + bpy.ops.object.mode_set(mode='OBJECT') + # PKHG>INFO move origin object back print("old location = " , old_location) + bpy.ops.transform.translate(value=old_location, constraint_axis=(False, False, False), + constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', + proportional_edit_falloff='SMOOTH', proportional_size=1) + bpy.ops.object.mode_set(mode='EDIT') + + def makeElliptic(self, bm, mesh, arcVertices, parameters): + if parameters["ellipticFactor"] != 0: # if 0 then nothing has to be done + lastVert = len(arcVertices) - 1 + if parameters["drawArcCenters"]: + lastVert = lastVert - 1 # center gets added as last vert of arc + v0co = arcVertices[0].co + v1co = arcVertices[lastVert].co + + for vertex in arcVertices: # range(len(res_list)): + # PKHg>INFO compute the base on the edge of the height-vector + top = vertex.co # res_list[nr].co + t = 0 + if v1co - v0co != 0: + t = (v1co - v0co).dot(top - v0co) / (v1co - v0co).length ** 2 + h_bottom = v0co + t * (v1co - v0co) + height = (h_bottom - top) + vertex.co = top + parameters["ellipticFactor"] * height + + return arcVertices + + def arcPostprocessing(self, edge, parameters, bm, mesh, roundifyParams, spinnedVerts, edgeCenter): + [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] = roundifyParams + rotatedVerts = [] + if parameters["rotateCenter"] == 'Edge': + rotatedVerts = self.rotateArcAroundSpinAxis(bm, mesh, spinnedVerts, parameters, edgeCenter) + elif parameters["rotateCenter"] == 'Spin': + rotatedVerts = self.rotateArcAroundSpinAxis(bm, mesh, spinnedVerts, parameters, chosenSpinCenter) + elif parameters["rotateCenter"] == 'V1': + rotatedVerts = self.rotateArcAroundSpinAxis(bm, mesh, spinnedVerts, parameters, edge.verts[0].co) + elif parameters["rotateCenter"] == 'V2': + rotatedVerts = self.rotateArcAroundSpinAxis(bm, mesh, spinnedVerts, parameters, edge.verts[1].co) + + offsetVerts = self.offsetArcPerpendicular(bm, mesh, rotatedVerts, edge, parameters) + offsetVerts2 = self.offsetArcParallel(bm, mesh, offsetVerts, edge, parameters) + ellipticVerts = self.makeElliptic(bm, mesh, offsetVerts2, parameters) + self.rotateArcAroundEdge(bm, mesh, ellipticVerts, parameters) + + if parameters["connectArcWithEdge"]: + self.connectArcTogetherWithEdge(edge, offsetVerts2, bm, mesh, parameters) + return offsetVerts2 + + def connectArcTogetherWithEdge(self, edge, arcVertices, bm, mesh, parameters): + lastVert = len(arcVertices) - 1 + if parameters["drawArcCenters"]: + lastVert = lastVert - 1 # center gets added as last vert of arc + edgeV1 = edge.verts[0].co + edgeV2 = edge.verts[1].co + arcV1 = arcVertices[0].co + arcV2 = arcVertices[lastVert].co + + bmv1 = bm.verts.new(edgeV1) + bmv2 = bm.verts.new(arcV1) + + bmv3 = bm.verts.new(edgeV2) + bmv4 = bm.verts.new(arcV2) + + if parameters["connectArcWithEdgeFlip"] is False: + bme = bm.edges.new([bmv1, bmv2]) + bme2 = bm.edges.new([bmv3, bmv4]) + else: + bme = bm.edges.new([bmv1, bmv4]) + bme2 = bm.edges.new([bmv3, bmv2]) + self.sel.refreshMesh(bm, mesh) + + def connectScaledEdgesWithBaseEdge(self, scaledEdges, baseEdges, bm, mesh): + for i in range(0, len(scaledEdges)): + scaledEdgeV1 = scaledEdges[i].verts[0].co + baseEdgeV1 = baseEdges[i].verts[0].co + scaledEdgeV2 = scaledEdges[i].verts[1].co + baseEdgeV2 = baseEdges[i].verts[1].co + + bmv1 = bm.verts.new(baseEdgeV1) + bmv2 = bm.verts.new(scaledEdgeV1) + bme = bm.edges.new([bmv1, bmv2]) + + bmv3 = bm.verts.new(scaledEdgeV2) + bmv4 = bm.verts.new(baseEdgeV2) + bme = bm.edges.new([bmv3, bmv4]) + self.sel.refreshMesh(bm, mesh) + + def connectArcsTogether(self, arcs, bm, mesh, parameters): + for i in range(0, len(arcs) - 1): + if arcs[i] is None or arcs[i + 1] is None: # in case on XZ or YZ there are no arcs drawn + return + lastVert = len(arcs[i]) - 1 + if parameters["drawArcCenters"]: + lastVert = lastVert - 1 # center gets added as last vert of arc + # take last vert of arc i and first vert of arc i+1 + + V1 = arcs[i][lastVert].co + V2 = arcs[i + 1][0].co + + if parameters["connectArcsFlip"]: + V1 = arcs[i][0].co + V2 = arcs[i + 1][lastVert].co + + bmv1 = bm.verts.new(V1) + bmv2 = bm.verts.new(V2) + bme = bm.edges.new([bmv1, bmv2]) + + # connect last arc and first one + lastArcId = len(arcs) - 1 + lastVertIdOfLastArc = len(arcs[lastArcId]) - 1 + if parameters["drawArcCenters"]: + lastVertIdOfLastArc = lastVertIdOfLastArc - 1 # center gets added as last vert of arc + V1 = arcs[lastArcId][lastVertIdOfLastArc].co + V2 = arcs[0][0].co + if parameters["connectArcsFlip"]: + V1 = arcs[lastArcId][0].co + V2 = arcs[0][lastVertIdOfLastArc].co + + bmv1 = bm.verts.new(V1) + bmv2 = bm.verts.new(V2) + bme = bm.edges.new([bmv1, bmv2]) + + self.sel.refreshMesh(bm, mesh) + + def offsetArcPerpendicular(self, bm, mesh, Verts, edge, parameters): + perpendicularVector = self.getEdgePerpendicularVector(edge, parameters["plane"]) + offset = parameters["offset"] + translation = offset * perpendicularVector + + try: + bmesh.ops.translate(bm, verts=Verts, vec=translation) + except ValueError: + print("[Edge Roundifier]: Perpendicular translate value error - " + "multiple vertices in list - try unchecking 'Centers'") + + indexes = [v.index for v in Verts] + self.sel.refreshMesh(bm, mesh) + offsetVertices = [bm.verts[i] for i in indexes] + return offsetVertices + + def offsetArcParallel(self, bm, mesh, Verts, edge, parameters): + edgeVector = self.getNormalizedEdgeVector(edge) + offset = parameters["offset2"] + translation = offset * edgeVector + + try: + bmesh.ops.translate(bm, verts=Verts, vec=translation) + except ValueError: + print("[Edge Roundifier]: Parallel translate value error - " + "multiple vertices in list - try unchecking 'Centers'") + + indexes = [v.index for v in Verts] + self.sel.refreshMesh(bm, mesh) + offsetVertices = [bm.verts[i] for i in indexes] + return offsetVertices + + def skipThisEdge(self, V1, V2, plane): + # Check If It is possible to spin selected verts on this plane if not exit roundifier + if(plane == XY): + if (V1[0] == V2[0] and V1[1] == V2[1]): + return True + elif(plane == YZ): + if (V1[1] == V2[1] and V1[2] == V2[2]): + return True + elif(plane == XZ): + if (V1[0] == V2[0] and V1[2] == V2[2]): + return True + return False + + def calculateRoundifyParams(self, edge, parameters, bm, mesh): + # Because all data from mesh is in local coordinates + # and spin operator works on global coordinates + # We first need to translate all input data by vector equal + # to origin position and then perform calculations + # At least that is my understanding :) <komi3D> + + # V1 V2 stores Local Coordinates + V1, V2, edgeVector, edgeLength, edgeCenter = self.getEdgeInfo(edge) + + debugPrintNew(d_Plane, "PLANE: " + parameters["plane"]) + lineAB = self.calc.getLineCoefficientsPerpendicularToVectorInPoint( + edgeCenter, edgeVector, + parameters["plane"] + ) + circleMidPoint = V1 + circleMidPointOnPlane = self.calc.getCircleMidPointOnPlane(V1, parameters["plane"]) + radius = parameters["radius"] + + angle = 0 + if (parameters["entryMode"] == 'Angle'): + if (parameters["angleEnum"] != 'Other'): + radius, angle = self.CalculateRadiusAndAngleForAnglePresets( + parameters["angleEnum"], radius, + angle, edgeLength + ) + else: + radius, angle = self.CalculateRadiusAndAngle(edgeLength) + debugPrintNew(d_Radius_Angle, "RADIUS = " + str(radius) + " ANGLE = " + str(angle)) + roots = None + if angle != pi: # mode other than 180 + if lineAB is None: + roots = self.calc.getLineCircleIntersectionsWhenXPerpendicular( + edgeCenter, circleMidPointOnPlane, + radius, parameters["plane"] + ) + else: + roots = self.calc.getLineCircleIntersections(lineAB, circleMidPointOnPlane, radius) + + if roots is None: + debugPrintNew(True, "[Edge Roundifier]: No centers were found. Change radius to higher value") + return None + roots = self.addMissingCoordinate(roots, V1, parameters["plane"]) # adds X, Y or Z coordinate + else: + roots = [edgeCenter, edgeCenter] + debugPrintNew(d_Roots, "roots=" + str(roots)) + + refObjectLocation = None + objectLocation = bpy.context.active_object.location # Origin Location + + if parameters["refObject"] == "ORG": + refObjectLocation = [0, 0, 0] + elif parameters["refObject"] == "CUR": + refObjectLocation = bpy.context.scene.cursor_location - objectLocation + else: + refObjectLocation = self.calc.getEdgeReference(edge, edgeCenter, parameters["plane"]) + + debugPrintNew(d_RefObject, parameters["refObject"], refObjectLocation) + chosenSpinCenter, otherSpinCenter = self.getSpinCenterClosestToRefCenter(refObjectLocation, roots) + + if (parameters["entryMode"] == "Radius"): + halfAngle = self.calc.getAngle(edgeCenter, chosenSpinCenter, circleMidPoint) + angle = 2 * halfAngle[0] # in radians + self.a = degrees(angle) # in degrees + + spinAxis = self.getSpinAxis(parameters["plane"]) + steps = parameters["segments"] + angle = -angle # rotate clockwise by default + + return [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] + + def drawSpin(self, edge, edgeCenter, roundifyParams, parameters, bm, mesh): + [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] = roundifyParams + + v0org, v1org = (edge.verts[0], edge.verts[1]) + + if parameters["flip"]: + angle = -angle + spinCenterTemp = chosenSpinCenter + chosenSpinCenter = otherSpinCenter + otherSpinCenter = spinCenterTemp + + if(parameters["invertAngle"]): + if angle < 0: + angle = two_pi + angle + elif angle > 0: + angle = -two_pi + angle + else: + angle = two_pi + + if(parameters["fullCircles"]): + angle = two_pi + + v0 = bm.verts.new(v0org.co) + + result = bmesh.ops.spin(bm, geom=[v0], cent=chosenSpinCenter, axis=spinAxis, + angle=angle, steps=steps, use_duplicate=False) + + # it seems there is something wrong with last index of this spin + # I need to calculate the last index manually here + vertsLength = len(bm.verts) + bm.verts.ensure_lookup_table() + lastVertIndex = bm.verts[vertsLength - 1].index + lastSpinVertIndices = self.getLastSpinVertIndices(steps, lastVertIndex) + + self.sel.refreshMesh(bm, mesh) + + alternativeLastSpinVertIndices = [] + bothSpinVertices = [] + spinVertices = [] + alternate = False + + if ((angle == pi or angle == -pi) and not parameters["bothSides"]): + + midVertexIndex = lastVertIndex - round(steps / 2) + bm.verts.ensure_lookup_table() + midVert = bm.verts[midVertexIndex].co + + midVertexDistance = (Vector(refObjectLocation) - Vector(midVert)).length + midEdgeDistance = (Vector(refObjectLocation) - Vector(edgeCenter)).length + + if ((parameters["invertAngle"]) or (parameters["flip"])): + if (midVertexDistance > midEdgeDistance): + alternativeLastSpinVertIndices = self.alternateSpin( + bm, mesh, angle, chosenSpinCenter, + spinAxis, steps, v0, v1org, lastSpinVertIndices + ) + else: + if (midVertexDistance < midEdgeDistance): + alternativeLastSpinVertIndices = self.alternateSpin( + bm, mesh, angle, chosenSpinCenter, + spinAxis, steps, v0, v1org, lastSpinVertIndices + ) + elif (angle != two_pi): # to allow full circles + if (result['geom_last'][0].co - v1org.co).length > SPIN_END_THRESHOLD: + alternativeLastSpinVertIndices = self.alternateSpin( + bm, mesh, angle, chosenSpinCenter, + spinAxis, steps, v0, v1org, lastSpinVertIndices + ) + alternate = True + + self.sel.refreshMesh(bm, mesh) + if alternativeLastSpinVertIndices != []: + lastSpinVertIndices = alternativeLastSpinVertIndices + + if lastSpinVertIndices.stop <= len(bm.verts): # make sure arc was added to bmesh + spinVertices = [bm.verts[i] for i in lastSpinVertIndices] + if alternativeLastSpinVertIndices != []: + spinVertices = spinVertices + [v0] + else: + spinVertices = [v0] + spinVertices + + if (parameters["bothSides"]): + # do some more testing here!!! + if (angle == pi or angle == -pi): + alternativeLastSpinVertIndices = self.alternateSpinNoDelete( + bm, mesh, -angle, chosenSpinCenter, + spinAxis, steps, v0, v1org, [] + ) + elif alternate: + alternativeLastSpinVertIndices = self.alternateSpinNoDelete( + bm, mesh, angle, otherSpinCenter, + spinAxis, steps, v0, v1org, [] + ) + elif not alternate: + alternativeLastSpinVertIndices = self.alternateSpinNoDelete( + bm, mesh, -angle, otherSpinCenter, + spinAxis, steps, v0, v1org, [] + ) + bothSpinVertices = [bm.verts[i] for i in lastSpinVertIndices] + alternativeSpinVertices = [bm.verts[i] for i in alternativeLastSpinVertIndices] + bothSpinVertices = [v0] + bothSpinVertices + alternativeSpinVertices + spinVertices = bothSpinVertices + + if (parameters["fullCircles"]): + v1 = bm.verts.new(v1org.co) + spinVertices = spinVertices + [v1] + + if (parameters['drawArcCenters']): + centerVert = bm.verts.new(chosenSpinCenter) + spinVertices.append(centerVert) + + return spinVertices, [chosenSpinCenter, otherSpinCenter, spinAxis, angle, steps, refObjectLocation] + + def deleteSpinVertices(self, bm, mesh, lastSpinVertIndices): + verticesForDeletion = [] + bm.verts.ensure_lookup_table() + for i in lastSpinVertIndices: + vi = bm.verts[i] + vi.select = True + debugPrintNew(True, str(i) + ") " + str(vi)) + verticesForDeletion.append(vi) + + bmesh.ops.delete(bm, geom=verticesForDeletion, context=1) + bmesh.update_edit_mesh(mesh, True) + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + + def alternateSpinNoDelete(self, bm, mesh, angle, chosenSpinCenter, + spinAxis, steps, v0, v1org, lastSpinVertIndices): + v0prim = v0 + + result2 = bmesh.ops.spin(bm, geom=[v0prim], cent=chosenSpinCenter, axis=spinAxis, + angle=angle, steps=steps, use_duplicate=False) + vertsLength = len(bm.verts) + bm.verts.ensure_lookup_table() + lastVertIndex2 = bm.verts[vertsLength - 1].index + + lastSpinVertIndices2 = self.getLastSpinVertIndices(steps, lastVertIndex2) + return lastSpinVertIndices2 + + def alternateSpin(self, bm, mesh, angle, chosenSpinCenter, spinAxis, steps, v0, v1org, lastSpinVertIndices): + self.deleteSpinVertices(bm, mesh, lastSpinVertIndices) + v0prim = v0 + + result2 = bmesh.ops.spin(bm, geom=[v0prim], cent=chosenSpinCenter, axis=spinAxis, + angle=-angle, steps=steps, use_duplicate=False) + # it seems there is something wrong with last index of this spin + # I need to calculate the last index manually here + vertsLength = len(bm.verts) + bm.verts.ensure_lookup_table() + lastVertIndex2 = bm.verts[vertsLength - 1].index + + lastSpinVertIndices2 = self.getLastSpinVertIndices(steps, lastVertIndex2) + # second spin also does not hit the v1org + if (result2['geom_last'][0].co - v1org.co).length > SPIN_END_THRESHOLD: + + self.deleteSpinVertices(bm, mesh, lastSpinVertIndices2) + self.deleteSpinVertices(bm, mesh, range(v0.index, v0.index + 1)) + return [] + else: + return lastSpinVertIndices2 + + def getLastSpinVertIndices(self, steps, lastVertIndex): + arcfirstVertexIndex = lastVertIndex - steps + 1 + lastSpinVertIndices = range(arcfirstVertexIndex, lastVertIndex + 1) + return lastSpinVertIndices + + def rotateArcAroundSpinAxis(self, bm, mesh, vertices, parameters, edgeCenter): + axisAngle = parameters["axisAngle"] + plane = parameters["plane"] + # compensate rotation center + objectLocation = bpy.context.active_object.location + center = objectLocation + edgeCenter + + rot = Euler((0.0, 0.0, radians(axisAngle)), 'XYZ').to_matrix() + if plane == YZ: + rot = Euler((radians(axisAngle), 0.0, 0.0), 'XYZ').to_matrix() + if plane == XZ: + rot = Euler((0.0, radians(axisAngle), 0.0), 'XYZ').to_matrix() + + indexes = [v.index for v in vertices] + + bmesh.ops.rotate( + bm, + cent=center, + matrix=rot, + verts=vertices, + space=bpy.context.edit_object.matrix_world + ) + self.sel.refreshMesh(bm, mesh) + bm.verts.ensure_lookup_table() + rotatedVertices = [bm.verts[i] for i in indexes] + + return rotatedVertices + + def CalculateRadiusAndAngle(self, edgeLength): + degAngle = self.a + angle = radians(degAngle) + self.r = radius = edgeLength / (2 * sin(angle / 2)) + return radius, angle + + def CalculateRadiusAndAngleForAnglePresets(self, angleEnum, initR, initA, edgeLength): + radius = initR + angle = initA + try: + # Note - define an integer string in the angleEnum + angle_convert = int(angleEnum) + self.a = angle_convert + except: + self.a = 180 # fallback + debugPrintNew(True, "CalculateRadiusAndAngleForAnglePresets problem with int conversion") + + return self.CalculateRadiusAndAngle(edgeLength) + + def getSpinCenterClosestToRefCenter(self, objLocation, roots): + root0Distance = (Vector(objLocation) - Vector(roots[0])).length + root1Distance = (Vector(objLocation) - Vector(roots[1])).length + + chosenId = 0 + rejectedId = 1 + if (root0Distance > root1Distance): + chosenId = 1 + rejectedId = 0 + return roots[chosenId], roots[rejectedId] + + def addMissingCoordinate(self, roots, startVertex, plane): + if roots is not None: + a, b = roots[0] + c, d = roots[1] + if plane == XY: + roots[0] = Vector((a, b, startVertex[2])) + roots[1] = Vector((c, d, startVertex[2])) + if plane == YZ: + roots[0] = Vector((startVertex[0], a, b)) + roots[1] = Vector((startVertex[0], c, d)) + if plane == XZ: + roots[0] = Vector((a, startVertex[1], b)) + roots[1] = Vector((c, startVertex[1], d)) + return roots + + def selectEdgesAfterRoundifier(self, context, edges): + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + mesh = context.scene.objects.active.data + bmnew = bmesh.new() + bmnew.from_mesh(mesh) + + self.deselectEdges(bmnew) + for selectedEdge in edges: + for e in bmnew.edges: + if (e.verts[0].co - selectedEdge.verts[0].co).length <= self.threshold \ + and (e.verts[1].co - selectedEdge.verts[1].co).length <= self.threshold: + e.select_set(True) + + bpy.ops.object.mode_set(mode='OBJECT') + bmnew.to_mesh(mesh) + bmnew.free() + bpy.ops.object.mode_set(mode='EDIT') + + def deselectEdges(self, bm): + for edge in bm.edges: + edge.select_set(False) + + def getSpinAxis(self, plane): + axis = (0, 0, 1) + if plane == YZ: + axis = (1, 0, 0) + if plane == XZ: + axis = (0, 1, 0) + return axis + + +def register(): + bpy.utils.register_class(EdgeRoundifier) + + +def unregister(): + bpy.utils.unregister_class(EdgeRoundifier) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_edges_length.py b/mesh_extra_tools/mesh_edges_length.py new file mode 100644 index 00000000..177fdb23 --- /dev/null +++ b/mesh_extra_tools/mesh_edges_length.py @@ -0,0 +1,330 @@ +# gpl author: Giuseppe De Marco [BlenderLab] inspired by NirenYang + +bl_info = { + "name": "Set edges length", + "description": "edges length", + "author": "Giuseppe De Marco [BlenderLab] inspired by NirenYang", + "version": (0, 1, 0), + "blender": (2, 7, 0, 5), + "location": "[Toolbar][Tools][Mesh Tools]: set Length(Shit+Alt+E)", + "warning": "", + "category": "Mesh", + "wiki_url": "", + "tracker_url": "", +} + +import bpy +import bmesh +from mathutils import Vector +from bpy.types import Operator +from bpy.props import ( + FloatProperty, + EnumProperty, + ) + +# GLOBALS +edge_length_debug = False +_error_message = "Please select at least one edge to fill select history" +_error_message_2 = "Edges with shared vertices are not allowed. Please, use scale instead" + +# Note : Refactor - removed all the operators apart from LengthSet +# and merged the other ones as options of length (lijenstina) + + +def get_edge_vector(edge): + verts = (edge.verts[0].co, edge.verts[1].co) + vector = verts[1] - verts[0] + + return vector + + +def get_selected(bmesh_obj, geometry_type): + # geometry type should be edges, verts or faces + + selected = [] + for i in getattr(bmesh_obj, geometry_type): + if i.select: + selected.append(i) + return tuple(selected) + + +def get_center_vector(verts): + # verts = [Vector((x,y,z)), Vector((x,y,z))] + + center_vector = Vector((((verts[1][0] + verts[0][0]) / 2.), + ((verts[1][1] + verts[0][1]) / 2.), + ((verts[1][2] + verts[0][2]) / 2.))) + return center_vector + + +class LengthSet(Operator): + bl_idname = "object.mesh_edge_length_set" + bl_label = "Set edge length" + bl_description = ("Change one selected edge length by a specified target,\n" + "existing lenght and different modes\n" + "Note: works only with Edges that not share a vertex") + bl_options = {'REGISTER', 'UNDO'} + + old_length = FloatProperty( + name="Original length", + options={'HIDDEN'}, + ) + set_lenght_type = EnumProperty( + items=[ + ('manual', "Manual", "Input manually the desired Target Lenght"), + ('existing', "Existing Lenght", "Use existing geometry Edges' characteristics"), + ], + name="Set Type of Input", + ) + target_length = FloatProperty( + name="Target Length", + description="Input a value for an Edges Lenght target", + default=1.00, + unit='LENGTH', + precision=5 + ) + existing_lenght = EnumProperty( + items=[ + ('min', "Shortest", "Set all to shortest Edge of selection"), + ('max', "Longest", "Set all to the longest Edge of selection"), + ('average', "Average", "Set all to the average Edge lenght of selection"), + ('active', "Active", "Set all to the active Edge's one\n" + "Needs a selection to be done in Edge Select mode"), + ], + name="Existing lenght" + ) + mode = EnumProperty( + items=[ + ('fixed', "Fixed", "Fixed"), + ('increment', "Increment", "Increment"), + ('decrement', "Decrement", "Decrement"), + ], + name="Mode" + ) + behaviour = EnumProperty( + items=[ + ('proportional', "Proportional", + "Move vertex locations proportionally to the center of the Edge"), + ('clockwise', "Clockwise", + "Compute the Edges' vertex locations in a clockwise fashion"), + ('unclockwise', "Counterclockwise", + "Compute the Edges' vertex locations in a counterclockwise fashion"), + ], + name="Resize behavior") + + originary_edge_length_dict = {} + edge_lenghts = [] + selected_edges = () + + @classmethod + def poll(cls, context): + return (context.edit_object and context.object.type == 'MESH') + + def check(self, context): + return True + + def draw(self, context): + layout = self.layout + + layout.label("Original Active lenght is: {:.3f}".format(self.old_length)) + + layout.label("Input Mode:") + layout.prop(self, "set_lenght_type", expand=True) + if self.set_lenght_type == 'manual': + layout.prop(self, "target_length") + else: + layout.prop(self, "existing_lenght", text="") + + layout.label("Mode:") + layout.prop(self, "mode", text="") + + layout.label("Resize Behavior") + layout.prop(self, "behaviour", text="") + + def get_existing_edge_lenght(self, bm): + if self.existing_lenght != "active": + if self.existing_lenght == "min": + return min(self.edge_lenghts) + if self.existing_lenght == "max": + return max(self.edge_lenghts) + elif self.existing_lenght == "average": + return sum(self.edge_lenghts) / float(len(self.selected_edges)) + else: + bm.edges.ensure_lookup_table() + active_edge_length = None + + for elem in reversed(bm.select_history): + if isinstance(elem, bmesh.types.BMEdge): + active_edge_length = elem.calc_length() + break + return active_edge_length + + return 0.0 + + def invoke(self, context, event): + wm = context.window_manager + + obj = context.edit_object + bm = bmesh.from_edit_mesh(obj.data) + + bpy.ops.mesh.select_mode(type="EDGE") + self.selected_edges = get_selected(bm, 'edges') + + if self.selected_edges: + vertex_set = [] + + for edge in self.selected_edges: + vector = get_edge_vector(edge) + + if edge.verts[0].index not in vertex_set: + vertex_set.append(edge.verts[0].index) + else: + self.report({'ERROR_INVALID_INPUT'}, _error_message_2) + return {'CANCELLED'} + + if edge.verts[1].index not in vertex_set: + vertex_set.append(edge.verts[1].index) + else: + self.report({'ERROR_INVALID_INPUT'}, _error_message_2) + return {'CANCELLED'} + + # warning, it's a constant ! + verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index))) + self.originary_edge_length_dict[verts_index] = vector + self.edge_lenghts.append(vector.length) + self.old_length = vector.length + else: + self.report({'ERROR'}, _error_message) + return {'CANCELLED'} + + if edge_length_debug: + self.report({'INFO'}, str(self.originary_edge_length_dict)) + + if bpy.context.scene.unit_settings.system == 'IMPERIAL': + # imperial to metric conversion + vector.length = (0.9144 * vector.length) / 3 + + self.target_length = vector.length + + return wm.invoke_props_dialog(self) + + def execute(self, context): + + bpy.ops.mesh.select_mode(type="EDGE") + self.context = context + + obj = context.edit_object + bm = bmesh.from_edit_mesh(obj.data) + + self.selected_edges = get_selected(bm, 'edges') + + if not self.selected_edges: + self.report({'ERROR'}, _error_message) + return {'CANCELLED'} + + for edge in self.selected_edges: + vector = get_edge_vector(edge) + # what we should see in original length dialog field + self.old_length = vector.length + + if self.set_lenght_type == 'manual': + vector.length = abs(self.target_length) + else: + get_lenghts = self.get_existing_edge_lenght(bm) + # check for edit mode + if not get_lenghts: + self.report({'WARNING'}, + "Operation Cancelled. " + "Active Edge could not be determined (needs selection in Edit Mode)") + return {'CANCELLED'} + + vector.length = get_lenghts + + if vector.length == 0.0: + self.report({'ERROR'}, "Operation cancelled. Target lenght is set to zero") + return {'CANCELLED'} + + center_vector = get_center_vector((edge.verts[0].co, edge.verts[1].co)) + + verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index))) + + if edge_length_debug: + self.report({'INFO'}, + ' - '.join(('vector ' + str(vector), + 'originary_vector ' + str(self.originary_edge_length_dict[verts_index]) + ))) + verts = (edge.verts[0].co, edge.verts[1].co) + + if edge_length_debug: + self.report({'INFO'}, + '\n edge.verts[0].co ' + str(verts[0]) + + '\n edge.verts[1].co ' + str(verts[1]) + + '\n vector.length' + str(vector.length)) + + # the clockwise direction have v1 -> v0, unclockwise v0 -> v1 + if self.target_length >= 0: + if self.behaviour == 'proportional': + edge.verts[1].co = center_vector + vector / 2 + edge.verts[0].co = center_vector - vector / 2 + + if self.mode == 'decrement': + edge.verts[0].co = (center_vector + vector / 2) - \ + (self.originary_edge_length_dict[verts_index] / 2) + edge.verts[1].co = (center_vector - vector / 2) + \ + (self.originary_edge_length_dict[verts_index] / 2) + + elif self.mode == 'increment': + edge.verts[1].co = (center_vector + vector / 2) + \ + self.originary_edge_length_dict[verts_index] / 2 + edge.verts[0].co = (center_vector - vector / 2) - \ + self.originary_edge_length_dict[verts_index] / 2 + + elif self.behaviour == 'unclockwise': + if self.mode == 'increment': + edge.verts[1].co = verts[0] + (self.originary_edge_length_dict[verts_index] + vector) + elif self.mode == 'decrement': + edge.verts[0].co = verts[1] - (self.originary_edge_length_dict[verts_index] - vector) + else: + edge.verts[1].co = verts[0] + vector + + else: + # clockwise + if self.mode == 'increment': + edge.verts[0].co = verts[1] - (self.originary_edge_length_dict[verts_index] + vector) + elif self.mode == 'decrement': + edge.verts[1].co = verts[0] + (self.originary_edge_length_dict[verts_index] - vector) + else: + edge.verts[0].co = verts[1] - vector + + if bpy.context.scene.unit_settings.system == 'IMPERIAL': + """ + # yards to metric conversion + vector.length = ( 3. * vector.length ) / 0.9144 + # metric to yards conversion + vector.length = ( 0.9144 * vector.length ) / 3. + """ + for mvert in edge.verts: + # school time: 0.9144 : 3 = X : mvert + mvert.co = (0.9144 * mvert.co) / 3 + + if edge_length_debug: + self.report({'INFO'}, + '\n edge.verts[0].co' + str(verts[0]) + + '\n edge.verts[1].co' + str(verts[1]) + + '\n vector' + str(vector) + '\n v1 > v0:' + str((verts[1] >= verts[0])) + ) + bmesh.update_edit_mesh(obj.data, True) + + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(LengthSet) + + +def unregister(): + bpy.utils.unregister_class(LengthSet) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_edgetools.py b/mesh_extra_tools/mesh_edgetools.py new file mode 100644 index 00000000..fde651cc --- /dev/null +++ b/mesh_extra_tools/mesh_edgetools.py @@ -0,0 +1,1829 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# The Blender Edgetools is to bring CAD tools to Blender. +# Copyright (C) 2012 Paul Marshall +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +bl_info = { + "name": "EdgeTools", + "author": "Paul Marshall", + "version": (0, 9), + "blender": (2, 68, 0), + "location": "View3D > Toolbar and View3D > Specials (W-key)", + "warning": "", + "description": "CAD style edge manipulation tools", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" + "Scripts/Modeling/EdgeTools", + "tracker_url": "", + "category": "Mesh"} + + +import bpy +import bmesh +from bpy.types import ( + Operator, + Menu, + ) +from math import acos, pi, radians, sqrt +from mathutils import Matrix, Vector +from mathutils.geometry import ( + distance_point_to_plane, + interpolate_bezier, + intersect_point_line, + intersect_line_line, + intersect_line_plane, + ) +from bpy.props import ( + BoolProperty, + IntProperty, + FloatProperty, + EnumProperty, + ) + +""" +Blender EdgeTools +This is a toolkit for edge manipulation based on mesh manipulation +abilities of several CAD/CAE packages, notably CATIA's Geometric Workbench +from which most of these tools have a functional basis. + +The GUI and Blender add-on structure shamelessly coded in imitation of the +LoopTools addon. + +Examples: +- "Ortho" inspired from CATIA's line creation tool which creates a line of a + user specified length at a user specified angle to a curve at a chosen + point. The user then selects the plane the line is to be created in. +- "Shaft" is inspired from CATIA's tool of the same name. However, instead + of a curve around an axis, this will instead shaft a line, a point, or + a fixed radius about the selected axis. +- "Slice" is from CATIA's ability to split a curve on a plane. When + completed this be a Python equivalent with all the same basic + functionality, though it will sadly be a little clumsier to use due + to Blender's selection limitations. + +Notes: +- Fillet operator and related functions removed as they didn't work +- Buggy parts have been hidden behind ENABLE_DEBUG global (set it to True) + Example: Shaft with more than two edges selected + +Paul "BrikBot" Marshall +Created: January 28, 2012 +Last Modified: October 6, 2012 + +Coded in IDLE, tested in Blender 2.6. +Search for "@todo" to quickly find sections that need work + +Note: lijenstina - modified this script in preparation for merging +fixed the needless jumping to object mode for bmesh creation +causing the crash with the Slice > Rip operator +""" + +# Enable debug +# Set to True to have the Interesect Line Face Test and debug prints available +ENABLE_DEBUG = False + + +# Quick an dirty method for getting the sign of a number: +def sign(number): + return (number > 0) - (number < 0) + + +# is_parallel +# Checks to see if two lines are parallel + +def is_parallel(v1, v2, v3, v4): + result = intersect_line_line(v1, v2, v3, v4) + return result is None + + +# is_axial +# This is for the special case where the edge is parallel to an axis. +# The projection onto the XY plane will fail so it will have to be handled differently + +def is_axial(v1, v2, error=0.000002): + vector = v2 - v1 + # Don't need to store, but is easier to read: + vec0 = vector[0] > -error and vector[0] < error + vec1 = vector[1] > -error and vector[1] < error + vec2 = vector[2] > -error and vector[2] < error + if (vec0 or vec1) and vec2: + return 'Z' + elif vec0 and vec1: + return 'Y' + return None + + +# is_same_co +# For some reason "Vector = Vector" does not seem to look at the actual coordinates + +def is_same_co(v1, v2): + if len(v1) != len(v2): + return False + else: + for co1, co2 in zip(v1, v2): + if co1 != co2: + return False + return True + + +def is_face_planar(face, error=0.0005): + for v in face.verts: + d = distance_point_to_plane(v.co, face.verts[0].co, face.normal) + if ENABLE_DEBUG: + print("Distance: " + str(d)) + if d < -error or d > error: + return False + return True + + +# other_joined_edges +# Starts with an edge. Then scans for linked, selected edges and builds a +# list with them in "order", starting at one end and moving towards the other + +def order_joined_edges(edge, edges=[], direction=1): + if len(edges) == 0: + edges.append(edge) + edges[0] = edge + + if ENABLE_DEBUG: + print(edge, end=", ") + print(edges, end=", ") + print(direction, end="; ") + + # Robustness check: direction cannot be zero + if direction == 0: + direction = 1 + + newList = [] + for e in edge.verts[0].link_edges: + if e.select and edges.count(e) == 0: + if direction > 0: + edges.insert(0, e) + newList.extend(order_joined_edges(e, edges, direction + 1)) + newList.extend(edges) + else: + edges.append(e) + newList.extend(edges) + newList.extend(order_joined_edges(e, edges, direction - 1)) + + # This will only matter at the first level: + direction = direction * -1 + + for e in edge.verts[1].link_edges: + if e.select and edges.count(e) == 0: + if direction > 0: + edges.insert(0, e) + newList.extend(order_joined_edges(e, edges, direction + 2)) + newList.extend(edges) + else: + edges.append(e) + newList.extend(edges) + newList.extend(order_joined_edges(e, edges, direction)) + + if ENABLE_DEBUG: + print(newList, end=", ") + print(direction) + + return newList + + +# --------------- GEOMETRY CALCULATION METHODS -------------- + +# distance_point_line +# I don't know why the mathutils.geometry API does not already have this, but +# it is trivial to code using the structures already in place. Instead of +# returning a float, I also want to know the direction vector defining the +# distance. Distance can be found with "Vector.length" + +def distance_point_line(pt, line_p1, line_p2): + int_co = intersect_point_line(pt, line_p1, line_p2) + distance_vector = int_co[0] - pt + return distance_vector + + +# interpolate_line_line +# This is an experiment into a cubic Hermite spline (c-spline) for connecting +# two edges with edges that obey the general equation. +# This will return a set of point coordinates (Vectors) +# +# A good, easy to read background on the mathematics can be found at: +# http://cubic.org/docs/hermite.htm +# +# Right now this is . . . less than functional :P +# @todo +# - C-Spline and Bezier curves do not end on p2_co as they are supposed to. +# - B-Spline just fails. Epically. +# - Add more methods as I come across them. Who said flexibility was bad? + +def interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, segments, tension=1, + typ='BEZIER', include_ends=False): + pieces = [] + fraction = 1 / segments + + # Form: p1, tangent 1, p2, tangent 2 + if typ == 'HERMITE': + poly = [[2, -3, 0, 1], [1, -2, 1, 0], + [-2, 3, 0, 0], [1, -1, 0, 0]] + elif typ == 'BEZIER': + poly = [[-1, 3, -3, 1], [3, -6, 3, 0], + [1, 0, 0, 0], [-3, 3, 0, 0]] + p1_dir = p1_dir + p1_co + p2_dir = -p2_dir + p2_co + elif typ == 'BSPLINE': + # Supposed poly matrix for a cubic b-spline: + # poly = [[-1, 3, -3, 1], [3, -6, 3, 0], + # [-3, 0, 3, 0], [1, 4, 1, 0]] + # My own invention to try to get something that somewhat acts right + # This is semi-quadratic rather than fully cubic: + poly = [[0, -1, 0, 1], [1, -2, 1, 0], + [0, -1, 2, 0], [1, -1, 0, 0]] + + if include_ends: + pieces.append(p1_co) + + # Generate each point: + for i in range(segments - 1): + t = fraction * (i + 1) + if ENABLE_DEBUG: + print(t) + s = [t ** 3, t ** 2, t, 1] + h00 = (poly[0][0] * s[0]) + (poly[0][1] * s[1]) + (poly[0][2] * s[2]) + (poly[0][3] * s[3]) + h01 = (poly[1][0] * s[0]) + (poly[1][1] * s[1]) + (poly[1][2] * s[2]) + (poly[1][3] * s[3]) + h10 = (poly[2][0] * s[0]) + (poly[2][1] * s[1]) + (poly[2][2] * s[2]) + (poly[2][3] * s[3]) + h11 = (poly[3][0] * s[0]) + (poly[3][1] * s[1]) + (poly[3][2] * s[2]) + (poly[3][3] * s[3]) + pieces.append((h00 * p1_co) + (h01 * p1_dir) + (h10 * p2_co) + (h11 * p2_dir)) + if include_ends: + pieces.append(p2_co) + + # Return: + if len(pieces) == 0: + return None + else: + if ENABLE_DEBUG: + print(pieces) + return pieces + + +# intersect_line_face + +# Calculates the coordinate of intersection of a line with a face. It returns +# the coordinate if one exists, otherwise None. It can only deal with tris or +# quads for a face. A quad does NOT have to be planar +""" +Quad math and theory: +A quad may not be planar. Therefore the treated definition of the surface is +that the surface is composed of all lines bridging two other lines defined by +the given four points. The lines do not "cross" + +The two lines in 3-space can defined as: +┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ +│x1│ │a11│ │b11│ │x2│ │a21│ │b21│ +│y1│ = (1-t1)│a12│ + t1│b12│, │y2│ = (1-t2)│a22│ + t2│b22│ +│z1│ │a13│ │b13│ │z2│ │a23│ │b23│ +└ ┘ └ ┘ └ ┘ └ ┘ └ ┘ └ ┘ +Therefore, the surface is the lines defined by every point alone the two +lines with a same "t" value (t1 = t2). This is basically R = V1 + tQ, where +Q = V2 - V1 therefore R = V1 + t(V2 - V1) -> R = (1 - t)V1 + tV2: +┌ ┐ ┌ ┐ ┌ ┐ +│x12│ │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│ +│y12│ = (1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│ +│z12│ │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│ +└ ┘ └ ┘ └ ┘ +Now, the equation of our line can be likewise defined: +┌ ┐ ┌ ┐ ┌ ┐ +│x3│ │a31│ │b31│ +│y3│ = │a32│ + t3│b32│ +│z3│ │a33│ │b33│ +└ ┘ └ ┘ └ ┘ +Now we just have to find a valid solution for the two equations. This should +be our point of intersection. Therefore, x12 = x3 -> x, y12 = y3 -> y, +z12 = z3 -> z. Thus, to find that point we set the equation defining the +surface as equal to the equation for the line: + ┌ ┐ ┌ ┐ ┌ ┐ ┌ ┐ + │(1-t)a11 + t * b11│ │(1-t)a21 + t * b21│ │a31│ │b31│ +(1 - t12)│(1-t)a12 + t * b12│ + t12│(1-t)a22 + t * b22│ = │a32│ + t3│b32│ + │(1-t)a13 + t * b13│ │(1-t)a23 + t * b23│ │a33│ │b33│ + └ ┘ └ ┘ └ ┘ └ ┘ +This leaves us with three equations, three unknowns. Solving the system by +hand is practically impossible, but using Mathematica we are given an insane +series of three equations (not reproduced here for the sake of space: see +http://www.mediafire.com/file/cc6m6ba3sz2b96m/intersect_line_surface.nb and +http://www.mediafire.com/file/0egbr5ahg14talm/intersect_line_surface2.nb for +Mathematica computation). + +Additionally, the resulting series of equations may result in a div by zero +exception if the line in question if parallel to one of the axis or if the +quad is planar and parallel to either the XY, XZ, or YZ planes. However, the +system is still solvable but must be dealt with a little differently to avaid +these special cases. Because the resulting equations are a little different, +we have to code them differently. 00Hence the special cases. + +Tri math and theory: +A triangle must be planar (three points define a plane). So we just +have to make sure that the line intersects inside the triangle. + +If the point is within the triangle, then the angle between the lines that +connect the point to the each individual point of the triangle will be +equal to 2 * PI. Otherwise, if the point is outside the triangle, then the +sum of the angles will be less. +""" +# @todo +# - Figure out how to deal with n-gons +# How the heck is a face with 8 verts definied mathematically? +# How do I then find the intersection point of a line with said vert? +# How do I know if that point is "inside" all the verts? +# I have no clue, and haven't been able to find anything on it so far +# Maybe if someone (actually reads this and) who knows could note? + + +def intersect_line_face(edge, face, is_infinite=False, error=0.000002): + int_co = None + + # If we are dealing with a non-planar quad: + if len(face.verts) == 4 and not is_face_planar(face): + edgeA = face.edges[0] + edgeB = None + flipB = False + + for i in range(len(face.edges)): + if face.edges[i].verts[0] not in edgeA.verts and face.edges[i].verts[1] not in edgeA.verts: + edgeB = face.edges[i] + break + + # I haven't figured out a way to mix this in with the above. Doing so might remove a + # few extra instructions from having to be executed saving a few clock cycles: + for i in range(len(face.edges)): + if face.edges[i] == edgeA or face.edges[i] == edgeB: + continue + if ((edgeA.verts[0] in face.edges[i].verts and + edgeB.verts[1] in face.edges[i].verts) or + (edgeA.verts[1] in face.edges[i].verts and edgeB.verts[0] in face.edges[i].verts)): + + flipB = True + break + + # Define calculation coefficient constants: + # "xx1" is the x coordinate, "xx2" is the y coordinate, and "xx3" is the z coordinate + a11, a12, a13 = edgeA.verts[0].co[0], edgeA.verts[0].co[1], edgeA.verts[0].co[2] + b11, b12, b13 = edgeA.verts[1].co[0], edgeA.verts[1].co[1], edgeA.verts[1].co[2] + + if flipB: + a21, a22, a23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2] + b21, b22, b23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2] + else: + a21, a22, a23 = edgeB.verts[0].co[0], edgeB.verts[0].co[1], edgeB.verts[0].co[2] + b21, b22, b23 = edgeB.verts[1].co[0], edgeB.verts[1].co[1], edgeB.verts[1].co[2] + a31, a32, a33 = edge.verts[0].co[0], edge.verts[0].co[1], edge.verts[0].co[2] + b31, b32, b33 = edge.verts[1].co[0], edge.verts[1].co[1], edge.verts[1].co[2] + + # There are a bunch of duplicate "sub-calculations" inside the resulting + # equations for t, t12, and t3. Calculate them once and store them to + # reduce computational time: + m01 = a13 * a22 * a31 + m02 = a12 * a23 * a31 + m03 = a13 * a21 * a32 + m04 = a11 * a23 * a32 + m05 = a12 * a21 * a33 + m06 = a11 * a22 * a33 + m07 = a23 * a32 * b11 + m08 = a22 * a33 * b11 + m09 = a23 * a31 * b12 + m10 = a21 * a33 * b12 + m11 = a22 * a31 * b13 + m12 = a21 * a32 * b13 + m13 = a13 * a32 * b21 + m14 = a12 * a33 * b21 + m15 = a13 * a31 * b22 + m16 = a11 * a33 * b22 + m17 = a12 * a31 * b23 + m18 = a11 * a32 * b23 + m19 = a13 * a22 * b31 + m20 = a12 * a23 * b31 + m21 = a13 * a32 * b31 + m22 = a23 * a32 * b31 + m23 = a12 * a33 * b31 + m24 = a22 * a33 * b31 + m25 = a23 * b12 * b31 + m26 = a33 * b12 * b31 + m27 = a22 * b13 * b31 + m28 = a32 * b13 * b31 + m29 = a13 * b22 * b31 + m30 = a33 * b22 * b31 + m31 = a12 * b23 * b31 + m32 = a32 * b23 * b31 + m33 = a13 * a21 * b32 + m34 = a11 * a23 * b32 + m35 = a13 * a31 * b32 + m36 = a23 * a31 * b32 + m37 = a11 * a33 * b32 + m38 = a21 * a33 * b32 + m39 = a23 * b11 * b32 + m40 = a33 * b11 * b32 + m41 = a21 * b13 * b32 + m42 = a31 * b13 * b32 + m43 = a13 * b21 * b32 + m44 = a33 * b21 * b32 + m45 = a11 * b23 * b32 + m46 = a31 * b23 * b32 + m47 = a12 * a21 * b33 + m48 = a11 * a22 * b33 + m49 = a12 * a31 * b33 + m50 = a22 * a31 * b33 + m51 = a11 * a32 * b33 + m52 = a21 * a32 * b33 + m53 = a22 * b11 * b33 + m54 = a32 * b11 * b33 + m55 = a21 * b12 * b33 + m56 = a31 * b12 * b33 + m57 = a12 * b21 * b33 + m58 = a32 * b21 * b33 + m59 = a11 * b22 * b33 + m60 = a31 * b22 * b33 + m61 = a33 * b12 * b21 + m62 = a32 * b13 * b21 + m63 = a33 * b11 * b22 + m64 = a31 * b13 * b22 + m65 = a32 * b11 * b23 + m66 = a31 * b12 * b23 + m67 = b13 * b22 * b31 + m68 = b12 * b23 * b31 + m69 = b13 * b21 * b32 + m70 = b11 * b23 * b32 + m71 = b12 * b21 * b33 + m72 = b11 * b22 * b33 + n01 = m01 - m02 - m03 + m04 + m05 - m06 + n02 = -m07 + m08 + m09 - m10 - m11 + m12 + m13 - m14 - m15 + m16 + m17 - m18 - \ + m25 + m27 + m29 - m31 + m39 - m41 - m43 + m45 - m53 + m55 + m57 - m59 + n03 = -m19 + m20 + m33 - m34 - m47 + m48 + n04 = m21 - m22 - m23 + m24 - m35 + m36 + m37 - m38 + m49 - m50 - m51 + m52 + n05 = m26 - m28 - m30 + m32 - m40 + m42 + m44 - m46 + m54 - m56 - m58 + m60 + n06 = m61 - m62 - m63 + m64 + m65 - m66 - m67 + m68 + m69 - m70 - m71 + m72 + n07 = 2 * n01 + n02 + 2 * n03 + n04 + n05 + n08 = n01 + n02 + n03 + n06 + + # Calculate t, t12, and t3: + t = (n07 - sqrt(pow(-n07, 2) - 4 * (n01 + n03 + n04) * n08)) / (2 * n08) + + # t12 can be greatly simplified by defining it with t in it: + # If block used to help prevent any div by zero error. + t12 = 0 + + if a31 == b31: + # The line is parallel to the z-axis: + if a32 == b32: + t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t) + # The line is parallel to the y-axis: + elif a33 == b33: + t12 = ((a11 - a31) + (b11 - a11) * t) / ((a21 - a11) + (a11 - a21 - b11 + b21) * t) + # The line is along the y/z-axis but is not parallel to either: + else: + t12 = -(-(a33 - b33) * (-a32 + a12 * (1 - t) + b12 * t) + (a32 - b32) * + (-a33 + a13 * (1 - t) + b13 * t)) / (-(a33 - b33) * ((a22 - a12) * (1 - t) + (b22 - b12) * t) + + (a32 - b32) * ((a23 - a13) * (1 - t) + (b23 - b13) * t)) + elif a32 == b32: + # The line is parallel to the x-axis: + if a33 == b33: + t12 = ((a12 - a32) + (b12 - a12) * t) / ((a22 - a12) + (a12 - a22 - b12 + b22) * t) + # The line is along the x/z-axis but is not parallel to either: + else: + t12 = -(-(a33 - b33) * (-a31 + a11 * (1 - t) + b11 * t) + (a31 - b31) * (-a33 + a13 * + (1 - t) + b13 * t)) / (-(a33 - b33) * ((a21 - a11) * (1 - t) + (b21 - b11) * t) + + (a31 - b31) * ((a23 - a13) * (1 - t) + (b23 - b13) * t)) + # The line is along the x/y-axis but is not parallel to either: + else: + t12 = -(-(a32 - b32) * (-a31 + a11 * (1 - t) + b11 * t) + (a31 - b31) * (-a32 + a12 * + (1 - t) + b12 * t)) / (-(a32 - b32) * ((a21 - a11) * (1 - t) + (b21 - b11) * t) + + (a31 - b31) * ((a22 - a21) * (1 - t) + (b22 - b12) * t)) + + # Likewise, t3 is greatly simplified by defining it in terms of t and t12: + # If block used to prevent a div by zero error. + t3 = 0 + if a31 != b31: + t3 = (-a11 + a31 + (a11 - b11) * t + (a11 - a21) * t12 + (a21 - a11 + b11 - b21) * t * t12) / (a31 - b31) + elif a32 != b32: + t3 = (-a12 + a32 + (a12 - b12) * t + (a12 - a22) * t12 + (a22 - a12 + b12 - b22) * t * t12) / (a32 - b32) + elif a33 != b33: + t3 = (-a13 + a33 + (a13 - b13) * t + (a13 - a23) * t12 + (a23 - a13 + b13 - b23) * t * t12) / (a33 - b33) + else: + print("The second edge is a zero-length edge") + return None + + # Calculate the point of intersection: + x = (1 - t3) * a31 + t3 * b31 + y = (1 - t3) * a32 + t3 * b32 + z = (1 - t3) * a33 + t3 * b33 + int_co = Vector((x, y, z)) + + if ENABLE_DEBUG: + print(int_co) + + # If the line does not intersect the quad, we return "None": + if (t < -1 or t > 1 or t12 < -1 or t12 > 1) and not is_infinite: + int_co = None + + elif len(face.verts) == 3: + p1, p2, p3 = face.verts[0].co, face.verts[1].co, face.verts[2].co + int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co, p1, face.normal) + + # Only check if the triangle is not being treated as an infinite plane: + # Math based from http://paulbourke.net/geometry/linefacet/ + if int_co is not None and not is_infinite: + pA = p1 - int_co + pB = p2 - int_co + pC = p3 - int_co + # These must be unit vectors, else we risk a domain error: + pA.length = 1 + pB.length = 1 + pC.length = 1 + aAB = acos(pA.dot(pB)) + aBC = acos(pB.dot(pC)) + aCA = acos(pC.dot(pA)) + sumA = aAB + aBC + aCA + + # If the point is outside the triangle: + if (sumA > (pi + error) and sumA < (pi - error)): + int_co = None + + # This is the default case where we either have a planar quad or an n-gon + else: + int_co = intersect_line_plane(edge.verts[0].co, edge.verts[1].co, + face.verts[0].co, face.normal) + return int_co + + +# project_point_plane +# Projects a point onto a plane. Returns a tuple of the projection vector +# and the projected coordinate + +def project_point_plane(pt, plane_co, plane_no): + if ENABLE_DEBUG: + print("project_point_plane was called") + proj_co = intersect_line_plane(pt, pt + plane_no, plane_co, plane_no) + proj_ve = proj_co - pt + print("project_point_plane: proj_co is {}\nproj_ve is {}".format(proj_co, proj_ve)) + return (proj_ve, proj_co) + + +# ------------ CHAMPHER HELPER METHODS ------------- + +def is_planar_edge(edge, error=0.000002): + angle = edge.calc_face_angle() + return ((angle < error and angle > -error) or + (angle < (180 + error) and angle > (180 - error))) + + +# ------------- EDGE TOOL METHODS ------------------- + +# Extends an "edge" in two directions: +# - Requires two vertices to be selected. They do not have to form an edge +# - Extends "length" in both directions + +class Extend(Operator): + bl_idname = "mesh.edgetools_extend" + bl_label = "Extend" + bl_description = "Extend the selected edges of vertex pairs" + bl_options = {'REGISTER', 'UNDO'} + + di1 = BoolProperty( + name="Forwards", + description="Extend the edge forwards", + default=True + ) + di2 = BoolProperty( + name="Backwards", + description="Extend the edge backwards", + default=False + ) + length = FloatProperty( + name="Length", + description="Length to extend the edge", + min=0.0, max=1024.0, + default=1.0 + ) + + def draw(self, context): + layout = self.layout + layout.prop(self, "di1") + layout.prop(self, "di2") + layout.prop(self, "length") + + @classmethod + def poll(cls, context): + ob = context.active_object + return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') + + def invoke(self, context, event): + return self.execute(context) + + def execute(self, context): + me = context.object.data + bm = bmesh.from_edit_mesh(me) + bm.normal_update() + + bEdges = bm.edges + bVerts = bm.verts + + edges = [e for e in bEdges if e.select] + verts = [v for v in bVerts if v.select] + + if len(edges) > 0: + for e in edges: + vector = e.verts[0].co - e.verts[1].co + vector.length = self.length + + if self.di1: + v = bVerts.new() + if (vector[0] + vector[1] + vector[2]) < 0: + v.co = e.verts[1].co - vector + newE = bEdges.new((e.verts[1], v)) + bEdges.ensure_lookup_table() + else: + v.co = e.verts[0].co + vector + newE = bEdges.new((e.verts[0], v)) + bEdges.ensure_lookup_table() + if self.di2: + v = bVerts.new() + if (vector[0] + vector[1] + vector[2]) < 0: + v.co = e.verts[0].co + vector + newE = bEdges.new((e.verts[0], v)) + bEdges.ensure_lookup_table() + else: + v.co = e.verts[1].co - vector + newE = bEdges.new((e.verts[1], v)) + bEdges.ensure_lookup_table() + else: + vector = verts[0].co - verts[1].co + vector.length = self.length + + if self.di1: + v = bVerts.new() + if (vector[0] + vector[1] + vector[2]) < 0: + v.co = verts[1].co - vector + e = bEdges.new((verts[1], v)) + bEdges.ensure_lookup_table() + else: + v.co = verts[0].co + vector + e = bEdges.new((verts[0], v)) + bEdges.ensure_lookup_table() + if self.di2: + v = bVerts.new() + if (vector[0] + vector[1] + vector[2]) < 0: + v.co = verts[0].co + vector + e = bEdges.new((verts[0], v)) + bEdges.ensure_lookup_table() + else: + v.co = verts[1].co - vector + e = bEdges.new((verts[1], v)) + bEdges.ensure_lookup_table() + + bmesh.update_edit_mesh(me) + + return {'FINISHED'} + + +# Creates a series of edges between two edges using spline interpolation. +# This basically just exposes existing functionality in addition to some +# other common methods: Hermite (c-spline), Bezier, and b-spline. These +# alternates I coded myself after some extensive research into spline theory +# +# @todo Figure out what's wrong with the Blender bezier interpolation + +class Spline(Operator): + bl_idname = "mesh.edgetools_spline" + bl_label = "Spline" + bl_description = "Create a spline interplopation between two edges" + bl_options = {'REGISTER', 'UNDO'} + + alg = EnumProperty( + name="Spline Algorithm", + items=[('Blender', "Blender", "Interpolation provided through mathutils.geometry"), + ('Hermite', "C-Spline", "C-spline interpolation"), + ('Bezier', "Bezier", "Bezier interpolation"), + ('B-Spline', "B-Spline", "B-Spline interpolation")], + default='Bezier' + ) + segments = IntProperty( + name="Segments", + description="Number of segments to use in the interpolation", + min=2, max=4096, + soft_max=1024, + default=32 + ) + flip1 = BoolProperty( + name="Flip Edge", + description="Flip the direction of the spline on Edge 1", + default=False + ) + flip2 = BoolProperty( + name="Flip Edge", + description="Flip the direction of the spline on Edge 2", + default=False + ) + ten1 = FloatProperty( + name="Tension", + description="Tension on Edge 1", + min=-4096.0, max=4096.0, + soft_min=-8.0, soft_max=8.0, + default=1.0 + ) + ten2 = FloatProperty( + name="Tension", + description="Tension on Edge 2", + min=-4096.0, max=4096.0, + soft_min=-8.0, soft_max=8.0, + default=1.0 + ) + + def draw(self, context): + layout = self.layout + + layout.prop(self, "alg") + layout.prop(self, "segments") + layout.label("Edge 1:") + layout.prop(self, "ten1") + layout.prop(self, "flip1") + layout.label("Edge 2:") + layout.prop(self, "ten2") + layout.prop(self, "flip2") + + @classmethod + def poll(cls, context): + ob = context.active_object + return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') + + def invoke(self, context, event): + return self.execute(context) + + def execute(self, context): + me = context.object.data + bm = bmesh.from_edit_mesh(me) + bm.normal_update() + + bEdges = bm.edges + bVerts = bm.verts + + seg = self.segments + edges = [e for e in bEdges if e.select] + verts = [edges[v // 2].verts[v % 2] for v in range(4)] + + if self.flip1: + v1 = verts[1] + p1_co = verts[1].co + p1_dir = verts[1].co - verts[0].co + else: + v1 = verts[0] + p1_co = verts[0].co + p1_dir = verts[0].co - verts[1].co + if self.ten1 < 0: + p1_dir = -1 * p1_dir + p1_dir.length = -self.ten1 + else: + p1_dir.length = self.ten1 + + if self.flip2: + v2 = verts[3] + p2_co = verts[3].co + p2_dir = verts[2].co - verts[3].co + else: + v2 = verts[2] + p2_co = verts[2].co + p2_dir = verts[3].co - verts[2].co + if self.ten2 < 0: + p2_dir = -1 * p2_dir + p2_dir.length = -self.ten2 + else: + p2_dir.length = self.ten2 + + # Get the interploted coordinates: + if self.alg == 'Blender': + pieces = interpolate_bezier(p1_co, p1_dir, p2_dir, p2_co, self.segments) + elif self.alg == 'Hermite': + pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'HERMITE') + elif self.alg == 'Bezier': + pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BEZIER') + elif self.alg == 'B-Spline': + pieces = interpolate_line_line(p1_co, p1_dir, p2_co, p2_dir, self.segments, 1, 'BSPLINE') + + verts = [] + verts.append(v1) + # Add vertices and set the points: + for i in range(seg - 1): + v = bVerts.new() + v.co = pieces[i] + bVerts.ensure_lookup_table() + verts.append(v) + verts.append(v2) + # Connect vertices: + for i in range(seg): + e = bEdges.new((verts[i], verts[i + 1])) + bEdges.ensure_lookup_table() + + bmesh.update_edit_mesh(me) + + return {'FINISHED'} + + +# Creates edges normal to planes defined between each of two edges and the +# normal or the plane defined by those two edges. +# - Select two edges. The must form a plane. +# - On running the script, eight edges will be created. Delete the +# extras that you don't need. +# - The length of those edges is defined by the variable "length" +# +# @todo Change method from a cross product to a rotation matrix to make the +# angle part work. +# --- todo completed 2/4/2012, but still needs work --- +# @todo Figure out a way to make +/- predictable +# - Maybe use angle between edges and vector direction definition? +# --- TODO COMPLETED ON 2/9/2012 --- + +class Ortho(Operator): + bl_idname = "mesh.edgetools_ortho" + bl_label = "Angle Off Edge" + bl_description = "Creates new edges within an angle from vertices of selected edges" + bl_options = {'REGISTER', 'UNDO'} + + vert1 = BoolProperty( + name="Vertice 1", + description="Enable edge creation for Vertice 1", + default=True + ) + vert2 = BoolProperty( + name="Vertice 2", + description="Enable edge creation for Vertice 2", + default=True + ) + vert3 = BoolProperty( + name="Vertice 3", + description="Enable edge creation for Vertice 3", + default=True + ) + vert4 = BoolProperty( + name="Vertice 4", + description="Enable edge creation for Vertice 4", + default=True + ) + pos = BoolProperty( + name="Positive", + description="Enable creation of positive direction edges", + default=True + ) + neg = BoolProperty( + name="Negative", + description="Enable creation of negative direction edges", + default=True + ) + angle = FloatProperty( + name="Angle", + description="Define the angle off of the originating edge", + min=0.0, max=180.0, + default=90.0 + ) + length = FloatProperty( + name="Length", + description="Length of created edges", + min=0.0, max=1024.0, + default=1.0 + ) + + # For when only one edge is selected (Possible feature to be testd): + plane = EnumProperty( + name="Plane", + items=[("XY", "X-Y Plane", "Use the X-Y plane as the plane of creation"), + ("XZ", "X-Z Plane", "Use the X-Z plane as the plane of creation"), + ("YZ", "Y-Z Plane", "Use the Y-Z plane as the plane of creation")], + default="XY" + ) + + def draw(self, context): + layout = self.layout + + layout.label("Creation:") + split = layout.split() + col = split.column() + + col.prop(self, "vert1", toggle=True) + col.prop(self, "vert2", toggle=True) + + col = split.column() + col.prop(self, "vert3", toggle=True) + col.prop(self, "vert4", toggle=True) + + layout.label("Direction:") + row = layout.row(align=False) + row.alignment = 'EXPAND' + row.prop(self, "pos") + row.prop(self, "neg") + + layout.separator() + layout.prop(self, "angle") + layout.prop(self, "length") + + @classmethod + def poll(cls, context): + ob = context.active_object + return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') + + def invoke(self, context, event): + return self.execute(context) + + def execute(self, context): + me = context.object.data + bm = bmesh.from_edit_mesh(me) + bm.normal_update() + + bVerts = bm.verts + bEdges = bm.edges + edges = [e for e in bEdges if e.select] + vectors = [] + + # Until I can figure out a better way of handling it: + if len(edges) < 2: + bpy.ops.object.editmode_toggle() + self.report({'ERROR_INVALID_INPUT'}, "You must select two edges") + return {'CANCELLED'} + + verts = [edges[0].verts[0], + edges[0].verts[1], + edges[1].verts[0], + edges[1].verts[1]] + + cos = intersect_line_line(verts[0].co, verts[1].co, verts[2].co, verts[3].co) + + # If the two edges are parallel: + if cos is None: + self.report({'WARNING'}, + "Selected lines are parallel: results may be unpredictable") + vectors.append(verts[0].co - verts[1].co) + vectors.append(verts[0].co - verts[2].co) + vectors.append(vectors[0].cross(vectors[1])) + vectors.append(vectors[2].cross(vectors[0])) + vectors.append(-vectors[3]) + else: + # Warn the user if they have not chosen two planar edges: + if not is_same_co(cos[0], cos[1]): + self.report({'WARNING'}, + "Selected lines are not planar: results may be unpredictable") + + # This makes the +/- behavior predictable: + if (verts[0].co - cos[0]).length < (verts[1].co - cos[0]).length: + verts[0], verts[1] = verts[1], verts[0] + if (verts[2].co - cos[0]).length < (verts[3].co - cos[0]).length: + verts[2], verts[3] = verts[3], verts[2] + + vectors.append(verts[0].co - verts[1].co) + vectors.append(verts[2].co - verts[3].co) + + # Normal of the plane formed by vector1 and vector2: + vectors.append(vectors[0].cross(vectors[1])) + + # Possible directions: + vectors.append(vectors[2].cross(vectors[0])) + vectors.append(vectors[1].cross(vectors[2])) + + # Set the length: + vectors[3].length = self.length + vectors[4].length = self.length + + # Perform any additional rotations: + matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2]) + vectors.append(matrix * -vectors[3]) # vectors[5] + matrix = Matrix.Rotation(radians(90 - self.angle), 3, vectors[2]) + vectors.append(matrix * vectors[4]) # vectors[6] + vectors.append(matrix * vectors[3]) # vectors[7] + matrix = Matrix.Rotation(radians(90 + self.angle), 3, vectors[2]) + vectors.append(matrix * -vectors[4]) # vectors[8] + + # Perform extrusions and displacements: + # There will be a total of 8 extrusions. One for each vert of each edge. + # It looks like an extrusion will add the new vert to the end of the verts + # list and leave the rest in the same location. + # ----------- EDIT ----------- + # It looks like I might be able to do this within "bpy.data" with the ".add" function + + for v in range(len(verts)): + vert = verts[v] + if ((v == 0 and self.vert1) or (v == 1 and self.vert2) or + (v == 2 and self.vert3) or (v == 3 and self.vert4)): + + if self.pos: + new = bVerts.new() + new.co = vert.co - vectors[5 + (v // 2) + ((v % 2) * 2)] + bVerts.ensure_lookup_table() + bEdges.new((vert, new)) + bEdges.ensure_lookup_table() + if self.neg: + new = bVerts.new() + new.co = vert.co + vectors[5 + (v // 2) + ((v % 2) * 2)] + bVerts.ensure_lookup_table() + bEdges.new((vert, new)) + bEdges.ensure_lookup_table() + + bmesh.update_edit_mesh(me) + + return {'FINISHED'} + + +# Usage: +# Select an edge and a point or an edge and specify the radius (default is 1 BU) +# You can select two edges but it might be unpredictable which edge it revolves +# around so you might have to play with the switch + +class Shaft(Operator): + bl_idname = "mesh.edgetools_shaft" + bl_label = "Shaft" + bl_description = "Create a shaft mesh around an axis" + bl_options = {'REGISTER', 'UNDO'} + + # Selection defaults: + shaftType = 0 + + # For tracking if the user has changed selection: + last_edge = IntProperty( + name="Last Edge", + description="Tracks if user has changed selected edges", + min=0, max=1, + default=0 + ) + last_flip = False + + edge = IntProperty( + name="Edge", + description="Edge to shaft around", + min=0, max=1, + default=0 + ) + flip = BoolProperty( + name="Flip Second Edge", + description="Flip the percieved direction of the second edge", + default=False + ) + radius = FloatProperty( + name="Radius", + description="Shaft Radius", + min=0.0, max=1024.0, + default=1.0 + ) + start = FloatProperty( + name="Starting Angle", + description="Angle to start the shaft at", + min=-360.0, max=360.0, + default=0.0 + ) + finish = FloatProperty( + name="Ending Angle", + description="Angle to end the shaft at", + min=-360.0, max=360.0, + default=360.0 + ) + segments = IntProperty( + name="Shaft Segments", + description="Number of segments to use in the shaft", + min=1, max=4096, + soft_max=512, + default=32 + ) + + def draw(self, context): + layout = self.layout + + if self.shaftType == 0: + layout.prop(self, "edge") + layout.prop(self, "flip") + elif self.shaftType == 3: + layout.prop(self, "radius") + + layout.prop(self, "segments") + layout.prop(self, "start") + layout.prop(self, "finish") + + @classmethod + def poll(cls, context): + ob = context.active_object + return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') + + def invoke(self, context, event): + # Make sure these get reset each time we run: + self.last_edge = 0 + self.edge = 0 + + return self.execute(context) + + def execute(self, context): + me = context.object.data + bm = bmesh.from_edit_mesh(me) + bm.normal_update() + + bFaces = bm.faces + bEdges = bm.edges + bVerts = bm.verts + + active = None + edges, verts = [], [] + + # Pre-caclulated values: + rotRange = [radians(self.start), radians(self.finish)] + rads = radians((self.finish - self.start) / self.segments) + + numV = self.segments + 1 + numE = self.segments + + edges = [e for e in bEdges if e.select] + + # Robustness check: there should at least be one edge selected + if len(edges) < 1: + bpy.ops.object.editmode_toggle() + self.report({'ERROR_INVALID_INPUT'}, + "At least one edge must be selected") + return {'CANCELLED'} + + # If two edges are selected: + if len(edges) == 2: + # default: + edge = [0, 1] + vert = [0, 1] + + # By default, we want to shaft around the last selected edge (it + # will be the active edge). We know we are using the default if + # the user has not changed which edge is being shafted around (as + # is tracked by self.last_edge). When they are not the same, then + # the user has changed selection. + # We then need to make sure that the active object really is an edge + # (robustness check) + # Finally, if the active edge is not the inital one, we flip them + # and have the GUI reflect that + if self.last_edge == self.edge: + if isinstance(bm.select_history.active, bmesh.types.BMEdge): + if bm.select_history.active != edges[edge[0]]: + self.last_edge, self.edge = edge[1], edge[1] + edge = [edge[1], edge[0]] + else: + bpy.ops.object.editmode_toggle() + self.report({'ERROR_INVALID_INPUT'}, + "Active geometry is not an edge") + return {'CANCELLED'} + elif self.edge == 1: + edge = [1, 0] + + verts.append(edges[edge[0]].verts[0]) + verts.append(edges[edge[0]].verts[1]) + + if self.flip: + verts = [1, 0] + + verts.append(edges[edge[1]].verts[vert[0]]) + verts.append(edges[edge[1]].verts[vert[1]]) + + self.shaftType = 0 + # If there is more than one edge selected: + # There are some issues with it ATM, so don't expose is it to normal users + # @todo Fix edge connection ordering issue + elif ENABLE_DEBUG and len(edges) > 2: + if isinstance(bm.select_history.active, bmesh.types.BMEdge): + active = bm.select_history.active + edges.remove(active) + # Get all the verts: + # edges = order_joined_edges(edges[0]) + verts = [] + for e in edges: + if verts.count(e.verts[0]) == 0: + verts.append(e.verts[0]) + if verts.count(e.verts[1]) == 0: + verts.append(e.verts[1]) + else: + bpy.ops.object.editmode_toggle() + self.report({'ERROR_INVALID_INPUT'}, + "Active geometry is not an edge") + return {'CANCELLED'} + self.shaftType = 1 + else: + verts.append(edges[0].verts[0]) + verts.append(edges[0].verts[1]) + + for v in bVerts: + if v.select and verts.count(v) == 0: + verts.append(v) + v.select = False + if len(verts) == 2: + self.shaftType = 3 + else: + self.shaftType = 2 + + # The vector denoting the axis of rotation: + if self.shaftType == 1: + axis = active.verts[1].co - active.verts[0].co + else: + axis = verts[1].co - verts[0].co + + # We will need a series of rotation matrices. We could use one which + # would be faster but also might cause propagation of error + # matrices = [] + # for i in range(numV): + # matrices.append(Matrix.Rotation((rads * i) + rotRange[0], 3, axis)) + matrices = [Matrix.Rotation((rads * i) + rotRange[0], 3, axis) for i in range(numV)] + + # New vertice coordinates: + verts_out = [] + + # If two edges were selected: + # - If the lines are not parallel, then it will create a cone-like shaft + if self.shaftType == 0: + for i in range(len(verts) - 2): + init_vec = distance_point_line(verts[i + 2].co, verts[0].co, verts[1].co) + co = init_vec + verts[i + 2].co + # These will be rotated about the orgin so will need to be shifted: + for j in range(numV): + verts_out.append(co - (matrices[j] * init_vec)) + elif self.shaftType == 1: + for i in verts: + init_vec = distance_point_line(i.co, active.verts[0].co, active.verts[1].co) + co = init_vec + i.co + # These will be rotated about the orgin so will need to be shifted: + for j in range(numV): + verts_out.append(co - (matrices[j] * init_vec)) + # Else if a line and a point was selected: + elif self.shaftType == 2: + init_vec = distance_point_line(verts[2].co, verts[0].co, verts[1].co) + # These will be rotated about the orgin so will need to be shifted: + verts_out = [(verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)] + else: + # Else the above are not possible, so we will just use the edge: + # - The vector defined by the edge is the normal of the plane for the shaft + # - The shaft will have radius "radius" + if is_axial(verts[0].co, verts[1].co) is None: + proj = (verts[1].co - verts[0].co) + proj[2] = 0 + norm = proj.cross(verts[1].co - verts[0].co) + vec = norm.cross(verts[1].co - verts[0].co) + vec.length = self.radius + elif is_axial(verts[0].co, verts[1].co) == 'Z': + vec = verts[0].co + Vector((0, 0, self.radius)) + else: + vec = verts[0].co + Vector((0, self.radius, 0)) + init_vec = distance_point_line(vec, verts[0].co, verts[1].co) + # These will be rotated about the orgin so will need to be shifted: + verts_out = [(verts[i].co - (matrices[j] * init_vec)) for i in range(2) for j in range(numV)] + + # We should have the coordinates for a bunch of new verts + # Now add the verts and build the edges and then the faces + + newVerts = [] + + if self.shaftType == 1: + # Vertices: + for i in range(numV * len(verts)): + new = bVerts.new() + new.co = verts_out[i] + bVerts.ensure_lookup_table() + new.select = True + newVerts.append(new) + # Edges: + for i in range(numE): + for j in range(len(verts)): + e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * j) + 1])) + bEdges.ensure_lookup_table() + e.select = True + for i in range(numV): + for j in range(len(verts) - 1): + e = bEdges.new((newVerts[i + (numV * j)], newVerts[i + (numV * (j + 1))])) + bEdges.ensure_lookup_table() + e.select = True + + # Faces: There is a problem with this right now + """ + for i in range(len(edges)): + for j in range(numE): + f = bFaces.new((newVerts[i], newVerts[i + 1], + newVerts[i + (numV * j) + 1], newVerts[i + (numV * j)])) + f.normal_update() + """ + else: + # Vertices: + for i in range(numV * 2): + new = bVerts.new() + new.co = verts_out[i] + new.select = True + bVerts.ensure_lookup_table() + newVerts.append(new) + # Edges: + for i in range(numE): + e = bEdges.new((newVerts[i], newVerts[i + 1])) + e.select = True + bEdges.ensure_lookup_table() + e = bEdges.new((newVerts[i + numV], newVerts[i + numV + 1])) + e.select = True + bEdges.ensure_lookup_table() + for i in range(numV): + e = bEdges.new((newVerts[i], newVerts[i + numV])) + e.select = True + bEdges.ensure_lookup_table() + # Faces: + for i in range(numE): + f = bFaces.new((newVerts[i], newVerts[i + 1], + newVerts[i + numV + 1], newVerts[i + numV])) + bFaces.ensure_lookup_table() + f.normal_update() + + bmesh.update_edit_mesh(me) + + return {'FINISHED'} + + +# "Slices" edges crossing a plane defined by a face + +class Slice(Operator): + bl_idname = "mesh.edgetools_slice" + bl_label = "Slice" + bl_description = "Cut edges at the plane defined by a selected face" + bl_options = {'REGISTER', 'UNDO'} + + make_copy = BoolProperty( + name="Make Copy", + description="Make new vertices at intersection points instead of spliting the edge", + default=False + ) + rip = BoolProperty( + name="Rip", + description="Split into two edges that DO NOT share an intersection vertex", + default=True + ) + pos = BoolProperty( + name="Positive", + description="Remove the portion on the side of the face normal", + default=False + ) + neg = BoolProperty( + name="Negative", + description="Remove the portion on the side opposite of the face normal", + default=False + ) + + def draw(self, context): + layout = self.layout + + layout.prop(self, "make_copy") + if not self.make_copy: + layout.prop(self, "rip") + layout.label("Remove Side:") + layout.prop(self, "pos") + layout.prop(self, "neg") + + @classmethod + def poll(cls, context): + ob = context.active_object + return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') + + def invoke(self, context, event): + return self.execute(context) + + def execute(self, context): + me = context.object.data + bm = bmesh.from_edit_mesh(me) + bm.normal_update() + + bVerts = bm.verts + bEdges = bm.edges + bFaces = bm.faces + + face, normal = None, None + + # Find the selected face. This will provide the plane to project onto: + # - First check to use the active face. Allows users to just + # select a bunch of faces with the last being the cutting plane + # - If that fails, then use the first found selected face in the BMesh face list + if isinstance(bm.select_history.active, bmesh.types.BMFace): + face = bm.select_history.active + normal = bm.select_history.active.normal + bm.select_history.active.select = False + else: + for f in bFaces: + if f.select: + face = f + normal = f.normal + f.select = False + break + + # If we don't find a selected face exit: + if face is None: + bpy.ops.object.editmode_toggle() + self.report({'ERROR_INVALID_INPUT'}, + "You must select a face as the cutting plane") + return {'CANCELLED'} + + # Warn the user if they are using an n-gon might lead to some odd results + elif len(face.verts) > 4 and not is_face_planar(face): + self.report({'WARNING'}, + "Selected face is an N-gon. Results may be unpredictable") + + if ENABLE_DEBUG: + dbg = 0 + print("Number of Edges: ", len(bEdges)) + + for e in bEdges: + if ENABLE_DEBUG: + print("Looping through Edges - ", dbg) + dbg = dbg + 1 + + # Get the end verts on the edge: + v1 = e.verts[0] + v2 = e.verts[1] + + # Make sure that verts are not a part of the cutting plane: + if e.select and (v1 not in face.verts and v2 not in face.verts): + if len(face.verts) < 5: # Not an n-gon + intersection = intersect_line_face(e, face, True) + else: + intersection = intersect_line_plane(v1.co, v2.co, face.verts[0].co, normal) + + if ENABLE_DEBUG: + print("Intersection: ", intersection) + + # If an intersection exists find the distance of each of the end + # points from the plane, with "positive" being in the direction + # of the cutting plane's normal. If the points are on opposite + # side of the plane, then it intersects and we need to cut it + if intersection is not None: + bVerts.ensure_lookup_table() + bEdges.ensure_lookup_table() + bFaces.ensure_lookup_table() + + d1 = distance_point_to_plane(v1.co, face.verts[0].co, normal) + d2 = distance_point_to_plane(v2.co, face.verts[0].co, normal) + # If they have different signs, then the edge crosses the cutting plane: + if abs(d1 + d2) < abs(d1 - d2): + # Make the first vertex the positive one: + if d1 < d2: + v2, v1 = v1, v2 + + if self.make_copy: + new = bVerts.new() + new.co = intersection + new.select = True + bVerts.ensure_lookup_table() + elif self.rip: + if ENABLE_DEBUG: + print("Branch rip engaged") + newV1 = bVerts.new() + newV1.co = intersection + bVerts.ensure_lookup_table() + if ENABLE_DEBUG: + print("newV1 created", end='; ') + + newV2 = bVerts.new() + newV2.co = intersection + bVerts.ensure_lookup_table() + + if ENABLE_DEBUG: + print("newV2 created", end='; ') + + newE1 = bEdges.new((v1, newV1)) + newE2 = bEdges.new((v2, newV2)) + bEdges.ensure_lookup_table() + + if ENABLE_DEBUG: + print("new edges created", end='; ') + + if e.is_valid: + bEdges.remove(e) + + bEdges.ensure_lookup_table() + + if ENABLE_DEBUG: + print("Old edge removed.\nWe're done with this edge") + else: + new = list(bmesh.utils.edge_split(e, v1, 0.5)) + bEdges.ensure_lookup_table() + new[1].co = intersection + e.select = False + new[0].select = False + if self.pos: + bEdges.remove(new[0]) + if self.neg: + bEdges.remove(e) + bEdges.ensure_lookup_table() + + if ENABLE_DEBUG: + print("The Edge Loop has exited. Now to update the bmesh") + dbg = 0 + + bmesh.update_edit_mesh(me) + + return {'FINISHED'} + + +# This projects the selected edges onto the selected plane +# and/or both points on the selected edge + +class Project(Operator): + bl_idname = "mesh.edgetools_project" + bl_label = "Project" + bl_description = ("Projects the selected Vertices/Edges onto a selected plane\n" + "(Active is projected onto the rest)") + bl_options = {'REGISTER', 'UNDO'} + + make_copy = BoolProperty( + name="Make Copy", + description="Make duplicates of the vertices instead of altering them", + default=False + ) + + def draw(self, context): + layout = self.layout + layout.prop(self, "make_copy") + + @classmethod + def poll(cls, context): + ob = context.active_object + return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') + + def invoke(self, context, event): + return self.execute(context) + + def execute(self, context): + me = context.object.data + bm = bmesh.from_edit_mesh(me) + bm.normal_update() + + bFaces = bm.faces + bVerts = bm.verts + + fVerts = [] + + # Find the selected face. This will provide the plane to project onto: + # @todo Check first for an active face + for f in bFaces: + if f.select: + for v in f.verts: + fVerts.append(v) + normal = f.normal + f.select = False + break + + for v in bVerts: + if v.select: + if v in fVerts: + v.select = False + continue + d = distance_point_to_plane(v.co, fVerts[0].co, normal) + if self.make_copy: + temp = v + v = bVerts.new() + v.co = temp.co + bVerts.ensure_lookup_table() + vector = normal + vector.length = abs(d) + v.co = v.co - (vector * sign(d)) + v.select = False + + bmesh.update_edit_mesh(me) + + return {'FINISHED'} + + +# Project_End is for projecting/extending an edge to meet a plane +# This is used be selecting a face to define the plane then all the edges +# Then move the vertices in the edge that is closest to the +# plane to the coordinates of the intersection of the edge and the plane + +class Project_End(Operator): + bl_idname = "mesh.edgetools_project_end" + bl_label = "Project (End Point)" + bl_description = ("Projects the vertices of the selected\n" + "edges closest to a plane onto that plane") + bl_options = {'REGISTER', 'UNDO'} + + make_copy = BoolProperty( + name="Make Copy", + description="Make a duplicate of the vertice instead of moving it", + default=False + ) + keep_length = BoolProperty( + name="Keep Edge Length", + description="Maintain edge lengths", + default=False + ) + use_force = BoolProperty( + name="Use opposite vertices", + description="Force the usage of the vertices at the other end of the edge", + default=False + ) + use_normal = BoolProperty( + name="Project along normal", + description="Use the plane's normal as the projection direction", + default=False + ) + + def draw(self, context): + layout = self.layout + + if not self.keep_length: + layout.prop(self, "use_normal") + layout.prop(self, "make_copy") + layout.prop(self, "use_force") + + @classmethod + def poll(cls, context): + ob = context.active_object + return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') + + def invoke(self, context, event): + return self.execute(context) + + def execute(self, context): + me = context.object.data + bm = bmesh.from_edit_mesh(me) + bm.normal_update() + + bFaces = bm.faces + bEdges = bm.edges + bVerts = bm.verts + + fVerts = [] + + # Find the selected face. This will provide the plane to project onto: + for f in bFaces: + if f.select: + for v in f.verts: + fVerts.append(v) + normal = f.normal + f.select = False + break + + for e in bEdges: + if e.select: + v1 = e.verts[0] + v2 = e.verts[1] + if v1 in fVerts or v2 in fVerts: + e.select = False + continue + intersection = intersect_line_plane(v1.co, v2.co, fVerts[0].co, normal) + if intersection is not None: + # Use abs because we don't care what side of plane we're on: + d1 = distance_point_to_plane(v1.co, fVerts[0].co, normal) + d2 = distance_point_to_plane(v2.co, fVerts[0].co, normal) + # If d1 is closer than we use v1 as our vertice: + # "xor" with 'use_force': + if (abs(d1) < abs(d2)) is not self.use_force: + if self.make_copy: + v1 = bVerts.new() + v1.co = e.verts[0].co + bVerts.ensure_lookup_table() + bEdges.ensure_lookup_table() + if self.keep_length: + v1.co = intersection + elif self.use_normal: + vector = normal + vector.length = abs(d1) + v1.co = v1.co - (vector * sign(d1)) + else: + v1.co = intersection + else: + if self.make_copy: + v2 = bVerts.new() + v2.co = e.verts[1].co + bVerts.ensure_lookup_table() + bEdges.ensure_lookup_table() + if self.keep_length: + v2.co = intersection + elif self.use_normal: + vector = normal + vector.length = abs(d2) + v2.co = v2.co - (vector * sign(d2)) + else: + v2.co = intersection + e.select = False + + bmesh.update_edit_mesh(me) + + return {'FINISHED'} + + +# For testing the mess that is "intersect_line_face" for possible math errors +# This will NOT be directly exposed to end users: it will always require +# switching the ENABLE_DEBUG global to True +# So far no errors have been found. Thanks to anyone who tests and reports bugs! +# @todo: this is will be removed before final merge + +class Intersect_Line_Face(Operator): + bl_idname = "mesh.edgetools_ilf" + bl_label = "Interesect Line Face Test" + bl_description = "TEST ONLY: Intersect line face" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + @classmethod + def poll(cls, context): + ob = context.active_object + return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH') + + def invoke(self, context, event): + return self.execute(context) + + def execute(self, context): + # Switch to true to have access to this operator: + if not ENABLE_DEBUG: + self.report({'ERROR_INVALID_INPUT'}, + "This is for debugging only: you should not be able to run this!") + return {'CANCELLED'} + + me = context.object.data + bm = bmesh.from_edit_mesh(me) + bm.normal_update() + + bFaces = bm.faces + bEdges = bm.edges + bVerts = bm.verts + + face = None + for f in bFaces: + if f.select: + face = f + break + + edge = None + for e in bEdges: + if e.select and e not in face.edges: + edge = e + break + + point = intersect_line_face(edge, face, True) + + if point is not None: + new = bVerts.new() + new.co = point + bVerts.ensure_lookup_table() + else: + bpy.ops.object.editmode_toggle() + self.report({'ERROR_INVALID_INPUT'}, "point was \"None\"") + return {'CANCELLED'} + + bmesh.update_edit_mesh(me) + + return {'FINISHED'} + + +class VIEW3D_MT_edit_mesh_edgetools(Menu): + bl_label = "Edge Tools" + bl_description = "Various tools for manipulating edges" + + def draw(self, context): + layout = self.layout + + layout.operator("mesh.edgetools_extend") + layout.operator("mesh.edgetools_spline") + layout.operator("mesh.edgetools_ortho") + layout.operator("mesh.edgetools_shaft") + layout.operator("mesh.edgetools_slice") + layout.separator() + + layout.operator("mesh.edgetools_project") + layout.operator("mesh.edgetools_project_end") + if ENABLE_DEBUG: + # For internal testing ONLY: + layout.separator() + layout.operator("mesh.edgetools_ilf") + + +# define classes for registration +classes = [ + VIEW3D_MT_edit_mesh_edgetools, + Extend, + Spline, + Ortho, + Shaft, + Slice, + Project, + Project_End, + Intersect_Line_Face + ] + + +# registering and menu integration +def register(): + for c in classes: + bpy.utils.register_class(c) + + +# unregistering and removing menus +def unregister(): + for c in classes: + bpy.utils.unregister_class(c) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_extrude_and_reshape.py b/mesh_extra_tools/mesh_extrude_and_reshape.py new file mode 100644 index 00000000..14d9d744 --- /dev/null +++ b/mesh_extra_tools/mesh_extrude_and_reshape.py @@ -0,0 +1,365 @@ +### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# ##### END GPL LICENSE BLOCK ##### + +# Contact for more information about the Addon: +# Email: germano.costa@ig.com.br +# Twitter: wii_mano @mano_wii + +bl_info = { + "name": "Extrude and Reshape", + "author": "Germano Cavalcante", + "version": (0, 8, 1), + "blender": (2, 76, 5), + "location": "View3D > TOOLS > Tools > Mesh Tools > Add: > Extrude Menu (Alt + E)", + "description": "Extrude face and merge edge intersections between the mesh and the new edges", + "wiki_url" : "http://blenderartists.org/forum/showthread.php?376618-Addon-Push-Pull-Face", + "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/", + "category": "Mesh"} + +import bpy, bmesh +from mathutils.geometry import intersect_line_line +from bpy.props import FloatProperty + +class BVHco(): + i = 0 + c1x = 0.0 + c1y = 0.0 + c1z = 0.0 + c2x = 0.0 + c2y = 0.0 + c2z = 0.0 + +def edges_BVH_overlap(bm, edges, epsilon = 0.0001): + bco = set() + for e in edges: + bvh = BVHco() + bvh.i = e.index + b1 = e.verts[0] + b2 = e.verts[1] + co1 = b1.co.x + co2 = b2.co.x + if co1 <= co2: + bvh.c1x = co1 - epsilon + bvh.c2x = co2 + epsilon + else: + bvh.c1x = co2 - epsilon + bvh.c2x = co1 + epsilon + co1 = b1.co.y + co2 = b2.co.y + if co1 <= co2: + bvh.c1y = co1 - epsilon + bvh.c2y = co2 + epsilon + else: + bvh.c1y = co2 - epsilon + bvh.c2y = co1 + epsilon + co1 = b1.co.z + co2 = b2.co.z + if co1 <= co2: + bvh.c1z = co1 - epsilon + bvh.c2z = co2 + epsilon + else: + bvh.c1z = co2 - epsilon + bvh.c2z = co1 + epsilon + bco.add(bvh) + del edges + overlap = {} + oget = overlap.get + for e1 in bm.edges: + by = bz = True + a1 = e1.verts[0] + a2 = e1.verts[1] + c1x = a1.co.x + c2x = a2.co.x + if c1x > c2x: + tm = c1x + c1x = c2x + c2x = tm + for bvh in bco: + if c1x <= bvh.c2x and c2x >= bvh.c1x: + if by: + by = False + c1y = a1.co.y + c2y = a2.co.y + if c1y > c2y: + tm = c1y + c1y = c2y + c2y = tm + if c1y <= bvh.c2y and c2y >= bvh.c1y: + if bz: + bz = False + c1z = a1.co.z + c2z = a2.co.z + if c1z > c2z: + tm = c1z + c1z = c2z + c2z = tm + if c1z <= bvh.c2z and c2z >= bvh.c1z: + e2 = bm.edges[bvh.i] + if e1 != e2: + overlap[e1] = oget(e1, set()).union({e2}) + return overlap + +def intersect_edges_edges(overlap, precision = 4): + epsilon = .1**precision + fpre_min = -epsilon + fpre_max = 1+epsilon + splits = {} + sp_get = splits.get + new_edges1 = set() + new_edges2 = set() + targetmap = {} + for edg1 in overlap: + #print("***", ed1.index, "***") + for edg2 in overlap[edg1]: + #print('loop', ed2.index) + a1 = edg1.verts[0] + a2 = edg1.verts[1] + b1 = edg2.verts[0] + b2 = edg2.verts[1] + + # test if are linked + if a1 in {b1, b2} or a2 in {b1, b2}: + #print('linked') + continue + + aco1, aco2 = a1.co, a2.co + bco1, bco2 = b1.co, b2.co + tp = intersect_line_line(aco1, aco2, bco1, bco2) + if tp: + p1, p2 = tp + if (p1 - p2).to_tuple(precision) == (0,0,0): + v = aco2-aco1 + f = p1 - aco1 + x,y,z = abs(v.x), abs(v.y), abs(v.z) + max1 = 0 if x >= y and x >= z else\ + 1 if y >= x and y >= z else 2 + fac1 = f[max1]/v[max1] + + v = bco2-bco1 + f = p2 - bco1 + x,y,z = abs(v.x), abs(v.y), abs(v.z) + max2 = 0 if x >= y and x >= z else\ + 1 if y >= x and y >= z else 2 + fac2 = f[max2]/v[max2] + + if fpre_min <= fac1 <= fpre_max: + #print(edg1.index, 'can intersect', edg2.index) + ed1 = edg1 + + elif edg1 in splits: + for ed1 in splits[edg1]: + a1 = ed1.verts[0] + a2 = ed1.verts[1] + + vco1 = a1.co + vco2 = a2.co + + v = vco2-vco1 + f = p1 - vco1 + fac1 = f[max1]/v[max1] + if fpre_min <= fac1 <= fpre_max: + #print(e.index, 'can intersect', edg2.index) + break + else: + #print(edg1.index, 'really does not intersect', edg2.index) + continue + else: + #print(edg1.index, 'not intersect', edg2.index) + continue + + if fpre_min <= fac2 <= fpre_max: + #print(ed1.index, 'actually intersect', edg2.index) + ed2 = edg2 + + elif edg2 in splits: + for ed2 in splits[edg2]: + b1 = ed2.verts[0] + b2 = ed2.verts[1] + + vco1 = b1.co + vco2 = b2.co + + v = vco2-vco1 + f = p2 - vco1 + fac2 = f[max2]/v[max2] + if fpre_min <= fac2 <= fpre_max: + #print(ed1.index, 'actually intersect', e.index) + break + else: + #print(ed1.index, 'really does not intersect', ed2.index) + continue + else: + #print(ed1.index, 'not intersect', edg2.index) + continue + + new_edges1.add(ed1) + new_edges2.add(ed2) + + if abs(fac1) <= epsilon: + nv1 = a1 + elif fac1 + epsilon >= 1: + nv1 = a2 + else: + ne1, nv1 = bmesh.utils.edge_split(ed1, a1, fac1) + new_edges1.add(ne1) + splits[edg1] = sp_get(edg1, set()).union({ne1}) + + if abs(fac2) <= epsilon: + nv2 = b1 + elif fac2 + epsilon >= 1: + nv2 = b2 + else: + ne2, nv2 = bmesh.utils.edge_split(ed2, b1, fac2) + new_edges2.add(ne2) + splits[edg2] = sp_get(edg2, set()).union({ne2}) + + if nv1 != nv2: #necessary? + targetmap[nv1] = nv2 + #else: + #print('not coplanar') + #else: + #print("parallel or collinear") + return new_edges1, new_edges2, targetmap + +class Extrude_and_Reshape(bpy.types.Operator): + """Push and pull face entities to sculpt 3d models""" + bl_idname = "mesh.extrude_reshape" + bl_label = "Extrude and Reshape" + bl_options = {'REGISTER', 'GRAB_CURSOR', 'BLOCKING'} + + @classmethod + def poll(cls, context): + return context.mode is not 'EDIT_MESH' + + def modal(self, context, event): + if self.confirm: + sface = self.bm.faces.active + if not sface: + for face in self.bm.faces: + if face.select == True: + sface = face + break + else: + return {'FINISHED'} + # edges to intersect + edges = set() + [[edges.add(ed) for ed in v.link_edges] for v in sface.verts] + + overlap = edges_BVH_overlap(self.bm, edges, epsilon = 0.0001) + overlap = {k: v for k,v in overlap.items() if k not in edges} # remove repetition + + #print([e.index for e in edges]) + #for a, b in overlap.items(): + #print(a.index, [e.index for e in b]) + + new_edges1, new_edges2, targetmap = intersect_edges_edges(overlap) + pos_weld = set() + for e in new_edges1: + v1, v2 = e.verts + if v1 in targetmap and v2 in targetmap: + pos_weld.add((targetmap[v1], targetmap[v2])) + if targetmap: + bmesh.ops.weld_verts(self.bm, targetmap=targetmap) + #print([e.is_valid for e in new_edges1]) + #print([e.is_valid for e in new_edges2]) + #sp_faces1 = set() + for e in pos_weld: + v1, v2 = e + lf1 = set(v1.link_faces) + lf2 = set(v2.link_faces) + rlfe = lf1.intersection(lf2) + for f in rlfe: + try: + nf = bmesh.utils.face_split(f, v1, v2) + #sp_faces1.update({f, nf[0]}) + except: + pass + + #sp_faces2 = set() + for e in new_edges2: + lfe = set(e.link_faces) + v1, v2 = e.verts + lf1 = set(v1.link_faces) + lf2 = set(v2.link_faces) + rlfe = lf1.intersection(lf2) + for f in rlfe.difference(lfe): + nf = bmesh.utils.face_split(f, v1, v2) + #sp_faces2.update({f, nf[0]}) + + bmesh.update_edit_mesh(self.mesh, tessface=True, destructive=True) + return {'FINISHED'} + if self.cancel: + return {'FINISHED'} + self.cancel = event.type in {'ESC', 'NDOF_BUTTON_ESC'} + self.confirm = event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'} + return {'PASS_THROUGH'} + + def execute(self, context): + self.mesh = context.object.data + self.bm = bmesh.from_edit_mesh(self.mesh) + try: + selection = self.bm.select_history[-1] + except: + for face in self.bm.faces: + if face.select == True: + selection = face + break + else: + return {'FINISHED'} + if not isinstance(selection, bmesh.types.BMFace): + bpy.ops.mesh.extrude_region_move('INVOKE_DEFAULT') + return {'FINISHED'} + else: + face = selection + #face.select = False + bpy.ops.mesh.select_all(action='DESELECT') + geom = [] + for edge in face.edges: + if abs(edge.calc_face_angle(0) - 1.5707963267948966) < 0.01: #self.angle_tolerance: + geom.append(edge) + + ret_dict = bmesh.ops.extrude_discrete_faces(self.bm, faces = [face]) + + for face in ret_dict['faces']: + self.bm.faces.active = face + face.select = True + sface = face + dfaces = bmesh.ops.dissolve_edges(self.bm, edges = geom, use_verts=True, use_face_split=False) + bmesh.update_edit_mesh(self.mesh, tessface=True, destructive=True) + bpy.ops.transform.translate('INVOKE_DEFAULT', constraint_axis=(False, False, True), constraint_orientation='NORMAL', release_confirm=True) + + context.window_manager.modal_handler_add(self) + + self.cancel = False + self.confirm = False + return {'RUNNING_MODAL'} + +def operator_draw(self,context): + layout = self.layout + col = layout.column(align=True) + col.operator("mesh.extrude_reshape", text="Extrude and Reshape") + +def register(): + bpy.utils.register_class(Extrude_and_Reshape) + bpy.types.VIEW3D_MT_edit_mesh_extrude.append(operator_draw) + +def unregister(): + bpy.types.VIEW3D_MT_edit_mesh_extrude.remove(operator_draw) + bpy.utils.unregister_class(Extrude_and_Reshape) + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_fastloop.py b/mesh_extra_tools/mesh_fastloop.py new file mode 100644 index 00000000..81b9ffb7 --- /dev/null +++ b/mesh_extra_tools/mesh_fastloop.py @@ -0,0 +1,113 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +bl_info = { + "name": "Fast Loop", + "description": "Add loops fast", + "author": "Andy Davies (metalliandy)", + "version": (0, 16), + "blender": (2, 5, 6), + "location": "Tool Shelf", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh" + } + +""" +About this script:- +This script enables the fast creation of multiple loops on a mesh + +Usage:- +1)Click the FastLoop button on the Tool Shelf to activate the tool +2)Hover over the mesh in the general area where you would like a loop to be added + (shown by a highlight on the mesh) +3)Click once to confirm the loop placement +4)place the loop and then slide to fine tune its position +5)Repeat 1-4 if needed +6)Press Esc. twice to exit the tool + +Related Links:- +http://blenderartists.org/forum/showthread.php?t=206989 +http://www.metalliandy.com + +Thanks to:- +Bartius Crouch (Crouch) - http://sites.google.com/site/bartiuscrouch/ +Dealga McArdle (zeffii) - http://www.digitalaphasia.com + +Version history:- +v0.16 - Ammended script for compatibility with recent API changes +v0.15 - Ammended script meta information and button rendering code for + compatibility with recent API changes +v0.14 - Modal operator +v0.13 - Initial revision +""" + +import bpy +from bpy.types import Operator +from bpy.props import BoolProperty + + +class OBJECT_OT_FastLoop(Operator): + bl_idname = "object_ot.fastloop" + bl_label = "FastLoop" + bl_description = ("Create multiple edge loops in succession\n" + "Runs modal until ESC is pressed twice") + + active = BoolProperty( + name="active", + default=False + ) + + @classmethod + def poll(cls, context): + return bpy.ops.mesh.loopcut_slide.poll() + + def modal(self, context, event): + if event.type == 'ESC': + context.area.header_text_set() + return {'CANCELLED'} + + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.active = False + + if not self.active: + self.active = True + bpy.ops.mesh.loopcut_slide('INVOKE_DEFAULT') + context.area.header_text_set("Press ESC twice to stop FastLoop") + + return {'RUNNING_MODAL'} + + def invoke(self, context, event): + context.window_manager.modal_handler_add(self) + + return {'RUNNING_MODAL'} + + +def register(): + bpy.utils.register_module(__name__) + pass + + +def unregister(): + bpy.utils.unregister_module(__name__) + pass + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_filletplus.py b/mesh_extra_tools/mesh_filletplus.py new file mode 100644 index 00000000..025de0b0 --- /dev/null +++ b/mesh_extra_tools/mesh_filletplus.py @@ -0,0 +1,411 @@ +# -*- coding: utf-8 -*- + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + + +bl_info = { + "name": "FilletPlus", + "author": "Gert De Roost - original by zmj100", + "version": (0, 4, 3), + "blender": (2, 61, 0), + "location": "View3D > Tool Shelf", + "description": "", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + + +import bpy +from bpy.props import ( + FloatProperty, + IntProperty, + BoolProperty, + ) +from bpy.types import Operator +import bmesh +from mathutils import Matrix +from math import cos, pi, degrees, sin, tan + + +def list_clear_(l): + if l: + del l[:] + return l + + +def get_adj_v_(list_): + tmp = {} + for i in list_: + try: + tmp[i[0]].append(i[1]) + except KeyError: + tmp[i[0]] = [i[1]] + try: + tmp[i[1]].append(i[0]) + except KeyError: + tmp[i[1]] = [i[0]] + return tmp + + +class f_buf(): + # one of the angles was not 0 or 180 + check = False + + +def fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius): + try: + dict_0 = get_adj_v_(list_0) + list_1 = [[dict_0[i][0], i, dict_0[i][1]] for i in dict_0 if (len(dict_0[i]) == 2)][0] + list_3 = [] + for elem in list_1: + list_3.append(bm.verts[elem]) + list_2 = [] + + p_ = list_3[1] + p = (list_3[1].co).copy() + p1 = (list_3[0].co).copy() + p2 = (list_3[2].co).copy() + + vec1 = p - p1 + vec2 = p - p2 + + ang = vec1.angle(vec2, any) + check_angle = round(degrees(ang)) + + if check_angle == 180 or check_angle == 0.0: + return False + else: + f_buf.check = True + + opp = adj + + if radius is False: + h = adj * (1 / cos(ang * 0.5)) + adj_ = adj + elif radius is True: + h = opp / sin(ang * 0.5) + adj_ = opp / tan(ang * 0.5) + + p3 = p - (vec1.normalized() * adj_) + p4 = p - (vec2.normalized() * adj_) + rp = p - ((p - ((p3 + p4) * 0.5)).normalized() * h) + + vec3 = rp - p3 + vec4 = rp - p4 + + axis = vec1.cross(vec2) + + if out is False: + if flip is False: + rot_ang = vec3.angle(vec4) + elif flip is True: + rot_ang = vec1.angle(vec2) + elif out is True: + rot_ang = (2 * pi) - vec1.angle(vec2) + + for j in range(n + 1): + new_angle = rot_ang * j / n + mtrx = Matrix.Rotation(new_angle, 3, axis) + if out is False: + if flip is False: + tmp = p4 - rp + tmp1 = mtrx * tmp + tmp2 = tmp1 + rp + elif flip is True: + p3 = p - (vec1.normalized() * opp) + tmp = p3 - p + tmp1 = mtrx * tmp + tmp2 = tmp1 + p + elif out is True: + p4 = p - (vec2.normalized() * opp) + tmp = p4 - p + tmp1 = mtrx * tmp + tmp2 = tmp1 + p + + v = bm.verts.new(tmp2) + list_2.append(v) + + if flip is True: + list_3[1:2] = list_2 + else: + list_2.reverse() + list_3[1:2] = list_2 + + list_clear_(list_2) + + n1 = len(list_3) + + for t in range(n1 - 1): + bm.edges.new([list_3[t], list_3[(t + 1) % n1]]) + + v = bm.verts.new(p) + bm.edges.new([v, p_]) + + bm.edges.ensure_lookup_table() + + if face is not None: + for l in face.loops: + if l.vert == list_3[0]: + startl = l + break + vertlist2 = [] + + if startl.link_loop_next.vert == startv: + l = startl.link_loop_prev + while len(vertlist) > 0: + vertlist2.insert(0, l.vert) + vertlist.pop(vertlist.index(l.vert)) + l = l.link_loop_prev + else: + l = startl.link_loop_next + while len(vertlist) > 0: + vertlist2.insert(0, l.vert) + vertlist.pop(vertlist.index(l.vert)) + l = l.link_loop_next + + for v in list_3: + vertlist2.append(v) + bm.faces.new(vertlist2) + if startv.is_valid: + bm.verts.remove(startv) + else: + print("\n[Function fillets Error]\n" + "Starting vertex (startv var) couldn't be removed\n") + return False + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + list_3[1].select = 1 + list_3[-2].select = 1 + bm.edges.get([list_3[0], list_3[1]]).select = 1 + bm.edges.get([list_3[-1], list_3[-2]]).select = 1 + bm.verts.index_update() + bm.edges.index_update() + bm.faces.index_update() + + me.update(calc_edges=True, calc_tessface=True) + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + + except Exception as e: + print("\n[Function fillets Error]\n{}\n".format(e)) + return False + + +def do_filletplus(self, pair): + is_finished = True + try: + startv = None + global inaction + global flip + list_0 = [list([e.verts[0].index, e.verts[1].index]) for e in pair] + + vertset = set([]) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + vertset.add(bm.verts[list_0[0][0]]) + vertset.add(bm.verts[list_0[0][1]]) + vertset.add(bm.verts[list_0[1][0]]) + vertset.add(bm.verts[list_0[1][1]]) + + v1, v2, v3 = vertset + + if len(list_0) != 2: + self.report({'WARNING'}, "Two adjacent edges must be selected") + is_finished = False + else: + inaction = 1 + vertlist = [] + found = 0 + for f in v1.link_faces: + if v2 in f.verts and v3 in f.verts: + found = 1 + if not found: + for v in [v1, v2, v3]: + if v.index in list_0[0] and v.index in list_0[1]: + startv = v + face = None + else: + for f in v1.link_faces: + if v2 in f.verts and v3 in f.verts: + for v in f.verts: + if not(v in vertset): + vertlist.append(v) + if (v in vertset and v.link_loops[0].link_loop_prev.vert in vertset and + v.link_loops[0].link_loop_next.vert in vertset): + startv = v + face = f + if out is True: + flip = False + if startv: + fills = fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius) + if not fills: + is_finished = False + else: + is_finished = False + except Exception as e: + print("\n[Function do_filletplus Error]\n{}\n".format(e)) + is_finished = False + return is_finished + + +def check_is_not_coplanar(bm_data): + from mathutils import Vector + check = False + angles, norm_angle = 0, 0 + z_vec = Vector((0, 0, 1)) + try: + bm_data.faces.ensure_lookup_table() + + for f in bm_data.faces: + norm_angle = f.normal.angle(z_vec) + if angles == 0: + angles = norm_angle + if angles != norm_angle: + check = True + break + except Exception as e: + print("\n[Function check_is_not_coplanar Error]\n{}\n".format(e)) + check = True + return check + + +# Operator + +class MESH_OT_fillet_plus(Operator): + bl_idname = "mesh.fillet_plus" + bl_label = "Fillet Plus" + bl_description = ("Fillet adjoining edges\n" + "Note: Works on a mesh whose all faces share the same normal") + bl_options = {"REGISTER", "UNDO"} + + adj = FloatProperty( + name="", + description="Size of the filleted corners", + default=0.1, + min=0.00001, max=100.0, + step=1, + precision=3 + ) + n = IntProperty( + name="", + description="Subdivision of the filleted corners", + default=3, + min=1, max=50, + step=1 + ) + out = BoolProperty( + name="Outside", + description="Fillet towards outside", + default=False + ) + flip = BoolProperty( + name="Flip", + description="Flip the direction of the Fillet\n" + "Only available if Outside option is not active", + default=False + ) + radius = BoolProperty( + name="Radius", + description="Use radius for the size of the filleted corners", + default=False + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH') + + def draw(self, context): + layout = self.layout + + if f_buf.check is False: + layout.label(text="Angle equal to 0 or 180", icon="INFO") + layout.label("Can not fillet") + else: + layout.prop(self, "radius") + if self.radius is True: + layout.label("Radius:") + elif self.radius is False: + layout.label("Distance:") + layout.prop(self, "adj") + layout.label("Number of sides:") + layout.prop(self, "n") + if self.n > 1: + row = layout.row(align=False) + row.prop(self, "out") + if self.out is False: + row.prop(self, "flip") + + def execute(self, context): + global inaction + global bm, me, adj, n, out, flip, radius + + adj = self.adj + n = self.n + out = self.out + flip = self.flip + radius = self.radius + + inaction = 0 + f_buf.check = False + + ob_act = context.active_object + try: + me = ob_act.data + bm = bmesh.from_edit_mesh(me) + warn_obj = bool(check_is_not_coplanar(bm)) + if warn_obj is False: + tempset = set([]) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + for v in bm.verts: + if v.select and v.is_boundary: + tempset.add(v) + for v in tempset: + edgeset = set([]) + for e in v.link_edges: + if e.select and e.is_boundary: + edgeset.add(e) + if len(edgeset) == 2: + is_finished = do_filletplus(self, edgeset) + if not is_finished: + break + + if inaction == 1: + bpy.ops.mesh.select_all(action="DESELECT") + for v in bm.verts: + if len(v.link_edges) == 0: + bm.verts.remove(v) + bpy.ops.object.editmode_toggle() + bpy.ops.object.editmode_toggle() + else: + self.report({'WARNING'}, "Filletplus operation could not be performed") + return {'CANCELLED'} + else: + self.report({'WARNING'}, "Mesh is not a coplanar surface. Operation cancelled") + return {'CANCELLED'} + except: + self.report({'WARNING'}, "Filletplus operation could not be performed") + return {'CANCELLED'} + + return {'FINISHED'} diff --git a/mesh_extra_tools/mesh_help.py b/mesh_extra_tools/mesh_help.py new file mode 100644 index 00000000..7d06c40c --- /dev/null +++ b/mesh_extra_tools/mesh_help.py @@ -0,0 +1,215 @@ +# gpl authors: lijenstina, meta-androcto + +# Note: this script contains the Help Operator used by the various functions +# Usage: add a key string the dictionary with the list of strings to pass to labels in this file +# and call the operator from the add-on UI draw function by passing the help_ids parameter +# If the size of the pop-up if needed, define popup_size in the call by using varibles +# Example (variable prop): +# prop = layout.row("mesh.extra_tools_help") +# prop.help_ids = "default" +# prop.popup_size = 400 + + +import bpy +from bpy.types import Operator +from bpy.props import ( + StringProperty, + IntProperty, + ) + + +class MESH_OT_extra_tools_help(Operator): + bl_idname = "mesh.extra_tools_help" + bl_label = "" + bl_options = {'REGISTER'} + + help_ids = StringProperty( + name="ID of the Operator to display", + options={'HIDDEN'}, + default="default" + ) + popup_size = IntProperty( + name="Size of the Help Pop-up Menu", + default=350, + min=100, + max=600, + ) + + def draw(self, context): + layout = self.layout + pick_help = help_custom_draw(self.help_ids) + + for line_text in pick_help: + layout.label(line_text) + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_popup(self, width=self.popup_size) + + +def help_custom_draw(identifier="default"): + # A table of lists containing the help text under an index key that is the script name + # If several returns are needed per file, add some suffix after the script name + # and call them separately + # In case nothing is passed from the UI call, the returned list is default + # If undefined one is passed, it will return a warning message + help_text = {"default": [ + "This is a placeholder text", + "Please fill up the entries in the " + __name__ + " script", + ], + "random_vertices": [ + "To use:", + "Make a selection or selection of Vertices", + "Randomize displaced positions", + "Note:", + "There is an option to use Vertex Weights for displacement", + "Before use, don't forget to assign after updating the Group Weight", + ], + "mesh_vertex_chamfer": [ + "To use:", + "Make a selection or selection of vertices", + "Result is a triangle Chamfer, works on a single vertex", + "Note:", + "The difference to the vertex Bevel is that original geometry", + "(selected vertices) can be kept as an option", + "Limitation:", + "In some cases, may need to press F to fill the result", + ], + "mesh_filletplus": [ + "To use:", + "Select two adjacent edges and press Fillet button", + "Limitation:", + "Works on a mesh whose all faces share the same normal", + "(Flat Surface - faces have the same direction)", + "Planes with already round corners can produce unsatisfactory results", + "Only boundary edges will be evaluated", + ], + "mesh_offset_edges": [ + "To use:", + "Make a selection or selection of Edges", + "Extrude, rotate extrusions and more", + "Limitation:", + "Operates only on separate Edge loops selections", + "(i.e. Edge loops that are not connected by a selected edge)", + ], + "mesh_edge_roundifier": [ + "To use:", + "Select a single or multiple Edges", + "Make Arcs with various parameters", + "Reference, Rotation, Scaling, Connection and Offset", + "Note:", + "The Mode - Reset button restores the default values", + ], + "mesh_edges_length": [ + "To use:", + "Select a single or multiple Edges", + "Change length with various parameters", + "Limitation:", + "Does not operate on edges that share a vertex", + "If the selection wasn't done in Edge Selection mode,", + "the option Active will not work (due to Blender's limitation)", + ], + "mesh_to_wall": [ + "To use:", + "Extrudes flat along edges", + "Adds depth on both sides of an edge wire", + "Limitation:", + "Works best on Flat surfaces i.e. Ground Plan like geometry", + ], + "mesh_mextrude_plus": [ + "To use:", + "Make a selection of Faces", + "Extrude with Rotation, Scaling, Variation,", + "Randomization and Offset parameters", + "Limitation:", + "Works only with selections that enclose Faces", + "(i.e. all Edges or Vertices of a Face selected)", + ], + "mesh_extrude_and_reshape": [ + "To use:", + "Extrude Face and merge Edge intersections,", + "between the mesh and the new Edges", + "Note:", + "If selected Vertices don't form Face they will be", + "still extruded in the same direction", + "Limitation:", + "Works only with the last selected face", + "(or all Edges or Vertices of a Face selected)", + ], + "face_inset_fillet": [ + "To use:", + "Select one or multiple faces and inset", + "Inset square, circle or outside", + "Note:", + "Radius: use remove doubles to tidy joins", + "Out: select and use normals flip before extruding", + "Limitation:", + "Using the Out option, sometimes can lead to unsatisfactory results", + ], + "mesh_cut_faces": [ + "To use:", + "Make a selection or selection of Faces", + "Some Functions work on a plane only", + "Limitation:", + "The selection must include at least two Faces with adjacent edges", + "(Selections not sharing edges will not work)", + ], + "split_solidify": [ + "To use:", + "Make a selection or selection of Faces", + "Split Faces and Extrude results", + "Similar to a shatter/explode effect", + ], + "mesh_fastloop": [ + "To use:", + "Activate the tool and hover over the mesh in the general area", + "for the loop and left click once to confirm the loop placement", + "Slide using the mouse to fine tune its position, left click to confirm", + "Repeat the operations if needed for new loops", + "Press Esc. twice to exit the tool", + "Limitation:", + "The tool has the same limitations as Loop Cut and Slide", + "In the Operator Panel, only the last loop can be tweaked", + ], + "mesh_pen_tool": [ + "To use:", + "Press Ctrl + D key or click Draw button", + "To draw along x use SHIFT + MOUSEMOVE", + "To draw along y use ALT + MOUSEMOVE", + "Press Ctrl to toggle Extrude at Cursor tool", + "Right click to finish drawing or", + "Press Esc to cancel", + ], + "pkhg_faces": [ + "To use:", + "Needs a Face Selection in Edit Mode", + "Select an option from Face Types drop down list", + "Extrude, rotate extrusions and more", + "Toggle Edit Mode after use", + "Note:", + "After using the operator, normals could need repair,", + "or Removing Doubles", + ], + } + + if identifier in help_text: + return help_text[identifier] + + return ["ERROR:", "Help Operator", "Undefined call to the Dictionary"] + + +# register +def register(): + bpy.utils.register_module(__name__) + pass + + +def unregister(): + bpy.utils.unregister_module(__name__) + pass + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_mextrude_plus.py b/mesh_extra_tools/mesh_mextrude_plus.py new file mode 100644 index 00000000..1cfd1714 --- /dev/null +++ b/mesh_extra_tools/mesh_mextrude_plus.py @@ -0,0 +1,239 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### +# +# Repeats extrusion + rotation + scale for one or more faces + + +bl_info = { + "name": "MExtrude Plus1", + "author": "liero", + "version": (1, 2, 9), + "blender": (2, 77, 0), + "location": "View3D > Tool Shelf", + "description": "Repeat extrusions from faces to create organic shapes", + "warning": "", + "wiki_url": "", + "tracker_url": "https://developer.blender.org/T28570", + "category": "Mesh"} + + +import bpy +import bmesh +import random +from bpy.types import Operator +from random import gauss +from math import radians +from mathutils import Euler +from bpy.props import ( + FloatProperty, + IntProperty, + ) + + +def vloc(self, r): + random.seed(self.ran + r) + return self.off * (1 + gauss(0, self.var1 / 3)) + + +def vrot(self, r): + random.seed(self.ran + r) + return Euler((radians(self.rotx) + gauss(0, self.var2 / 3), + radians(self.roty) + gauss(0, self.var2 / 3), + radians(self.rotz) + gauss(0, self.var2 / 3)), 'XYZ') + + +def vsca(self, r): + random.seed(self.ran + r) + return self.sca * (1 + random.gauss(0, self.var3 / 3)) + + +class MExtrude(Operator): + bl_idname = "object.mextrude" + bl_label = "Multi Extrude" + bl_description = ("Extrude selected Faces with Rotation,\n" + "Scaling, Variation, Randomization") + bl_options = {"REGISTER", "UNDO"} + + off = FloatProperty( + name="Offset", + soft_min=0.001, soft_max=2, + min=-2, max=5, + default=.5, + description="Translation" + ) + rotx = FloatProperty( + name="Rot X", + min=-85, max=85, + soft_min=-30, soft_max=30, + default=0, + description="X Rotation" + ) + roty = FloatProperty( + name="Rot Y", + min=-85, max=85, + soft_min=-30, + soft_max=30, + default=0, + description="Y Rotation" + ) + rotz = FloatProperty( + name="Rot Z", + min=-85, max=85, + soft_min=-30, soft_max=30, + default=-0, + description="Z Rotation" + ) + sca = FloatProperty( + name="Scale", + min=0.1, max=2, + soft_min=0.5, soft_max=1.2, + default=1.0, + description="Scaling of the selected faces after extrusion" + ) + var1 = FloatProperty( + name="Offset Var", min=-5, max=5, + soft_min=-1, soft_max=1, + default=0, + description="Offset variation" + ) + var2 = FloatProperty( + name="Rotation Var", + min=-5, max=5, + soft_min=-1, soft_max=1, + default=0, + description="Rotation variation" + ) + var3 = FloatProperty( + name="Scale Noise", + min=-5, max=5, + soft_min=-1, soft_max=1, + default=0, + description="Scaling noise" + ) + num = IntProperty( + name="Repeat", + min=1, max=50, + soft_max=100, + default=5, + description="Repetitions") + ran = IntProperty( + name="Seed", + min=-9999, max=9999, + default=0, + description="Seed to feed random values") + + @classmethod + def poll(cls, context): + obj = context.object + return (obj and obj.type == 'MESH') + + def draw(self, context): + layout = self.layout + + col = layout.column(align=True) + col.label(text="Transformations:") + col.prop(self, "off", slider=True) + col.prop(self, "rotx", slider=True) + col.prop(self, "roty", slider=True) + col.prop(self, "rotz", slider=True) + col.prop(self, "sca", slider=True) + + col = layout.column(align=True) + col.label(text="Variation settings:") + col.prop(self, "var1", slider=True) + col.prop(self, "var2", slider=True) + col.prop(self, "var3", slider=True) + col.prop(self, "ran") + + col = layout.column(align=False) + col.prop(self, 'num') + + def execute(self, context): + obj = bpy.context.object + om = obj.mode + bpy.context.tool_settings.mesh_select_mode = [False, False, True] + + # bmesh operations + bpy.ops.object.mode_set() + bm = bmesh.new() + bm.from_mesh(obj.data) + sel = [f for f in bm.faces if f.select] + + after = [] + + # faces loop + for i, of in enumerate(sel): + rot = vrot(self, i) + off = vloc(self, i) + of.normal_update() + + # extrusion loop + for r in range(self.num): + nf = of.copy() + nf.normal_update() + no = nf.normal.copy() + ce = nf.calc_center_bounds() + s = vsca(self, i + r) + + for v in nf.verts: + v.co -= ce + v.co.rotate(rot) + v.co += ce + no * off + v.co = v.co.lerp(ce, 1 - s) + + # extrude code from TrumanBlending + for a, b in zip(of.loops, nf.loops): + sf = bm.faces.new((a.vert, a.link_loop_next.vert, + b.link_loop_next.vert, b.vert)) + sf.normal_update() + + bm.faces.remove(of) + of = nf + after.append(of) + + for v in bm.verts: + v.select = False + for e in bm.edges: + e.select = False + + for f in after: + f.select = True + + bm.to_mesh(obj.data) + obj.data.update() + + # restore user settings + bpy.ops.object.mode_set(mode=om) + + if not len(sel): + self.report({"WARNING"}, "No suitable Face selection found. Operation cancelled") + return {'CANCELLED'} + + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + +if __name__ == '__main__': + register() diff --git a/mesh_extra_tools/mesh_offset_edges.py b/mesh_extra_tools/mesh_offset_edges.py new file mode 100644 index 00000000..b1f9cf09 --- /dev/null +++ b/mesh_extra_tools/mesh_offset_edges.py @@ -0,0 +1,823 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +bl_info = { + "name": "Offset Edges", + "author": "Hidesato Ikeya", + "version": (0, 2, 6), + "blender": (2, 70, 0), + "location": "VIEW3D > Edge menu(CTRL-E) > Offset Edges", + "description": "Offset Edges", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/offset_edges", + "tracker_url": "", + "category": "Mesh"} + +import bpy +import bmesh +from bpy.types import Operator +from math import sin, cos, pi, radians +from mathutils import Vector +from time import perf_counter + +from bpy.props import ( + BoolProperty, + FloatProperty, + EnumProperty, + ) + +# Globals +X_UP = Vector((1.0, .0, .0)) +Y_UP = Vector((.0, 1.0, .0)) +Z_UP = Vector((.0, .0, 1.0)) +ZERO_VEC = Vector((.0, .0, .0)) +ANGLE_90 = pi / 2 +ANGLE_180 = pi +ANGLE_360 = 2 * pi + +# switch performance logging +ENABLE_DEBUG = False + + +def calc_loop_normal(verts, fallback=Z_UP): + # Calculate normal from verts using Newell's method + normal = ZERO_VEC.copy() + + if verts[0] is verts[-1]: + # Perfect loop + range_verts = range(1, len(verts)) + else: + # Half loop + range_verts = range(0, len(verts)) + + for i in range_verts: + v1co, v2co = verts[i - 1].co, verts[i].co + normal.x += (v1co.y - v2co.y) * (v1co.z + v2co.z) + normal.y += (v1co.z - v2co.z) * (v1co.x + v2co.x) + normal.z += (v1co.x - v2co.x) * (v1co.y + v2co.y) + + if normal != ZERO_VEC: + normal.normalize() + else: + normal = fallback + + return normal + + +def collect_edges(bm): + set_edges_orig = set() + for e in bm.edges: + if e.select: + co_faces_selected = 0 + for f in e.link_faces: + if f.select: + co_faces_selected += 1 + if co_faces_selected == 2: + break + else: + set_edges_orig.add(e) + + if not set_edges_orig: + return None + + return set_edges_orig + + +def collect_loops(set_edges_orig): + set_edges_copy = set_edges_orig.copy() + + loops = [] # [v, e, v, e, ... , e, v] + while set_edges_copy: + edge_start = set_edges_copy.pop() + v_left, v_right = edge_start.verts + lp = [v_left, edge_start, v_right] + reverse = False + while True: + edge = None + for e in v_right.link_edges: + if e in set_edges_copy: + if edge: + # Overlap detected. + return None + edge = e + set_edges_copy.remove(e) + if edge: + v_right = edge.other_vert(v_right) + lp.extend((edge, v_right)) + continue + else: + if v_right is v_left: + # Real loop. + loops.append(lp) + break + elif reverse is False: + # Right side of half loop + # Reversing the loop to operate same procedure on the left side + lp.reverse() + v_right, v_left = v_left, v_right + reverse = True + continue + else: + # Half loop, completed + loops.append(lp) + break + return loops + + +def get_adj_ix(ix_start, vec_edges, half_loop): + # Get adjacent edge index, skipping zero length edges + len_edges = len(vec_edges) + if half_loop: + range_right = range(ix_start, len_edges) + range_left = range(ix_start - 1, -1, -1) + else: + range_right = range(ix_start, ix_start + len_edges) + range_left = range(ix_start - 1, ix_start - 1 - len_edges, -1) + + ix_right = ix_left = None + for i in range_right: + # Right + i %= len_edges + if vec_edges[i] != ZERO_VEC: + ix_right = i + break + for i in range_left: + # Left + i %= len_edges + if vec_edges[i] != ZERO_VEC: + ix_left = i + break + if half_loop: + # If index of one side is None, assign another index + if ix_right is None: + ix_right = ix_left + if ix_left is None: + ix_left = ix_right + + return ix_right, ix_left + + +def get_adj_faces(edges): + adj_faces = [] + for e in edges: + adj_f = None + co_adj = 0 + for f in e.link_faces: + # Search an adjacent face + # Selected face has precedence + if not f.hide and f.normal != ZERO_VEC: + adj_f = f + co_adj += 1 + if f.select: + adj_faces.append(adj_f) + break + else: + if co_adj == 1: + adj_faces.append(adj_f) + else: + adj_faces.append(None) + return adj_faces + + +def get_edge_rail(vert, set_edges_orig): + co_edges = co_edges_selected = 0 + vec_inner = None + for e in vert.link_edges: + if (e not in set_edges_orig and + (e.select or (co_edges_selected == 0 and not e.hide))): + v_other = e.other_vert(vert) + vec = v_other.co - vert.co + if vec != ZERO_VEC: + vec_inner = vec + if e.select: + co_edges_selected += 1 + if co_edges_selected == 2: + return None + else: + co_edges += 1 + if co_edges_selected == 1: + vec_inner.normalize() + return vec_inner + elif co_edges == 1: + # No selected edges, one unselected edge + vec_inner.normalize() + return vec_inner + else: + return None + + +def get_cross_rail(vec_tan, vec_edge_r, vec_edge_l, normal_r, normal_l): + # Cross rail is a cross vector between normal_r and normal_l + vec_cross = normal_r.cross(normal_l) + if vec_cross.dot(vec_tan) < .0: + vec_cross *= -1 + cos_min = min(vec_tan.dot(vec_edge_r), vec_tan.dot(-vec_edge_l)) + cos = vec_tan.dot(vec_cross) + if cos >= cos_min: + vec_cross.normalize() + return vec_cross + else: + return None + + +def move_verts(width, depth, verts, directions, geom_ex): + if geom_ex: + geom_s = geom_ex['side'] + verts_ex = [] + for v in verts: + for e in v.link_edges: + if e in geom_s: + verts_ex.append(e.other_vert(v)) + break + verts = verts_ex + + for v, (vec_width, vec_depth) in zip(verts, directions): + v.co += width * vec_width + depth * vec_depth + + +def extrude_edges(bm, edges_orig): + extruded = bmesh.ops.extrude_edge_only(bm, edges=edges_orig)['geom'] + n_edges = n_faces = len(edges_orig) + n_verts = len(extruded) - n_edges - n_faces + + geom = dict() + geom['verts'] = verts = set(extruded[:n_verts]) + geom['edges'] = edges = set(extruded[n_verts:n_verts + n_edges]) + geom['faces'] = set(extruded[n_verts + n_edges:]) + geom['side'] = set(e for v in verts for e in v.link_edges if e not in edges) + + return geom + + +def clean(bm, mode, edges_orig, geom_ex=None): + for f in bm.faces: + f.select = False + if geom_ex: + for e in geom_ex['edges']: + e.select = True + if mode == 'offset': + lis_geom = list(geom_ex['side']) + list(geom_ex['faces']) + bmesh.ops.delete(bm, geom=lis_geom, context=2) + else: + for e in edges_orig: + e.select = True + + +def collect_mirror_planes(edit_object): + mirror_planes = [] + eob_mat_inv = edit_object.matrix_world.inverted() + for m in edit_object.modifiers: + if (m.type == 'MIRROR' and m.use_mirror_merge): + merge_limit = m.merge_threshold + if not m.mirror_object: + loc = ZERO_VEC + norm_x, norm_y, norm_z = X_UP, Y_UP, Z_UP + else: + mirror_mat_local = eob_mat_inv * m.mirror_object.matrix_world + loc = mirror_mat_local.to_translation() + norm_x, norm_y, norm_z, _ = mirror_mat_local.adjugated() + norm_x = norm_x.to_3d().normalized() + norm_y = norm_y.to_3d().normalized() + norm_z = norm_z.to_3d().normalized() + if m.use_x: + mirror_planes.append((loc, norm_x, merge_limit)) + if m.use_y: + mirror_planes.append((loc, norm_y, merge_limit)) + if m.use_z: + mirror_planes.append((loc, norm_z, merge_limit)) + return mirror_planes + + +def get_vert_mirror_pairs(set_edges_orig, mirror_planes): + if mirror_planes: + set_edges_copy = set_edges_orig.copy() + vert_mirror_pairs = dict() + for e in set_edges_orig: + v1, v2 = e.verts + for mp in mirror_planes: + p_co, p_norm, mlimit = mp + v1_dist = abs(p_norm.dot(v1.co - p_co)) + v2_dist = abs(p_norm.dot(v2.co - p_co)) + if v1_dist <= mlimit: + # v1 is on a mirror plane + vert_mirror_pairs[v1] = mp + if v2_dist <= mlimit: + # v2 is on a mirror plane + vert_mirror_pairs[v2] = mp + if v1_dist <= mlimit and v2_dist <= mlimit: + # This edge is on a mirror_plane, so should not be offsetted + set_edges_copy.remove(e) + return vert_mirror_pairs, set_edges_copy + else: + return None, set_edges_orig + + +def get_mirror_rail(mirror_plane, vec_up): + p_norm = mirror_plane[1] + mirror_rail = vec_up.cross(p_norm) + if mirror_rail != ZERO_VEC: + mirror_rail.normalize() + # Project vec_up to mirror_plane + vec_up = vec_up - vec_up.project(p_norm) + vec_up.normalize() + return mirror_rail, vec_up + else: + return None, vec_up + + +def reorder_loop(verts, edges, lp_normal, adj_faces): + for i, adj_f in enumerate(adj_faces): + if adj_f is None: + continue + + v1, v2 = verts[i], verts[i + 1] + fv = tuple(adj_f.verts) + if fv[fv.index(v1) - 1] is v2: + # Align loop direction + verts.reverse() + edges.reverse() + adj_faces.reverse() + + if lp_normal.dot(adj_f.normal) < .0: + lp_normal *= -1 + break + else: + # All elements in adj_faces are None + for v in verts: + if v.normal != ZERO_VEC: + if lp_normal.dot(v.normal) < .0: + verts.reverse() + edges.reverse() + lp_normal *= -1 + break + + return verts, edges, lp_normal, adj_faces + + +def get_directions(lp, vec_upward, normal_fallback, vert_mirror_pairs, **options): + opt_follow_face = options['follow_face'] + opt_edge_rail = options['edge_rail'] + opt_er_only_end = options['edge_rail_only_end'] + opt_threshold = options['threshold'] + + verts, edges = lp[::2], lp[1::2] + set_edges = set(edges) + lp_normal = calc_loop_normal(verts, fallback=normal_fallback) + + # Loop order might be changed below + if lp_normal.dot(vec_upward) < .0: + # Make this loop's normal towards vec_upward + verts.reverse() + edges.reverse() + lp_normal *= -1 + + if opt_follow_face: + adj_faces = get_adj_faces(edges) + verts, edges, lp_normal, adj_faces = \ + reorder_loop(verts, edges, lp_normal, adj_faces) + else: + adj_faces = (None, ) * len(edges) + # Loop order might be changed above + + vec_edges = tuple((e.other_vert(v).co - v.co).normalized() + for v, e in zip(verts, edges)) + + if verts[0] is verts[-1]: + # Real loop. Popping last vertex + verts.pop() + HALF_LOOP = False + else: + # Half loop + HALF_LOOP = True + + len_verts = len(verts) + directions = [] + for i in range(len_verts): + vert = verts[i] + ix_right, ix_left = i, i - 1 + + VERT_END = False + if HALF_LOOP: + if i == 0: + # First vert + ix_left = ix_right + VERT_END = True + elif i == len_verts - 1: + # Last vert + ix_right = ix_left + VERT_END = True + + edge_right, edge_left = vec_edges[ix_right], vec_edges[ix_left] + face_right, face_left = adj_faces[ix_right], adj_faces[ix_left] + + norm_right = face_right.normal if face_right else lp_normal + norm_left = face_left.normal if face_left else lp_normal + if norm_right.angle(norm_left) > opt_threshold: + # Two faces are not flat + two_normals = True + else: + two_normals = False + + tan_right = edge_right.cross(norm_right).normalized() + tan_left = edge_left.cross(norm_left).normalized() + tan_avr = (tan_right + tan_left).normalized() + norm_avr = (norm_right + norm_left).normalized() + + rail = None + if two_normals or opt_edge_rail: + # Get edge rail + # edge rail is a vector of an inner edge + if two_normals or (not opt_er_only_end) or VERT_END: + rail = get_edge_rail(vert, set_edges) + if vert_mirror_pairs and VERT_END: + if vert in vert_mirror_pairs: + rail, norm_avr = get_mirror_rail(vert_mirror_pairs[vert], norm_avr) + if (not rail) and two_normals: + # Get cross rail + # Cross rail is a cross vector between norm_right and norm_left + rail = get_cross_rail( + tan_avr, edge_right, edge_left, norm_right, norm_left) + if rail: + dot = tan_avr.dot(rail) + if dot > .0: + tan_avr = rail + elif dot < .0: + tan_avr = -rail + + vec_plane = norm_avr.cross(tan_avr) + e_dot_p_r = edge_right.dot(vec_plane) + e_dot_p_l = edge_left.dot(vec_plane) + if e_dot_p_r or e_dot_p_l: + if e_dot_p_r > e_dot_p_l: + vec_edge, e_dot_p = edge_right, e_dot_p_r + else: + vec_edge, e_dot_p = edge_left, e_dot_p_l + + vec_tan = (tan_avr - tan_avr.project(vec_edge)).normalized() + # Make vec_tan perpendicular to vec_edge + vec_up = vec_tan.cross(vec_edge) + + vec_width = vec_tan - (vec_tan.dot(vec_plane) / e_dot_p) * vec_edge + vec_depth = vec_up - (vec_up.dot(vec_plane) / e_dot_p) * vec_edge + else: + vec_width = tan_avr + vec_depth = norm_avr + + directions.append((vec_width, vec_depth)) + + return verts, directions + + +angle_presets = {'0°': 0, + '15°': radians(15), + '30°': radians(30), + '45°': radians(45), + '60°': radians(60), + '75°': radians(75), + '90°': radians(90), + } + + +def use_cashes(self, context): + self.caches_valid = True + + +def assign_angle_presets(self, context): + use_cashes(self, context) + self.angle = angle_presets[self.angle_presets] + + +class OffsetEdges(Operator): + bl_idname = "mesh.offset_edges" + bl_label = "Offset Edges" + bl_description = ("Extrude, Move or Offset the selected Edges\n" + "Operates only on separate Edge loops selections") + bl_options = {'REGISTER', 'UNDO'} + + geometry_mode = EnumProperty( + items=[('offset', "Offset", "Offset edges"), + ('extrude', "Extrude", "Extrude edges"), + ('move', "Move", "Move selected edges")], + name="Geometry mode", + default='offset', + update=use_cashes + ) + width = FloatProperty( + name="Width", + default=.2, + precision=4, step=1, + update=use_cashes + ) + flip_width = BoolProperty( + name="Flip Width", + default=False, + description="Flip width direction", + update=use_cashes + ) + depth = FloatProperty( + name="Depth", + default=.0, + precision=4, step=1, + update=use_cashes + ) + flip_depth = BoolProperty( + name="Flip Depth", + default=False, + description="Flip depth direction", + update=use_cashes + ) + depth_mode = EnumProperty( + items=[('angle', "Angle", "Angle"), + ('depth', "Depth", "Depth")], + name="Depth mode", + default='angle', + update=use_cashes + ) + angle = FloatProperty( + name="Angle", default=0, + precision=3, step=.1, + min=-2 * pi, max=2 * pi, + subtype='ANGLE', + description="Angle", + update=use_cashes + ) + flip_angle = BoolProperty( + name="Flip Angle", + default=False, + description="Flip Angle", + update=use_cashes + ) + follow_face = BoolProperty( + name="Follow Face", + default=False, + description="Offset along faces around" + ) + mirror_modifier = BoolProperty( + name="Mirror Modifier", + default=False, + description="Take into account of Mirror modifier" + ) + edge_rail = BoolProperty( + name="Edge Rail", + default=False, + description="Align vertices along inner edges" + ) + edge_rail_only_end = BoolProperty( + name="Edge Rail Only End", + default=False, + description="Apply edge rail to end verts only" + ) + threshold = FloatProperty( + name="Flat Face Threshold", + default=radians(0.05), precision=5, + step=1.0e-4, subtype='ANGLE', + description="If difference of angle between two adjacent faces is " + "below this value, those faces are regarded as flat", + options={'HIDDEN'} + ) + caches_valid = BoolProperty( + name="Caches Valid", + default=False, + options={'HIDDEN'} + ) + angle_presets = EnumProperty( + items=[('0°', "0°", "0°"), + ('15°', "15°", "15°"), + ('30°', "30°", "30°"), + ('45°', "45°", "45°"), + ('60°', "60°", "60°"), + ('75°', "75°", "75°"), + ('90°', "90°", "90°"), ], + name="Angle Presets", + default='0°', + update=assign_angle_presets + ) + + _cache_offset_infos = None + _cache_edges_orig_ixs = None + + @classmethod + def poll(self, context): + return context.mode == 'EDIT_MESH' + + def draw(self, context): + layout = self.layout + layout.prop(self, 'geometry_mode', text="") + + row = layout.row(align=True) + row.prop(self, 'width') + row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True) + layout.prop(self, 'depth_mode', expand=True) + + if self.depth_mode == 'angle': + d_mode = 'angle' + flip = 'flip_angle' + else: + d_mode = 'depth' + flip = 'flip_depth' + row = layout.row(align=True) + row.prop(self, d_mode) + row.prop(self, flip, icon='ARROW_LEFTRIGHT', icon_only=True) + if self.depth_mode == 'angle': + layout.prop(self, 'angle_presets', text="Presets", expand=True) + + layout.separator() + + layout.prop(self, 'follow_face') + + row = layout.row() + row.prop(self, 'edge_rail') + if self.edge_rail: + row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True) + + layout.prop(self, 'mirror_modifier') + layout.operator('mesh.offset_edges', text="Repeat") + + if self.follow_face: + layout.separator() + layout.prop(self, 'threshold', text="Threshold") + + def get_offset_infos(self, bm, edit_object): + if self.caches_valid and self._cache_offset_infos is not None: + # Return None, indicating to use cache + return None, None + + if ENABLE_DEBUG: + time = perf_counter() + + set_edges_orig = collect_edges(bm) + if set_edges_orig is None: + self.report({'WARNING'}, + "No edges selected or edge loops could not be determined") + return False, False + + if self.mirror_modifier: + mirror_planes = collect_mirror_planes(edit_object) + vert_mirror_pairs, set_edges = \ + get_vert_mirror_pairs(set_edges_orig, mirror_planes) + + if set_edges: + set_edges_orig = set_edges + else: + vert_mirror_pairs = None + else: + vert_mirror_pairs = None + + loops = collect_loops(set_edges_orig) + if loops is None: + self.report({'WARNING'}, + "Overlap detected. Select non-overlapping edge loops") + return False, False + + vec_upward = (X_UP + Y_UP + Z_UP).normalized() + # vec_upward is used to unify loop normals when follow_face is off + normal_fallback = Z_UP + # normal_fallback = Vector(context.region_data.view_matrix[2][:3]) + # normal_fallback is used when loop normal cannot be calculated + + follow_face = self.follow_face + edge_rail = self.edge_rail + er_only_end = self.edge_rail_only_end + threshold = self.threshold + + offset_infos = [] + for lp in loops: + verts, directions = get_directions( + lp, vec_upward, normal_fallback, vert_mirror_pairs, + follow_face=follow_face, edge_rail=edge_rail, + edge_rail_only_end=er_only_end, + threshold=threshold) + if verts: + offset_infos.append((verts, directions)) + + # Saving caches + self._cache_offset_infos = _cache_offset_infos = [] + for verts, directions in offset_infos: + v_ixs = tuple(v.index for v in verts) + _cache_offset_infos.append((v_ixs, directions)) + self._cache_edges_orig_ixs = tuple(e.index for e in set_edges_orig) + + if ENABLE_DEBUG: + print("Preparing OffsetEdges: ", perf_counter() - time) + + return offset_infos, set_edges_orig + + def do_offset_and_free(self, bm, me, offset_infos=None, set_edges_orig=None): + # If offset_infos is None, use caches + # Makes caches invalid after offset + + if ENABLE_DEBUG: + time = perf_counter() + + if offset_infos is None: + # using cache + bmverts = tuple(bm.verts) + bmedges = tuple(bm.edges) + edges_orig = [bmedges[ix] for ix in self._cache_edges_orig_ixs] + verts_directions = [] + for ix_vs, directions in self._cache_offset_infos: + verts = tuple(bmverts[ix] for ix in ix_vs) + verts_directions.append((verts, directions)) + else: + verts_directions = offset_infos + edges_orig = list(set_edges_orig) + + if self.depth_mode == 'angle': + w = self.width if not self.flip_width else -self.width + angle = self.angle if not self.flip_angle else -self.angle + width = w * cos(angle) + depth = w * sin(angle) + else: + width = self.width if not self.flip_width else -self.width + depth = self.depth if not self.flip_depth else -self.depth + + # Extrude + if self.geometry_mode == 'move': + geom_ex = None + else: + geom_ex = extrude_edges(bm, edges_orig) + + for verts, directions in verts_directions: + move_verts(width, depth, verts, directions, geom_ex) + + clean(bm, self.geometry_mode, edges_orig, geom_ex) + + bpy.ops.object.mode_set(mode="OBJECT") + bm.to_mesh(me) + bpy.ops.object.mode_set(mode="EDIT") + bm.free() + self.caches_valid = False # Make caches invalid + + if ENABLE_DEBUG: + print("OffsetEdges offset: ", perf_counter() - time) + + def execute(self, context): + # In edit mode + edit_object = context.edit_object + bpy.ops.object.mode_set(mode="OBJECT") + + me = edit_object.data + bm = bmesh.new() + bm.from_mesh(me) + + offset_infos, edges_orig = self.get_offset_infos(bm, edit_object) + if offset_infos is False: + bpy.ops.object.mode_set(mode="EDIT") + return {'CANCELLED'} + + self.do_offset_and_free(bm, me, offset_infos, edges_orig) + + return {'FINISHED'} + + def restore_original_and_free(self, context): + self.caches_valid = False # Make caches invalid + context.area.header_text_set() + + me = context.edit_object.data + bpy.ops.object.mode_set(mode="OBJECT") + self._bm_orig.to_mesh(me) + bpy.ops.object.mode_set(mode="EDIT") + + self._bm_orig.free() + context.area.header_text_set() + + def invoke(self, context, event): + # In edit mode + edit_object = context.edit_object + me = edit_object.data + bpy.ops.object.mode_set(mode="OBJECT") + for p in me.polygons: + if p.select: + self.follow_face = True + break + + self.caches_valid = False + bpy.ops.object.mode_set(mode="EDIT") + return self.execute(context) + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + +if __name__ == '__main__': + register() diff --git a/mesh_extra_tools/mesh_pen_tool.py b/mesh_extra_tools/mesh_pen_tool.py new file mode 100644 index 00000000..ebe5ae8d --- /dev/null +++ b/mesh_extra_tools/mesh_pen_tool.py @@ -0,0 +1,553 @@ +# -*- coding: utf-8 -*- + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +bl_info = { + "name": "Pen Tool", + "author": "zmj100", + "version": (0, 2, 8), + "blender": (2, 6, 5), + "location": "View3D > Tool Shelf", + "description": "", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh", + } + +import bpy +import blf +import bgl +import bmesh +from bpy.types import ( + Operator, + PropertyGroup, + Panel + ) +from bpy.props import ( + FloatProperty, + IntProperty, + PointerProperty, + BoolProperty + ) +from bpy_extras.view3d_utils import region_2d_to_location_3d, location_3d_to_region_2d +from mathutils import Vector, Matrix +from math import degrees + + +def edit_mode_out(): + bpy.ops.object.mode_set(mode='OBJECT') + + +def edit_mode_in(): + bpy.ops.object.mode_set(mode='EDIT') + + +def get_direction_(bme, list_, ob_act): + n = len(list_) + for i in range(n): + p = ob_act.matrix_world * (bme.verts[list_[i]].co).copy() + p1 = ob_act.matrix_world * (bme.verts[list_[(i - 1) % n]].co).copy() + p2 = ob_act.matrix_world * (bme.verts[list_[(i + 1) % n]].co).copy() + + if p == p1 or p == p2: + continue + ang = round(degrees((p - p1).angle((p - p2), any))) + if ang == 0 or ang == 180: + continue + elif ang != 0 or ang != 180: + return(((p - p1).cross((p - p2))).normalized()) + break + + +def store_restore_view(context, store=True): + if not context.scene.pen_tool_props.restore_view: + return + + if store is True: + # copy the original view_matrix and rotation for restoring + pt_buf.store_view_matrix = context.space_data.region_3d.view_matrix.copy() + pt_buf.view_location = context.space_data.region_3d.view_location.copy() + else: + context.space_data.region_3d.view_matrix = pt_buf.store_view_matrix + context.space_data.region_3d.view_location = pt_buf.view_location + + +def align_view_to_face_(context, bme, f): + store_restore_view(context, True) + ob_act = context.active_object + list_e = [[v.index for v in e.verts] for e in f.edges][0] + vec0 = -get_direction_(bme, [v.index for v in f.verts], ob_act) + vec1 = ((ob_act.matrix_world * bme.verts[list_e[0]].co.copy()) - + (ob_act.matrix_world * bme.verts[list_e[1]].co.copy())).normalized() + vec2 = (vec0.cross(vec1)).normalized() + context.space_data.region_3d.view_matrix = ((Matrix((vec1, vec2, vec0))).to_4x4()).inverted() + context.space_data.region_3d.view_location = f.calc_center_median() + + +def draw_callback_px(self, context): + font_id = 0 + alpha = context.scene.pen_tool_props.a + font_size = context.scene.pen_tool_props.fs + + bgl.glColor4f(0.0, 0.6, 1.0, alpha) + bgl.glPointSize(4.0) + bgl.glBegin(bgl.GL_POINTS) + bgl.glVertex2f(pt_buf.x, pt_buf.y) + bgl.glEnd() + bgl.glDisable(bgl.GL_BLEND) + + # location 3d + if context.scene.pen_tool_props.b2 is True: + mloc3d = region_2d_to_location_3d( + context.region, + context.space_data.region_3d, Vector((pt_buf.x, pt_buf.y)), + pt_buf.depth_location + ) + blf.position(font_id, pt_buf.x + 15, pt_buf.y - 15, 0) + blf.size(font_id, font_size, context.user_preferences.system.dpi) + blf.draw(font_id, + '(' + str(round(mloc3d[0], 4)) + ', ' + str(round(mloc3d[1], 4)) + + ', ' + str(round(mloc3d[2], 4)) + ')') + + n = len(pt_buf.list_m_loc_3d) + + if n != 0: + # add points + bgl.glEnable(bgl.GL_BLEND) + bgl.glPointSize(4.0) + bgl.glBegin(bgl.GL_POINTS) + for i in pt_buf.list_m_loc_3d: + loc_0 = location_3d_to_region_2d(context.region, context.space_data.region_3d, i) + bgl.glVertex2f(loc_0[0], loc_0[1]) + bgl.glEnd() + bgl.glDisable(bgl.GL_BLEND) + + # text next to the mouse + m_loc_3d = region_2d_to_location_3d( + context.region, + context.space_data.region_3d, Vector((pt_buf.x, pt_buf.y)), + pt_buf.depth_location + ) + vec0 = pt_buf.list_m_loc_3d[-1] - m_loc_3d + blf.position(font_id, pt_buf.x + 15, pt_buf.y + 15, 0) + blf.size(font_id, font_size, context.user_preferences.system.dpi) + blf.draw(font_id, str(round(vec0.length, 4))) + + # angle first after mouse + if n >= 2: + vec1 = pt_buf.list_m_loc_3d[-2] - pt_buf.list_m_loc_3d[-1] + if vec0.length == 0.0 or vec1.length == 0.0: + pass + else: + ang = vec0.angle(vec1) + + if round(degrees(ang), 2) == 180.0: + text_0 = '0.0' + elif round(degrees(ang), 2) == 0.0: + text_0 = '180.0' + else: + text_0 = str(round(degrees(ang), 2)) + + loc_4 = location_3d_to_region_2d( + context.region, + context.space_data.region_3d, + pt_buf.list_m_loc_3d[-1] + ) + bgl.glColor4f(0.0, 1.0, 0.525, alpha) + blf.position(font_id, loc_4[0] + 10, loc_4[1] + 10, 0) + blf.size(font_id, font_size, context.user_preferences.system.dpi) + blf.draw(font_id, text_0 + '') + + bgl.glLineStipple(4, 0x5555) + bgl.glEnable(bgl.GL_LINE_STIPPLE) # enable line stipple + + bgl.glColor4f(0.0, 0.6, 1.0, alpha) + # draw line between last point and mouse + bgl.glEnable(bgl.GL_BLEND) + bgl.glBegin(bgl.GL_LINES) + loc_1 = location_3d_to_region_2d( + context.region, + context.space_data.region_3d, + pt_buf.list_m_loc_3d[-1] + ) + bgl.glVertex2f(loc_1[0], loc_1[1]) + bgl.glVertex2f(pt_buf.x, pt_buf.y) + bgl.glEnd() + bgl.glDisable(bgl.GL_BLEND) + + # draw lines between points + bgl.glEnable(bgl.GL_BLEND) + bgl.glBegin(bgl.GL_LINE_STRIP) + for j in pt_buf.list_m_loc_3d: + loc_2 = location_3d_to_region_2d(context.region, context.space_data.region_3d, j) + bgl.glVertex2f(loc_2[0], loc_2[1]) + bgl.glEnd() + bgl.glDisable(bgl.GL_BLEND) + + bgl.glDisable(bgl.GL_LINE_STIPPLE) # disable line stipple + + # draw line length between points + if context.scene.pen_tool_props.b1 is True: + for k in range(n - 1): + loc_3 = location_3d_to_region_2d( + context.region, context.space_data.region_3d, + (pt_buf.list_m_loc_3d[k] + pt_buf.list_m_loc_3d[(k + 1) % n]) * 0.5 + ) + blf.position(font_id, loc_3[0] + 10, loc_3[1] + 10, 0) + blf.size(font_id, font_size, context.user_preferences.system.dpi) + blf.draw(font_id, + str(round((pt_buf.list_m_loc_3d[k] - pt_buf.list_m_loc_3d[(k + 1) % n]).length, 4))) + + # draw all angles + if context.scene.pen_tool_props.b0 is True: + for h in range(n - 1): + if n >= 2: + if h == 0: + pass + else: + vec_ = pt_buf.list_m_loc_3d[h] - pt_buf.list_m_loc_3d[(h - 1) % n] + vec_1_ = pt_buf.list_m_loc_3d[h] + vec_2_ = pt_buf.list_m_loc_3d[(h - 1) % n] + if vec_.length == 0.0 or vec_1_.length == 0.0 or vec_2_.length == 0.0: + pass + else: + ang = vec_.angle(vec_1_ - vec_2_) + if round(degrees(ang)) == 0.0: + pass + else: + loc_4 = location_3d_to_region_2d( + context.region, context.space_data.region_3d, + pt_buf.list_m_loc_3d[h] + ) + bgl.glColor4f(0.0, 1.0, 0.525, alpha) + blf.position(font_id, loc_4[0] + 10, loc_4[1] + 10, 0) + blf.size(font_id, font_size, context.user_preferences.system.dpi) + blf.draw(font_id, str(round(degrees(ang), 2)) + '') + # tools on / off + bgl.glColor4f(1.0, 1.0, 1.0, 1.0) + blf.position(font_id, self.text_location, 20, 0) + blf.size(font_id, 15, context.user_preferences.system.dpi) + blf.draw(font_id, "Draw On") + blf.position(font_id, self.text_location, 40, 0) + blf.draw(font_id, "Extrude On" if pt_buf.ctrl else "Extrude Off") + + +class pen_tool_properties(PropertyGroup): + a = FloatProperty( + name="Alpha", + description="Set Font Alpha", + default=1.0, + min=0.1, max=1.0, + step=10, + precision=1 + ) + fs = IntProperty( + name="Size", + description="Set Font Size", + default=14, + min=12, max=40, + step=1 + ) + b0 = BoolProperty( + name="Angles", + description="Display All Angles on Drawn Edges", + default=False + ) + b1 = BoolProperty( + name="Edge Length", + description="Display All Lenghts of Drawn Edges", + default=False + ) + b2 = BoolProperty( + name="Mouse Location 3D", + description="Display the location coordinates of the mouse cursor", + default=False + ) + restore_view = BoolProperty( + name="Restore View", + description="After the tool has finished, is the Viewport restored\n" + "to it's previous state", + default=True + ) + + +class pt_buf(): + list_m_loc_2d = [] + list_m_loc_3d = [] + x = 0 + y = 0 + sws = 'off' + depth_location = Vector((0.0, 0.0, 0.0)) + alt = False + shift = False + ctrl = False + store_view_matrix = Matrix() + view_location = (0.0, 0.0, 0.0) + + +# ------ Panel ------ +class pen_tool_panel(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Tools" + bl_label = "Pen Tool" + bl_context = "mesh_edit" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + + if pt_buf.sws == "on": + layout.active = False + layout.label("Pen Tool Active") + else: + layout.label("Font:") + + row = layout.split(0.50, align=True) + row.prop(context.scene.pen_tool_props, "fs", text="Size", slider=True) + row.prop(context.scene.pen_tool_props, "a", text="Alpha", slider=True) + + layout.prop(context.scene.pen_tool_props, "b0", text="Angles") + layout.prop(context.scene.pen_tool_props, "b1", text="Edge Length") + layout.prop(context.scene.pen_tool_props, "b2", text="Mouse Location 3D") + layout.prop(context.scene.pen_tool_props, "restore_view", text="Restore View") + + row1 = layout.split(0.80, align=True) + row1.operator("pen_tool.operator", text="Draw") + row1.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "mesh_pen_tool" + + +# Operator +class pen_tool_operator(Operator): + bl_idname = "pen_tool.operator" + bl_label = "Pen Tool" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + text_location = IntProperty( + name="", + default=0, + options={'HIDDEN'} + ) + + @classmethod + def poll(cls, context): + # do not run in object mode + return (context.active_object and context.active_object.type == 'MESH' and + context.mode == 'EDIT_MESH') + + def execute(self, context): + edit_mode_out() + ob_act = context.active_object + bme = bmesh.new() + bme.from_mesh(ob_act.data) + + mtrx = ob_act.matrix_world.inverted() # ob_act matrix world inverted + + # add vertices + list_ = [] + for i in pt_buf.list_m_loc_3d: + bme.verts.new(mtrx * i) + bme.verts.index_update() + bme.verts.ensure_lookup_table() + list_.append(bme.verts[-1]) + + # add edges + n = len(list_) + for j in range(n - 1): + bme.edges.new((list_[j], list_[(j + 1) % n])) + bme.edges.index_update() + + bme.to_mesh(ob_act.data) + store_restore_view(context, False) + edit_mode_in() + + pt_buf.list_m_loc_2d[:] = [] + pt_buf.list_m_loc_3d[:] = [] + pt_buf.depth_location = Vector((0.0, 0.0, 0.0)) + pt_buf.store_view_matrix = Matrix() + pt_buf.view_location = (0.0, 0.0, 0.0) + pt_buf.ctrl = False + + context.area.tag_redraw() + return {'FINISHED'} + + def modal(self, context, event): + context.area.tag_redraw() + + # allow moving in the 3D View + if event.type in { + 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', + 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_6', + 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_5'}: + return {'PASS_THROUGH'} + + if event.type in {'LEFT_ALT', 'RIGHT_ALT'}: + if event.value == 'PRESS': + pt_buf.alt = True + if event.value == 'RELEASE': + pt_buf.alt = False + return {'RUNNING_MODAL'} + + elif event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}: + if event.value == 'PRESS': + pt_buf.ctrl = not pt_buf.ctrl + return {'RUNNING_MODAL'} + + elif event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}: + if event.value == 'PRESS': + pt_buf.shift = True + if event.value == 'RELEASE': + pt_buf.shift = False + return {'RUNNING_MODAL'} + + elif event.type == 'MOUSEMOVE': + if pt_buf.list_m_loc_2d != []: + pt_buf_list_m_loc_3d_last_2d = location_3d_to_region_2d( + context.region, + context.space_data.region_3d, + pt_buf.list_m_loc_3d[-1] + ) + if pt_buf.alt is True: + pt_buf.x = pt_buf_list_m_loc_3d_last_2d[0] + pt_buf.y = event.mouse_region_y + elif pt_buf.shift is True: + pt_buf.x = event.mouse_region_x + pt_buf.y = pt_buf_list_m_loc_3d_last_2d[1] + else: + pt_buf.x = event.mouse_region_x + pt_buf.y = event.mouse_region_y + else: + pt_buf.x = event.mouse_region_x + pt_buf.y = event.mouse_region_y + + elif event.type == 'LEFTMOUSE': + if event.value == 'PRESS': + mouse_loc_2d = Vector((pt_buf.x, pt_buf.y)) + pt_buf.list_m_loc_2d.append(mouse_loc_2d) + + mouse_loc_3d = region_2d_to_location_3d( + context.region, context.space_data.region_3d, + mouse_loc_2d, pt_buf.depth_location + ) + pt_buf.list_m_loc_3d.append(mouse_loc_3d) + + pt_buf.depth_location = pt_buf.list_m_loc_3d[-1] # <-- depth location + # run Extrude at cursor + if pt_buf.ctrl: + try: + bpy.ops.mesh.dupli_extrude_cursor('INVOKE_DEFAULT', rotate_source=False) + except: + pass + elif event.value == 'RELEASE': + pass + elif event.type == 'RIGHTMOUSE': + context.space_data.draw_handler_remove(self._handle_px, 'WINDOW') + self.execute(context) + pt_buf.sws = 'off' + return {'FINISHED'} + elif event.type == 'ESC': + context.space_data.draw_handler_remove(self._handle_px, 'WINDOW') + store_restore_view(context, False) + pt_buf.list_m_loc_2d[:] = [] + pt_buf.list_m_loc_3d[:] = [] + pt_buf.depth_location = Vector((0.0, 0.0, 0.0)) + pt_buf.sws = 'off' + pt_buf.store_view_matrix = Matrix() + pt_buf.view_location = (0.0, 0.0, 0.0) + pt_buf.ctrl = False + return {'CANCELLED'} + + # Return has to be modal or the tool can crash + # It's better to define PASS_THROUGH as the exception and not the default + return {'RUNNING_MODAL'} + + def invoke(self, context, event): + bme = bmesh.from_edit_mesh(context.active_object.data) + list_f = [f for f in bme.faces if f.select] + + if len(list_f) != 0: + f = list_f[0] + pt_buf.depth_location = f.calc_center_median() + align_view_to_face_(context, bme, f) + + if context.area.type == 'VIEW_3D': + # pre-compute the text location (thanks to the Carver add-on) + self.text_location = 100 + overlap = context.user_preferences.system.use_region_overlap + for region in context.area.regions: + if region.type == "WINDOW": + self.text_location = region.width - 100 + if overlap: + for region in context.area.regions: + if region.type == 'TOOL_PROPS': + self.text_location = self.text_location - region.width + + if pt_buf.sws == 'on': + return {'RUNNING_MODAL'} + elif pt_buf.sws != 'on': + context.window_manager.modal_handler_add(self) + self._handle_px = context.space_data.draw_handler_add( + draw_callback_px, + (self, context), + 'WINDOW', 'POST_PIXEL' + ) + pt_buf.sws = 'on' + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Pen Tool: View3D not found, operation cancelled") + return {'CANCELLED'} + + +class_list = [pen_tool_panel, + pen_tool_operator, + pen_tool_properties + ] + + +def register(): + for c in class_list: + bpy.utils.register_class(c) + + bpy.types.Scene.pen_tool_props = PointerProperty(type=pen_tool_properties) + + wm = bpy.context.window_manager + km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D') + + # Note: left click + D key is reserved for Grease Pencil draw + kmi = km.keymap_items.new("pen_tool.operator", 'D', 'PRESS', ctrl=True) + + +def unregister(): + for c in class_list: + bpy.utils.unregister_class(c) + + del bpy.types.Scene.pen_tool_props + + wm = bpy.context.window_manager + km = wm.keyconfigs.addon.keymaps['3D View'] + for kmi in km.keymap_items: + if kmi.idname == 'pen_tool.operator': + km.keymap_items.remove(kmi) + break + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_select_tools/__init__.py b/mesh_extra_tools/mesh_select_tools/__init__.py new file mode 100644 index 00000000..2d066c82 --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/__init__.py @@ -0,0 +1,68 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +# +# ##### END GPL LICENSE BLOCK ##### +# menu & updates by meta-androcto # +# contributed to by Macouno, dustractor, liero, CoDEmanX, meta-androcto # + +bl_info = { + "name": "Select Tools", + "author": "Multiple Authors", + "version": (0, 3), + "blender": (2, 64, 0), + "location": "Editmode Select Menu/Toolshelf Tools Tab", + "description": "Adds More vert/face/edge select modes.", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh" + } + +if "bpy" in locals(): + import imp + imp.reload(mesh_select_by_direction) + imp.reload(mesh_select_by_edge_length) + imp.reload(mesh_select_by_pi) + imp.reload(mesh_select_by_type) + imp.reload(mesh_select_connected_faces) + imp.reload(mesh_index_select) + imp.reload(mesh_selection_topokit) + imp.reload(mesh_info_select) +else: + from . import mesh_select_by_direction + from . import mesh_select_by_edge_length + from . import mesh_select_by_pi + from . import mesh_select_by_type + from . import mesh_select_connected_faces + from . import mesh_index_select + from . import mesh_selection_topokit + from . import mesh_info_select + +import bpy + + +# Register + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_select_tools/mesh_index_select.py b/mesh_extra_tools/mesh_select_tools/mesh_index_select.py new file mode 100644 index 00000000..cd558e85 --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/mesh_index_select.py @@ -0,0 +1,169 @@ +# gpl author: liero + +bl_info = { + "name": "Select by index", + "author": "liero", + "version": (0, 2), + "blender": (2, 55, 0), + "location": "View3D > Tool Shelf", + "description": "Select mesh data by index / area / length / cursor", + "category": "Mesh", + } + +import bpy +from bpy.types import Operator +from bpy.props import ( + BoolProperty, + FloatProperty, + EnumProperty, + ) + + +class SelVertEdgeFace(Operator): + bl_idname = "mesh.select_vert_edge_face_index" + bl_label = "Select mesh index" + bl_description = "Select Vertices, Edges, Faces by index" + bl_options = {"REGISTER", "UNDO"} + + select_type = EnumProperty( + items=[ + ('VERT', "Vertices", "Select Vertices by index"), + ('EDGE', "Edges", "Select Edges by index"), + ('FACE', "Faces", "Select Faces by index"), + ], + name="Selection Mode", + description="", + default='VERT', + ) + indice = FloatProperty( + name="Selected", + default=0, + min=0, max=100, + description="Percentage of selection", + precision=2, + subtype="PERCENTAGE" + ) + delta = BoolProperty( + name="Use Parameter", + default=False, + description="Select by Index / Parameter" + ) + flip = BoolProperty( + name="Reverse Order", + default=False, + description="Reverse selecting order" + ) + start_new = BoolProperty( + name="Fresh Start", + default=False, + description="Start from no previous selection\n" + "If unchecked the previous selection is kept" + ) + + delta_text = {'VERT': "Use Cursor", + 'EDGE': "Use Edges' Length", + 'FACE': "Use Faces' Area"} + + @classmethod + def poll(cls, context): + return (context.object is not None and context.object.type == 'MESH') + + def draw(self, context): + layout = self.layout + + layout.label("Selection Type:") + layout.prop(self, "select_type", text="") + layout.separator() + + layout.label("Selected:") + layout.prop(self, "indice", text="", slider=True) + + d_text = self.delta_text[self.select_type] + layout.prop(self, "delta", text=d_text) + + layout.prop(self, "flip") + layout.prop(self, "start_new") + + def execute(self, context): + obj = bpy.context.object + + if self.start_new: + bpy.ops.mesh.select_all(action='DESELECT') + + # Selection mode - Vertex, Edge, Face + if self.select_type == 'VERT': + bpy.context.tool_settings.mesh_select_mode = [True, False, False] + ver = obj.data.vertices + loc = context.scene.cursor_location + sel = [] + for v in ver: + d = v.co - loc + sel.append((d.length, v.index)) + sel.sort(reverse=self.flip) + bpy.ops.object.mode_set() + valor = round(len(sel) / 100 * self.indice) + if self.delta: + for i in range(len(sel[:valor])): + ver[sel[i][1]].select = True + else: + for i in range(len(sel[:valor])): + if self.flip: + ver[len(sel) - i - 1].select = True + else: + ver[i].select = True + + elif self.select_type == 'EDGE': + bpy.context.tool_settings.mesh_select_mode = [False, True, False] + ver = obj.data.vertices + edg = obj.data.edges + sel = [] + for e in edg: + d = ver[e.vertices[0]].co - ver[e.vertices[1]].co + sel.append((d.length, e.index)) + sel.sort(reverse=self.flip) + bpy.ops.object.mode_set() + valor = round(len(sel) / 100 * self.indice) + if self.delta: + for i in range(len(sel[:valor])): + edg[sel[i][1]].select = True + else: + for i in range(len(sel[:valor])): + if self.flip: + edg[len(sel) - i - 1].select = True + else: + edg[i].select = True + + elif self.select_type == 'FACE': + bpy.context.tool_settings.mesh_select_mode = [False, False, True] + fac = obj.data.polygons + sel = [] + for f in fac: + sel.append((f.area, f.index)) + sel.sort(reverse=self.flip) + bpy.ops.object.mode_set() + valor = round(len(sel) / 100 * self.indice) + if self.delta: + for i in range(len(sel[:valor])): + fac[sel[i][1]].select = True + else: + for i in range(len(sel[:valor])): + if self.flip: + fac[len(sel) - i - 1].select = True + else: + fac[i].select = True + + bpy.ops.object.mode_set(mode='EDIT') + + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(SelVertEdgeFace) + + +def unregister(): + bpy.utils.register_class(SelVertEdgeFace) + + +if __name__ == '__main__': + register() diff --git a/mesh_extra_tools/mesh_select_tools/mesh_info_select.py b/mesh_extra_tools/mesh_select_tools/mesh_info_select.py new file mode 100644 index 00000000..448bdbdc --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/mesh_info_select.py @@ -0,0 +1,108 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# By CoDEmanX + +import bpy +from bpy.types import ( + Panel, + Operator, + ) + + +class DATA_PT_info_panel(Panel): + """Creates a face info / select panel in the Object properties window""" + bl_label = "Face Info / Select" + bl_idname = "DATA_PT_face_info" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "data" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(self, context): + return (context.active_object is not None and + context.active_object.type == 'MESH') + + def draw(self, context): + layout = self.layout + + ob = context.active_object + + info_str = "" + tris = quads = ngons = 0 + + for p in ob.data.polygons: + count = p.loop_total + if count == 3: + tris += 1 + elif count == 4: + quads += 1 + else: + ngons += 1 + + info_str = " Ngons: %i Quads: %i Tris: %i" % (ngons, quads, tris) + + col = layout.column() + split = col.split(0.9) + + split.label(info_str, icon='MESH_DATA') + split.operator("mesh.refresh_info_panel", text="", icon="FILE_REFRESH") + + col = layout.column() + col.label("Select faces by type:") + + row = layout.row() + row.operator("data.facetype_select", text="Ngons").face_type = "5" + row.operator("data.facetype_select", text="Quads").face_type = "4" + row.operator("data.facetype_select", text="Tris").face_type = "3" + + +class MESH_OT_refresh_info_panel(Operator): + bl_idname = "mesh.refresh_info_panel" + bl_label = "Refresh" + bl_description = ("Refresh the info panel by switching to Object mode and back\n" + "Limitation: the information doesn't account modifiers\n" + "Be careful with usage if you need the Redo History in Edit Mode") + bl_options = {"REGISTER", "INTERNAL"} + + @classmethod + def poll(self, context): + return (context.active_object is not None and + context.active_object.type == 'MESH') + + def invoke(self, context, event): + return self.execute(context) + + def execute(self, context): + try: + mode = bpy.context.active_object.mode + + # switch to Object mode and restore selection + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode=mode) + + return {'FINISHED'} + except: + import traceback + traceback.print_exc() + + self.report({'WARNING'}, + "The refresh could not be performed (Check the console for more info)") + + return {'CANCELLED'} diff --git a/mesh_extra_tools/mesh_select_tools/mesh_select_by_direction.py b/mesh_extra_tools/mesh_select_tools/mesh_select_by_direction.py new file mode 100644 index 00000000..7a82b949 --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/mesh_select_by_direction.py @@ -0,0 +1,220 @@ +# Copyright (C) 2011, Dolf Veenvliet +# Extrude a selection from a mesh multiple times + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** +''' +bl_info = { + "name": "Select by direction", + "author": "Dolf Veenvliet", + "version": (1,), + "blender": (2, 56, 0), + "location": "View3D > Select", + "description": "Select all items whose normals face a certain direction", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + +""" +Additional links: + Author Site: http://www.macouno.com + e-mail: dolf {at} macouno {dot} com +""" +''' + +import bpy +from bpy.types import Operator +from mathutils import Vector +from math import radians +from bpy.props import ( + FloatVectorProperty, + FloatProperty, + BoolProperty, + EnumProperty, + ) + + +class Select_by_direction(): + + # Initialise the class + def __init__(self, context, direction, divergence, extend, space): + + self.ob = context.active_object + bpy.ops.object.mode_set(mode='OBJECT') + + self.space = space + + # if we do stuff in global space we need to object matrix + if self.space == 'GLO': + # TODO: not sure if this is the correct way to solve the crash - lijenstina + mat = self.ob.matrix_world + mat_rot = mat.to_3x3().inverted() + direction = mat_rot * Vector(direction) + else: + direction = Vector(direction) + + direction = direction.normalized() + + vertSelect = bpy.context.tool_settings.mesh_select_mode[0] + edgeSelect = bpy.context.tool_settings.mesh_select_mode[1] + faceSelect = bpy.context.tool_settings.mesh_select_mode[2] + + if Vector(direction).length: + # Vert select + if vertSelect: + hasSelected = self.hasSelected(self.ob.data.vertices) + + for v in self.ob.data.vertices: + normal = v.normal + s = self.selectCheck(v.select, hasSelected, extend) + d = self.deselectCheck(v.select, hasSelected, extend) + + if s or d: + angle = direction.angle(normal) + + # Check if the verts match any of the directions + if s and angle <= divergence: + v.select = True + + if d and angle > divergence: + v.select = False + # Edge select + if edgeSelect: + hasSelected = self.hasSelected(self.ob.data.edges) + + for e in self.ob.data.edges: + s = self.selectCheck(e.select, hasSelected, extend) + d = self.deselectCheck(e.select, hasSelected, extend) + + # Check if the edges match any of the directions + if s or d: + normal = self.ob.data.vertices[e.vertices[0]].normal + normal += self.ob.data.vertices[e.vertices[1]].normal + + angle = direction.angle(normal) + + if s and angle <= divergence: + e.select = True + + if d and angle > divergence: + e.select = False + + # Face select + if faceSelect: + hasSelected = self.hasSelected(self.ob.data.polygons) + + # Loop through all the given faces + for f in self.ob.data.polygons: + s = self.selectCheck(f.select, hasSelected, extend) + d = self.deselectCheck(f.select, hasSelected, extend) + + if s or d: + angle = direction.angle(f.normal) + + # Check if the faces match any of the directions + if s and angle <= divergence: + f.select = True + + if d and angle > divergence: + f.select = False + + bpy.ops.object.mode_set(mode='EDIT') + + # See if the current item should be selected or not + def selectCheck(self, isSelected, hasSelected, extend): + # If the current item is not selected we may want to select + if not isSelected: + # If we are extending or nothing is selected we want to select + if extend or not hasSelected: + return True + + return False + + # See if the current item should be deselected or not + def deselectCheck(self, isSelected, hasSelected, extend): + # If the current item is selected we may want to deselect + if isSelected: + + # If something is selected and we're not extending we want to deselect + if hasSelected and not extend: + return True + + return False + + # See if there is at least one selected item + def hasSelected(self, items): + for item in items: + if item.select: + return True + + return False + + +class Select_init(Operator): + bl_idname = "mesh.select_by_direction" + bl_label = "Select by direction" + bl_description = ("Select all items with normals facing a certain direction,\n" + "defined by a vector with coordinates X, Y, Z") + bl_options = {'REGISTER', 'UNDO'} + + direction = FloatVectorProperty( + name="Direction", + description="Define a vector from the inputs axis X, Y, Z\n" + "Used to define the normals direction", + default=(0.0, 0.0, 1.0), + min=-100.0, max=100.0, + soft_min=-10.0, soft_max=10.0, + step=100, + precision=2 + ) + divergence = FloatProperty( + name="Divergence", + description="The number of degrees the selection may differ from the Vector\n" + "(Input is converted to radians)", + default=radians(30.0), + min=0.0, max=radians(360.0), + soft_min=0.0, soft_max=radians(360.0), + step=radians(5000), + precision=2, + subtype='ANGLE' + ) + extend = BoolProperty( + name="Extend", + description="Extend the current selection", + default=False + ) + # The spaces we use + spaces = (('LOC', 'Local', ''), ('GLO', 'Global', '')) + space = EnumProperty( + items=spaces, + name="Space", + description="The space to interpret the directions in", + default='LOC' + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj and obj.type == 'MESH') + + def execute(self, context): + Select_by_direction(context, self.direction, self.divergence, self.extend, self.space) + + return {'FINISHED'} diff --git a/mesh_extra_tools/mesh_select_tools/mesh_select_by_edge_length.py b/mesh_extra_tools/mesh_select_tools/mesh_select_by_edge_length.py new file mode 100644 index 00000000..8430355c --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/mesh_select_by_edge_length.py @@ -0,0 +1,248 @@ +# mesh_select_by_edge_length.py Copyright (C) 2011, Dolf Veenvliet +# Extrude a selection from a mesh multiple times + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +''' +bl_info = { + "name": "Select by edge length", + "author": "Dolf Veenvliet", + "version": (1,), + "blender": (2, 56, 0), + "location": "View3D > Select", + "description": "Select all items whose scale/length/surface matches a certain edge length", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + +""" +Usage: + +Launch from from "Select -> By edge length" + +Additional links: + Author Site: http://www.macouno.com + e-mail: dolf {at} macouno {dot} com +""" +''' + +import bpy +from bpy.props import ( + FloatProperty, + BoolProperty, + EnumProperty, + ) + + +class Select_by_edge_length(): + + # Initialize the class + def __init__(self, context, edgeLength, edgeSize, extend, space, start_new): + + if start_new: + bpy.ops.mesh.select_all(action='DESELECT') + + self.ob = context.active_object + bpy.ops.object.mode_set(mode='OBJECT') + + self.space = space + self.obMat = self.ob.matrix_world + + bigger = (True if edgeSize == 'BIG' else False) + smaller = (True if edgeSize == 'SMALL' else False) + + # We ignore vert selections completely + edgeSelect = bpy.context.tool_settings.mesh_select_mode[1] + faceSelect = bpy.context.tool_settings.mesh_select_mode[2] + + # Edge select + if edgeSelect: + hasSelected = self.hasSelected(self.ob.data.edges) + + for e in self.ob.data.edges: + + if self.selectCheck(e.select, hasSelected, extend): + + lene = self.getEdgeLength(e.vertices) + + if (lene == edgeLength or (bigger and lene >= edgeLength) or + (smaller and lene <= edgeLength)): + e.select = True + + if self.deselectCheck(e.select, hasSelected, extend): + lene = self.getEdgeLength(e.vertices) + + if (lene != edgeLength and not (bigger and lene >= edgeLength) and + not (smaller and lene <= edgeLength)): + e.select = False + + # Face select + if faceSelect: + hasSelected = self.hasSelected(self.ob.data.polygons) + + # Loop through all the given faces + for f in self.ob.data.polygons: + + # Check if the faces match any of the directions + if self.selectCheck(f.select, hasSelected, extend): + + mine, maxe = 0.0, 0.0 + + for i, e in enumerate(f.edge_keys): + lene = self.getEdgeLength(e) + if not i: + mine = lene + maxe = lene + elif lene < mine: + mine = lene + elif lene > maxe: + maxe = lene + + if ((mine == edgeLength and maxe == edgeLength) or + (bigger and mine >= edgeLength) or + (smaller and maxe <= edgeLength)): + + f.select = True + + if self.deselectCheck(f.select, hasSelected, extend): + + mine, maxe = 0.0, 0.0 + + for i, e in enumerate(f.edge_keys): + lene = self.getEdgeLength(e) + if not i: + mine = lene + maxe = lene + elif lene < mine: + mine = lene + elif lene > maxe: + maxe = lene + + if ((mine != edgeLength and maxe != edgeLength) and + not (bigger and mine >= edgeLength) and + not (smaller and maxe <= edgeLength)): + + f.select = False + + bpy.ops.object.mode_set(mode='EDIT') + + # Get the lenght of an edge, by giving this function all verts (2) in the edge + def getEdgeLength(self, verts): + + vec1 = self.ob.data.vertices[verts[0]].co + vec2 = self.ob.data.vertices[verts[1]].co + + vec = vec1 - vec2 + + if self.space == 'GLO': + vec = self.obMat * vec + + return round(vec.length, 5) + + # See if the current item should be selected or not + def selectCheck(self, isSelected, hasSelected, extend): + + # If the current item is not selected we may want to select + if not isSelected: + + # If we are extending or nothing is selected we want to select + if extend or not hasSelected: + return True + + return False + + # See if the current item should be deselected or not + def deselectCheck(self, isSelected, hasSelected, extend): + + # If the current item is selected we may want to deselect + if isSelected: + + # If something is selected and we're not extending we want to deselect + if hasSelected and not extend: + return True + + return False + + # See if there is at least one selected item + def hasSelected(self, items): + + for item in items: + if item.select: + return True + + return False + + +class Select_init(bpy.types.Operator): + bl_idname = "mesh.select_by_edge_length" + bl_label = "Select by edge length" + bl_description = ("Select all items whose scale/length/surface matches a certain edge length \n" + "Does not work in Vertex Select mode") + bl_options = {'REGISTER', 'UNDO'} + + edgeLength = FloatProperty( + name="Edge length", + description="The comparison scale in Blender units", + default=1.0, + min=0.0, max=1000.0, + soft_min=0.0, soft_max=100.0, + step=100, + precision=2 + ) + # Changed to Enum as two separate Booleans didn't make much sense + sizes = (('SMALL', 'Smaller', "Select items smaller or equal the size setting"), + ('BIG', 'Bigger', "Select items bigger or equal to the size setting"), + ('EQUAL', 'Equal', "Select edges equal to the size setting")) + edgeSize = EnumProperty( + items=sizes, + name="Edge comparison", + description="Choose the relation to set edge lenght", + default='EQUAL' + ) + extend = BoolProperty( + name="Extend", + description="Extend the current selection", + default=False + ) + start_new = BoolProperty( + name="Fresh Start", + default=False, + description="Start from no previous selection" + ) + # The spaces we use + spaces = (('LOC', 'Local', "Use Local space"), + ('GLO', 'Global', "Use Global Space")) + space = EnumProperty( + items=spaces, + name="Space", + description="The space to interpret the directions in", + default='LOC' + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj and obj.type == 'MESH' and not bpy.context.tool_settings.mesh_select_mode[0]) + + def execute(self, context): + Select_by_edge_length(context, self.edgeLength, self.edgeSize, + self.extend, self.space, self.start_new) + + return {'FINISHED'} diff --git a/mesh_extra_tools/mesh_select_tools/mesh_select_by_pi.py b/mesh_extra_tools/mesh_select_tools/mesh_select_by_pi.py new file mode 100644 index 00000000..6f2d4418 --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/mesh_select_by_pi.py @@ -0,0 +1,210 @@ +# mesh_select_by_pi.py Copyright (C) 2011, Dolf Veenvliet +# Extrude a selection from a mesh multiple times + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +''' +bl_info = { + "name": "Select by pi", + "author": "Dolf Veenvliet", + "version": 1, + "blender": (2, 56, 0), + "location": "View3D > Select", + "description": "Select fake random based on pi", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + +""" +Usage: + +Additional links: + Author Site: http://www.macouno.com + e-mail: dolf {at} macouno {dot} com +""" +''' + +import bpy +from bpy.types import Operator +from bpy.props import BoolProperty + + +class Select_by_pi(): + + # Initialise the class + def __init__(self, context, e, invert, extend, start_new): + + self.ob = context.active_object + # keep or not the original selection (helps with selected all) + if start_new: + bpy.ops.mesh.select_all(action='DESELECT') + + bpy.ops.object.mode_set(mode='OBJECT') + + self.invert = invert + + # Make pi as a list of integers + if e: + self.pi = list('27182818284590452353602874713526624977572470936999') + else: + self.pi = list('31415926535897932384626433832795028841971693993751') + + self.piLen = len(self.pi) + self.piPos = 0 + + vertSelect = bpy.context.tool_settings.mesh_select_mode[0] + edgeSelect = bpy.context.tool_settings.mesh_select_mode[1] + faceSelect = bpy.context.tool_settings.mesh_select_mode[2] + + # Vert select + if vertSelect: + hasSelected = self.hasSelected(self.ob.data.vertices) + + for v in self.ob.data.vertices: + s = self.selectCheck(v.select, hasSelected, extend) + d = self.deselectCheck(v.select, hasSelected, extend) + + # Check if the verts match any of the directions + if s and self.choose(): + v.select = True + + if d and not self.choose(): + v.select = False + + # Edge select + if edgeSelect: + hasSelected = self.hasSelected(self.ob.data.edges) + + for e in self.ob.data.edges: + s = self.selectCheck(e.select, hasSelected, extend) + d = self.deselectCheck(e.select, hasSelected, extend) + + if s and self.choose(): + e.select = True + + if d and not self.choose(): + e.select = False + + # Face select + if faceSelect: + hasSelected = self.hasSelected(self.ob.data.polygons) + + # Loop through all the given faces + for f in self.ob.data.polygons: + s = self.selectCheck(f.select, hasSelected, extend) + d = self.deselectCheck(f.select, hasSelected, extend) + + # Check if the faces match any of the directions + if s and self.choose(): + f.select = True + + if d and not self.choose(): + f.select = False + + bpy.ops.object.mode_set(mode='EDIT') + + # Choose by pi + def choose(self): + choice = True + + # We just choose the odd numbers + if int(self.pi[self.piPos]) % 2: + choice = False + + if self.invert: + choice = not choice + + self.incrementPiPos() + return choice + + # Increment the pi position + def incrementPiPos(self): + self.piPos += 1 + if self.piPos == self.piLen: + self.piPos = 0 + + # See if the current item should be selected or not + def selectCheck(self, isSelected, hasSelected, extend): + + # If the current item is not selected we may want to select + if not isSelected: + + # If we are extending or nothing is selected we want to select + if extend or not hasSelected: + return True + + return False + + # See if the current item should be deselected or not + def deselectCheck(self, isSelected, hasSelected, extend): + # If the current item is selected we may want to deselect + if isSelected: + # If something is selected and we're not extending we want to deselect + if hasSelected and not extend: + return True + + return False + + # See if there is at least one selected item + def hasSelected(self, items): + for item in items: + if item.select: + return True + + return False + + +class Select_init(Operator): + bl_idname = "mesh.select_by_pi" + bl_label = "Select by Pi or e" + bl_description = ("Select Vertices/Edges/Faces based on pi or e for a random-like selection\n" + "Number Pi (3.14 etc.) or e (2.71828 - Euler's number)") + bl_options = {'REGISTER', 'UNDO'} + + e = BoolProperty( + name="Use e", + description="Use e as the base of selection instead of pi", + default=False + ) + invert = BoolProperty( + name="Invert", + description="Invert the selection result", + default=False + ) + extend = BoolProperty( + name="Extend", + description="Extend the current selection", + default=False + ) + start_new = BoolProperty( + name="Fresh Start", + default=False, + description="Start from no previous selection" + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj and obj.type == 'MESH') + + def execute(self, context): + Select_by_pi(context, self.e, self.invert, self.extend, self.start_new) + + return {'FINISHED'} diff --git a/mesh_extra_tools/mesh_select_tools/mesh_select_by_type.py b/mesh_extra_tools/mesh_select_tools/mesh_select_by_type.py new file mode 100644 index 00000000..b718d453 --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/mesh_select_by_type.py @@ -0,0 +1,78 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# By CoDEmanX + +import bpy +from bpy.types import Operator +from bpy.props import ( + EnumProperty, + BoolProperty, + ) + + +class DATA_OP_facetype_select(Operator): + bl_idname = "data.facetype_select" + bl_label = "Select by face type" + bl_description = "Select all faces of a certain type" + bl_options = {'REGISTER', 'UNDO'} + + face_type = EnumProperty( + name="Select faces:", + items=(("3", "Triangles", "Faces made up of 3 vertices"), + ("4", "Quads", "Faces made up of 4 vertices"), + ("5", "Ngons", "Faces made up of 5 and more vertices")), + default="5" + ) + extend = BoolProperty( + name="Extend", + description="Extend Selection", + default=False + ) + + @classmethod + def poll(cls, context): + return context.active_object is not None and context.active_object.type == 'MESH' + + def execute(self, context): + try: + bpy.ops.object.mode_set(mode='EDIT') + + if not self.extend: + bpy.ops.mesh.select_all(action='DESELECT') + + context.tool_settings.mesh_select_mode = (False, False, True) + + if self.face_type == "3": + bpy.ops.mesh.select_face_by_sides(number=3, type='EQUAL') + elif self.face_type == "4": + bpy.ops.mesh.select_face_by_sides(number=4, type='EQUAL') + else: + bpy.ops.mesh.select_face_by_sides(number=4, type='GREATER') + + return {'FINISHED'} + + except Exception as e: + print("\n[Select by face type]\nERROR:\n") + + import traceback + traceback.printexc() + + self.report('WARNING', "Face selection could not be performed (Check the console for more info)") + + return {'CANCELLED'} diff --git a/mesh_extra_tools/mesh_select_tools/mesh_select_connected_faces.py b/mesh_extra_tools/mesh_select_tools/mesh_select_connected_faces.py new file mode 100644 index 00000000..ff131f9c --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/mesh_select_connected_faces.py @@ -0,0 +1,146 @@ +# Copyright (C) 2011, Dolf Veenvliet +# Extrude a selection from a mesh multiple times + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +''' + bl_info = { + "name": "Select connected faces", + "author": "Dolf Veenvliet", + "version": (1,), + "blender": (2, 56, 0), + "location": "View3D > Select", + "description": "Select all faces connected to the current selection", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + +""" +Usage: +Launch from from "Select -> Connected faces" + +Additional links: + Author Site: http://www.macouno.com + e-mail: dolf {at} macouno {dot} com +""" +''' + +import bpy +from bpy.types import Operator +from bpy.props import ( + IntProperty, + BoolProperty, + ) + + +class Select_connected_faces(): + # Initialize the class + def __init__(self, context, iterations, extend): + + self.ob = context.active_object + bpy.ops.object.mode_set(mode='OBJECT') + + # Make a list of all selected vertices + selVerts = [v.index for v in self.ob.data.vertices if v.select] + hasSelected = self.hasSelected(self.ob.data.polygons) + + for i in range(iterations): + nextVerts = [] + + for f in self.ob.data.polygons: + if self.selectCheck(f.select, hasSelected, extend): + + for v in f.vertices: + if v in selVerts: + f.select = True + + if f.select: + for v in f.vertices: + if v not in selVerts: + nextVerts.append(v) + + elif self.deselectCheck(f.select, hasSelected, extend): + for v in f.vertices: + if v in selVerts: + f.select = False + + selVerts = nextVerts + + bpy.ops.object.mode_set(mode='EDIT') + + # See if the current item should be selected or not + def selectCheck(self, isSelected, hasSelected, extend): + # If the current item is not selected we may want to select + if not isSelected: + return True + + return False + + # See if the current item should be deselected or not + def deselectCheck(self, isSelected, hasSelected, extend): + # If the current item is selected we may want to deselect + if isSelected: + # If something is selected and we're not extending we want to deselect + if hasSelected and not extend: + return True + + return False + + # See if there is at least one selected item + def hasSelected(self, items): + for item in items: + if item.select: + return True + + return False + + +class Select_init(Operator): + bl_idname = "mesh.select_connected_faces" + bl_label = "Select connected faces" + bl_description = ("Select all faces connected to the current selection \n" + "Works only in Face Selection mode") + bl_options = {'REGISTER', 'UNDO'} + + # Iterations + iterations = IntProperty( + name="Iterations", + default=1, + min=0, max=300, + soft_min=0, soft_max=100 + ) + extend = BoolProperty( + name="Extend", + description="Extend the current selection", + default=False + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return (obj and obj.type == 'MESH' and + bpy.context.tool_settings.mesh_select_mode[0] is False and + bpy.context.tool_settings.mesh_select_mode[1] is False and + bpy.context.tool_settings.mesh_select_mode[2] is True) + + def execute(self, context): + Select_connected_faces(context, self.iterations, self.extend) + + return {'FINISHED'} diff --git a/mesh_extra_tools/mesh_select_tools/mesh_selection_topokit.py b/mesh_extra_tools/mesh_select_tools/mesh_selection_topokit.py new file mode 100644 index 00000000..232714c4 --- /dev/null +++ b/mesh_extra_tools/mesh_select_tools/mesh_selection_topokit.py @@ -0,0 +1,628 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110 - 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +bl_info = { + "name": "Topokit 2", + "author": "dustractor", + "version": (2, 0), + "blender": (2, 60, 0), + "location": "edit mesh vertices/edges/faces menus", + "description": "", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + + +import bpy +from bpy.types import Operator +# In between calls, this stores any data that is expensive or static, +# matched to the size of the mesh and the id of the operator that created it +cachedata = dict() +# tkey is moved to mesh_extra_tools\__init__.py register function + + +# just a mix-in for the operators... +class meshpoller: + @classmethod + def poll(self, context): + try: + assert context.active_object.type == "MESH" + except: + return False + finally: + return True + + +# BEGIN VERTICES SECTION + +# This one works similarly to normal 'grow' (ctrl + NUMPAD_PLUS), +# except the original selection is not part of the result, +# +# 0--0--0 0--1--0 +# | | | | | | +# 0--1--0 --> 1--0--1 +# | | | | | | +# 0--0--0 0--1--0 + +class MESH_OT_vneighbors_edgewise(meshpoller, Operator): + bl_idname = "mesh.v2v_by_edge" + bl_label = "Neighbors by Edge" + bl_description = ("Select neighbour vertices of a starting selected vertex\n" + "Similar to Grow Selection - apart from the\n" + "original selection is not part of the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + next_state = bytearray(meshkey[0]) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + vert_to_vert_map, prev_state = cachedata[meshkey] + else: + vert_to_vert_map = {i: {} for i in range(meshkey[0])} + for a, b in mesh.edge_keys: + vert_to_vert_map[a][b] = 1 + vert_to_vert_map[b][a] = 1 + obj.tkkey = meshkey + prev_state = None + + if not prev_state: + selected_vert_indices = filter(lambda _: mesh.vertices[_].select, + range(len(mesh.vertices))) + else: + selected_vert_indices = filter( + lambda _: mesh.vertices[_].select and not prev_state[_], + range(len(mesh.vertices)) + ) + + for v in selected_vert_indices: + for neighbor_index in vert_to_vert_map[v]: + next_state[neighbor_index] = True + mesh.vertices.foreach_set("select", next_state) + cachedata[meshkey] = (vert_to_vert_map, next_state) + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +# This one is an alternate / counterpart to the previous. +# Think: diagonal opposite corners of a quad +# NOTE: does not apply to a triangle, since verts have no "opposite" +# +# 0--0--0 1--0--1 +# | | | | | | +# 0--1--0 --> 0--0--0 +# | | | | | | +# 0--0--0 1--0--1 + +class MESH_OT_vneighbors_facewise(meshpoller, Operator): + bl_idname = "mesh.v2v_facewise" + bl_label = "Neighbors by Face - Edge" + bl_description = ("Select diagonal opposite vertices of neighbour quads\n" + "Does not work with triangles\n" + "The original selection is not part of the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + next_state = bytearray(meshkey[0]) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + vert_to_vert_map = cachedata[meshkey] + else: + vert_to_vert_map = {i: {} for i in range(meshkey[0])} + for a, b in mesh.edge_keys: + vert_to_vert_map[a][b] = 1 + vert_to_vert_map[b][a] = 1 + obj.tkkey = meshkey + faces = filter(lambda face: (len(face.vertices) == 4) and + (face.select is False), mesh.polygons) + for f in faces: + has = False + t = set() + for v in f.vertices: + if mesh.vertices[v].select: + has = True + t.update(vert_to_vert_map[v]) + if has: + for v in f.vertices: + if not mesh.vertices[v].select: + if v not in t: + next_state[v] = 1 + mesh.vertices.foreach_set("select", next_state) + cachedata[meshkey] = vert_to_vert_map + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + +# END VERTICES SECTION + + +# BEGIN EDGES SECTION +# +--0--+--0--+--0--+ +--0--+--0--+--0--+ +# | | | | | | | | +# 0 0 0 0 0 1 1 0 +# | | | | | | | | +# +--0--+--1--+--0--+ ---> +--0--+--0--+--0--+ +# | | | | | | | | +# 0 0 0 0 0 1 1 0 +# | | | | | | | | +# +--0--+--0--+--0--+ +--0--+--0--+--0--+ + +class MESH_OT_eneighbors_shared_v_f(meshpoller, Operator): + bl_idname = "mesh.e2e_evfe" + bl_label = "Neighbors by Vert and Face" + bl_description = ("Select edges that share the neighbour vertices and faces\n" + "of the starting selected edge\n" + "The original selection is not part of the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + state_mask = bytearray(meshkey[1]) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + edge_to_edges_dict = cachedata + else: + edge_key_to_index = {k: i for i, k in enumerate(mesh.edge_keys)} + edge_to_edges_dict = {i: set() for i in range(len(mesh.edges))} + for f in mesh.polygons: + fed = [edge_key_to_index[k] for k in f.edge_keys] + for k in f.edge_keys: + edge_to_edges_dict[edge_key_to_index[k]].update(fed) + obj.tkkey = meshkey + + for e in filter(lambda _: mesh.edges[_].select, edge_to_edges_dict): + k1 = set(mesh.edges[e].key) + for n in edge_to_edges_dict[e]: + k2 = set(mesh.edges[n].key) + if not k1.isdisjoint(k2): + state_mask[n] = True + + for e in mesh.edges: + e.select ^= state_mask[e.index] + cachedata[meshkey] = edge_key_to_index + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +# +--0--+--0--+--0--+ +--0--+--0--+--0--+ +# | | | | | | | | +# 0 0 0 0 0 1 1 0 +# | | | | | | | | +# +--0--+--1--+--0--+ ---> +--1--+--0--+--1--+ +# | | | | | | | | +# 0 0 0 0 0 1 1 0 +# | | | | | | | | +# +--0--+--0--+--0--+ +--0--+--0--+--0--+ + +class MESH_OT_eneighbors_shared_v(meshpoller, Operator): + bl_idname = "mesh.e2e_eve" + bl_label = "Neighbors by Vert" + bl_description = ("Select edges that share the neighbour vertices\n" + "of the starting selected edge\n" + "The original selection is not part of the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + bpy.ops.object.mode_set(mode="OBJECT") + mesh = context.active_object.data + state_mask = bytearray(len(mesh.edges)) + + for e in mesh.edges: + state_mask[e.index] = mesh.vertices[e.vertices[0]].select ^ mesh.vertices[e.vertices[1]].select + mesh.edges.foreach_set('select', state_mask) + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +# +--0--+--0--+--0--+ +--0--+--1--+--0--+ +# | | | | | | | | +# 0 0 0 0 0 1 1 0 +# | | | | | | | | +# +--0--+--1--+--0--+ ---> +--0--+--0--+--0--+ +# | | | | | | | | +# 0 0 0 0 0 1 1 0 +# | | | | | | | | +# +--0--+--0--+--0--+ +--0--+--1--+--0--+ + +class MESH_OT_eneighbors_shared_f(meshpoller, Operator): + bl_idname = "mesh.e2e_efe" + bl_label = "Neighbors by Face" + bl_description = ("Select edges of neighbour faces to the starting selected edge\n" + "The original selection is not part of the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + edge_to_edges_dict = cachedata + else: + edge_key_to_index = {k: i for i, k in enumerate(mesh.edge_keys)} + edge_to_edges_dict = {i: set() for i in range(len(mesh.edges))} + for f in mesh.polygons: + fed = [edge_key_to_index[k] for k in f.edge_keys] + for k in f.edge_keys: + edge_to_edges_dict[edge_key_to_index[k]].update(fed) + obj.tkkey = meshkey + state_mask, esel = (bytearray(meshkey[1]), bytearray(meshkey[1])) + mesh.edges.foreach_get('select', esel) + + for e in filter(lambda _: mesh.edges[_].select, range(meshkey[1])): + for n in edge_to_edges_dict[e]: + state_mask[n] = 1 + + for e in range(meshkey[1]): + esel[e] ^= state_mask[e] + mesh.edges.foreach_set('select', esel) + cachedata[meshkey] = edge_to_edges_dict + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +# Notice that on these next two, the original selection stays +# +--0--+--0--+--0--+ +--0--+--1--+--0--+ +# | | | | | | | | +# 0 0 0 0 0 0 0 0 +# | | | | | | | | +# +--0--+--1--+--0--+ ---> +--0--+--1--+--0--+ +# | | | | | | | | +# 0 0 0 0 0 0 0 0 +# | | | | | | | | +# +--0--+--0--+--0--+ +--0--+--1--+--0--+ + +class MESH_OT_eneighbors_shared_f_notv(meshpoller, Operator): + bl_idname = "mesh.e2e_efnve" + bl_label = "Lateral Neighbors" + bl_description = ("Select edges that are lateral neighbours\n" + "The original selection is included in the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + state_mask = bytearray(meshkey[1]) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + edge_to_face_map, edge_key_to_index = cachedata[meshkey] + else: + edge_key_to_index = {} + edge_to_face_map = {i: set() for i in range(meshkey[1])} + for i, k in enumerate(mesh. edge_keys): + edge_key_to_index[k] = i + + for f in mesh.polygons: + for k in f.edge_keys: + edge_to_face_map[edge_key_to_index[k]].add(f.index) + obj.tkkey = meshkey + selected_edge_indices = filter(lambda _: mesh.edges[_].select, range(meshkey[1])) + + for e in selected_edge_indices: + for f in edge_to_face_map[e]: + for k in mesh.polygons[f].edge_keys: + hasv_in = False + for v in mesh.edges[e].key: + if v in k: + hasv_in = True + if hasv_in: + continue + else: + state_mask[edge_key_to_index[k]] = True + + for e in filter(lambda _: state_mask[_], range(meshkey[1])): + mesh.edges[e].select |= state_mask[e] + cachedata[meshkey] = (edge_to_face_map, edge_key_to_index) + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +# +--0--+--0--+--0--+ +--0--+--0--+--0--+ +# | | | | | | | | +# 0 0 0 0 0 0 0 0 +# | | | | | | | | +# +--0--+--1--+--0--+ ---> +--1--+--1--+--1--+ +# | | | | | | | | +# 0 0 0 0 0 0 0 0 +# | | | | | | | | +# +--0--+--0--+--0--+ +--0--+--0--+--0--+ + +class MESH_OT_eneighbors_shared_v_notf(meshpoller, Operator): + bl_idname = "mesh.e2e_evnfe" + bl_label = "Longitudinal Edges" + bl_description = ("Select Edges along the same longitude of the starting edge\n" + "The original selection is included in the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + state_mask = bytearray(meshkey[1]) + vstate = bytearray(meshkey[0]) + mesh.vertices.foreach_get('select', vstate) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + edge_to_face_map, vert_to_vert_map, edge_key_to_index = cachedata[meshkey] + else: + edge_key_to_index = {} + vert_to_vert_map = {i: set() for i in range(meshkey[0])} + edge_to_face_map = {i: set() for i in range(meshkey[1])} + + for i, k in enumerate(mesh.edge_keys): + edge_key_to_index[k] = i + vert_to_vert_map[k[0]].add(k[1]) + vert_to_vert_map[k[1]].add(k[0]) + + for f in mesh.polygons: + for k in f.edge_keys: + edge_to_face_map[edge_key_to_index[k]].add(f.index) + obj.tkkey = meshkey + selected_edge_indices = filter(lambda _: mesh.edges[_].select, range(meshkey[1])) + + for e in selected_edge_indices: + for v in mesh.edges[e].key: + state_mask[v] ^= 1 + + for f in edge_to_face_map[e]: + for v in mesh.polygons[f].vertices: + vstate[v] = 1 + + for v in filter(lambda _: state_mask[_], range(meshkey[1])): + for n in vert_to_vert_map[v]: + if not vstate[n] and (n != v): + mesh.edges[edge_key_to_index[(min(v, n), max(v, n))]].select = True + cachedata[meshkey] = (edge_to_face_map, vert_to_vert_map, edge_key_to_index) + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +# Deselects edges which are at the edge of a face-selection, +# causing selection to 'shrink in' +class MESH_OT_inner_edges(meshpoller, Operator): + bl_idname = "mesh.ie" + bl_label = "Inner Edge Selection" + bl_description = ("Deselects edges which are at the border\n" + "of a starting face selection\n" + "causing the selection to shrink inwards") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + state_mask = bytearray(meshkey[1]) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + edge_to_face_map = cachedata[meshkey] + else: + edge_key_to_index = {k: i for i, k in enumerate(mesh.edge_keys)} + edge_to_face_map = {i: set() for i in range(meshkey[1])} + for f in mesh.polygons: + for k in f.edge_keys: + edge_to_face_map[edge_key_to_index[k]].add(f.index) + obj.tkkey = meshkey + + for e in filter(lambda _: mesh.edges[_].select, range(meshkey[1])): + for f in edge_to_face_map[e]: + if mesh.polygons[f].select: + state_mask[e] ^= 1 + + for e in range(meshkey[1]): + mesh.edges[e].select ^= state_mask[e] + cachedata[meshkey] = edge_to_face_map + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + +# END EDGES SECTION + + +# BEGIN FACES SECTION + +# here is another one which functions very similarly to the ctrl+NUMPAD_PLUS 'growth' +# but it deselects the original selection, of course. +# This would be your checkerboard-type growth. +# [0][0][0] [0][1][0] +# [0][1][0] ---> [1][0][1] +# [0][0][0] [0][1][0] + +class MESH_OT_fneighbors_shared_e(meshpoller, Operator): + bl_idname = "mesh.f2f_fef" + bl_label = "Neighbor Faces sharing an Edge" + bl_description = ("Selects faces that share an edge with the starting face selection\n" + "Similar to the Grow selection \n" + "The original selection is not part of the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + face_to_face_map = cachedata[meshkey] + else: + edge_key_to_index = {k: i for i, k in enumerate(mesh.edge_keys)} + edge_to_face_map = {i: set() for i in range(meshkey[1])} + for f in mesh.polygons: + for k in f.edge_keys: + edge_to_face_map[edge_key_to_index[k]].add(f.index) + face_to_face_map = {i: set() for i in range(meshkey[2])} + for f in mesh.polygons: + for k in f.edge_keys: + face_to_face_map[f.index].update(edge_to_face_map[edge_key_to_index[k]]) + obj.tkkey = meshkey + mask_state = bytearray(meshkey[2]) + + for f in filter(lambda _: mesh.polygons[_].select, range(meshkey[2])): + for n in face_to_face_map[f]: + mask_state[n] = True + + for f in range(meshkey[2]): + mesh.polygons[f].select ^= mask_state[f] + cachedata[meshkey] = face_to_face_map + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +# [0][0][0] [1][0][1] +# [0][1][0] ---> [0][0][0] +# [0][0][0] [1][0][1] + +class MESH_OT_fneighbors_shared_v_note(meshpoller, Operator): + bl_idname = "mesh.f2f_fvnef" + bl_label = "Neighbors by Vertex not Edge" + bl_description = ("Select neighbour faces that share a vertex\n" + "with the starting selection\n" + "The original selection is not part of the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + edge_key_to_index = cachedata[meshkey] + else: + edge_key_to_index = {k: i for i, k in enumerate(mesh.edge_keys)} + obj.tkkey = meshkey + state_mask = bytearray(meshkey[2]) + face_verts = set() + + for f in filter(lambda _: mesh.polygons[_].select, range(meshkey[2])): + face_verts.update(mesh.polygons[f].vertices) + + for f in filter(lambda _: not mesh.polygons[_].select, range(meshkey[2])): + ct = 0 + for v in mesh.polygons[f].vertices: + ct += (v in face_verts) + if ct == 1: + state_mask[f] = 1 + mesh.polygons.foreach_set('select', state_mask) + cachedata[meshkey] = edge_key_to_index + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +# https://en.wikipedia.org/wiki/Conway's_Game_of_Life +class MESH_OT_conway(meshpoller, Operator): + bl_idname = "mesh.conway" + bl_label = "Conway's Selection" + bl_description = ("Select Faces with the Conway's game of life algorithm\n" + "Requires an initial Face selection\n" + "The edges of the original selection are included in the result") + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + global cachedata + + bpy.ops.object.mode_set(mode="OBJECT") + obj = context.active_object + mesh = obj.data + meshkey = (len(mesh.vertices), len(mesh.edges), len(mesh.polygons), id(self)) + + if (meshkey == obj.tkkey) and (meshkey in cachedata): + vert_to_face_map = cachedata[meshkey] + else: + vert_to_face_map = {i: set() for i in range(meshkey[0])} + for f in mesh.polygons: + for v in f.vertices: + vert_to_face_map[v].add(f.index) + obj.tkkey = meshkey + sel = set() + uns = set() + F = {i: set() for i in range(meshkey[2])} + + for f in range(meshkey[2]): + for v in mesh.polygons[f].vertices: + for n in filter(lambda _: mesh.polygons[_].select and (_ != f), vert_to_face_map[v]): + F[f].add(n) + + for f in F: + if len(F[f]) == 3: + sel.add(f) + elif len(F[f]) != 2: + uns.add(f) + + for f in range(meshkey[2]): + if f in sel: + mesh.polygons[f].select = True + if f in uns: + mesh.polygons[f].select = False + cachedata[meshkey] = vert_to_face_map + bpy.ops.object.mode_set(mode="EDIT") + + return {"FINISHED"} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_to_wall.py b/mesh_extra_tools/mesh_to_wall.py new file mode 100644 index 00000000..51d82f22 --- /dev/null +++ b/mesh_extra_tools/mesh_to_wall.py @@ -0,0 +1,252 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + + +bl_info = { + "name": "Mesh to wall", + "author": "luxuy_BlenderCN", + "version": (0.8), + "blender": (2, 71, 0), + "location": "View3D > EditMode > Mesh", + "description": "Make wall from single mesh lines.", + "url": "https://luxuy.github.io/BlenderAddons/Mesh-to-wall/Mesh_to_wall.html", + "category": "Mesh"} + +import bpy +import bmesh +from bpy.types import Operator +from bpy.props import FloatProperty +from math import sin, radians +from mathutils import Vector, Quaternion + + +def link_verts(bm, ver_index): + linked_verts = [] + bm.verts.ensure_lookup_table() + v = bm.verts[ver_index] + for e in v.link_edges: + linked_verts.append(e.verts[1].index) + linked_verts.append(e.verts[0].index) + + linked_verts = list(set(linked_verts) - set([ver_index])) + + return linked_verts + + +def qq_sort(bm, ver_index, vm, wm): + verts = link_verts(bm, ver_index) + pt0 = bm.verts[ver_index].co + + def ang_2d_sort(x): + pt1 = bm.verts[x].co + vec = vm * wm * pt1 - vm * wm * pt0 + vec = Vector(vec[0:2]) + ang = vec.angle_signed(Vector((1, 0))) + + if ang < 0: + return ang + 3.1415926 * 2 + else: + return ang + verts = sorted(verts, key=ang_2d_sort) + + return verts + + +def turn_left(bm, v1, v2, vm, wm): + links = [v1, v2] + + verts = qq_sort(bm, links[-1], vm, wm) + v = verts.index(links[-2]) + if v == 0: + v_nxt = verts[-1] + else: + v_nxt = verts[v - 1] + + links.append(v_nxt) + + while not(links[-1] == links[1] and links[-2] == links[0]): + + verts = qq_sort(bm, links[-1], vm, wm) + v = verts.index(links[-2]) + if v == 0: + v_nxt = verts[-1] + else: + v_nxt = verts[v - 1] + + links.append(v_nxt) + links.pop() + + return links + + +def lp_left(bm, lp, wid, vm, wm): + size = len(lp) + up = wm.inverted() * vm.inverted() * Vector((0, 0, 1)) + lp_off = [] + faces = [] + for i in range(size - 1): + if i == 0: + pt = bm.verts[lp[i]].co + pre = bm.verts[lp[-2]].co + nxt = bm.verts[lp[1]].co + pre_ind = lp[size - 2] + nxt_ind = lp[1] + else: + bm.verts.ensure_lookup_table() + pt = bm.verts[lp[i]].co + pre = bm.verts[lp[i - 1]].co + nxt = bm.verts[lp[i + 1]].co + pre_ind = lp[i - 1] + nxt_ind = lp[i + 1] + + vec1 = pt - pre + vec2 = pt - nxt + + mid = vec1.normalized() + vec2.normalized() + if mid.length < 10e-4: + + up2 = Vector((0, 0, 1)) + mid = up2.cross(vec1) + + else: + xx = mid.cross(vec1).dot(up) + + if xx > 0: + mid.negate() + + mid.normalize() + if pre_ind == nxt_ind: + mid = (pt - pre).normalized() + q_a = Quaternion((0.0, 0.0, 1.0), radians(90.0)) + q_b = Quaternion((0.0, 0.0, 1.0), radians(-180.0)) + mid.rotate(q_a) + pt1 = pt + mid * wid + mid.rotate(q_b) + pt2 = pt + mid * wid + new_vert_1 = bm.verts.new(pt1) + new_vert_2 = bm.verts.new(pt2) + lp_off.append([new_vert_1, new_vert_2]) + else: + ang = mid.angle(pre - pt) + + vec_len = wid / (sin(ang)) + pt = pt + mid * vec_len + new_vert = bm.verts.new(pt) + lp_off.append(new_vert) + lp_off.append(lp_off[0]) + bm.verts.index_update() + for i in range(len(lp_off) - 1): + bm.verts.ensure_lookup_table() + p1 = bm.verts[lp[i]] + p2 = bm.verts[lp[i + 1]] + p3 = lp_off[i + 1] + p4 = lp_off[i] + + if isinstance(p3, list): + + faces.append((p1, p2, p3[0], p4)) + # faces.append((p3[0],p2,p3[1])) + elif isinstance(p4, list): + + faces.append((p1, p2, p3, p4[1])) + else: + faces.append((p1, p2, p3, p4)) + + return faces + + +# Operators + +class MeshtoWall(Operator): + bl_idname = "bpt.mesh_to_wall" + bl_label = "Edge(s) to Wall" + bl_description = "Top View, Extrude Flat Along Edges" + bl_options = {'REGISTER', 'UNDO'} + + wid = FloatProperty( + name='Wall width:', + default=0.1, + min=0.001, max=10 + ) + + def check_vert(self, context): + obj = bpy.context.object + if len(obj.data.vertices) <= 1: + return False + return True + + def execute(self, context): + bpy.ops.object.mode_set(mode='OBJECT') + if self.check_vert(context): + bpy.ops.object.mode_set(mode='EDIT') + ob = bpy.context.object + bm = bmesh.from_edit_mesh(ob.data) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.003) + bmesh.ops.delete(bm, geom=bm.faces, context=3) + context.tool_settings.mesh_select_mode = (True, True, False) + + v3d = context.space_data + rv3d = v3d.region_3d + vm = rv3d.view_matrix + wm = ob.matrix_world + faces = [] + sel = [] + for v in bm.verts: + sel.append(v.index) + bpy.ops.mesh.select_all(action='DESELECT') + + for j in sel: + verts = link_verts(bm, j) + + if len(verts) > 1: + for i in verts: + lp = turn_left(bm, j, i, vm, wm) + + bpy.ops.mesh.select_all(action='DESELECT') + + faces += lp_left(bm, lp, self.wid * 0.5, vm, wm) + lp = [bm.verts[i] for i in lp] + + lp = lp[1:] + + bpy.ops.mesh.select_all(action='DESELECT') + for f in faces: + try: + bm.faces.new(f) + except: + pass + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.003) + bm = bmesh.update_edit_mesh(ob.data, 1, 1) + else: + bpy.ops.object.mode_set(mode='EDIT') + self.report({'WARNING'}, "None or a single vertex found, cancelling the operation") + return {'CANCELLED'} + + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/mesh_vertex_chamfer.py b/mesh_extra_tools/mesh_vertex_chamfer.py new file mode 100644 index 00000000..ba044663 --- /dev/null +++ b/mesh_extra_tools/mesh_vertex_chamfer.py @@ -0,0 +1,160 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +bl_info = { + "name": "Vertex Chamfer", + "author": "Andrew Hale (TrumanBlending)", + "version": (0, 1), + "blender": (2, 63, 0), + "location": "Spacebar Menu", + "description": "Chamfer vertex", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + + +import bpy +import bmesh +from bpy.types import Operator +from bpy.props import ( + BoolProperty, + FloatProperty, + ) + + +class VertexChamfer(Operator): + bl_idname = "mesh.vertex_chamfer" + bl_label = "Chamfer Vertex" + bl_description = "Tri chamfer selected vertices" + bl_options = {'REGISTER', 'UNDO'} + + factor = FloatProperty( + name="Factor", + description="Size of the Champfer", + default=0.1, + min=0.0, + soft_max=1.0 + ) + relative = BoolProperty( + name="Relative", + description="If Relative, Champfer size is relative to the edge lenght", + default=True + ) + dissolve = BoolProperty( + name="Remove", + description="Remove/keep the original selected vertices\n" + "Remove creates a new triangle face between the Champfer edges,\n" + "similar to the Dissolve Vertices operator", + default=True + ) + displace = FloatProperty( + name="Displace", + description="Active only if Remove option is disabled\n" + "Displaces the original selected vertices along the normals\n" + "defined by the Champfer edges", + soft_min=-5.0, + soft_max=5.0 + ) + + @classmethod + def poll(self, context): + return (context.active_object.type == 'MESH' and + context.mode == 'EDIT_MESH') + + def draw(self, context): + layout = self.layout + layout.prop(self, "factor", text="Distance" if self.relative else "Factor") + sub = layout.row() + sub.prop(self, "relative") + sub.prop(self, "dissolve") + if not self.dissolve: + layout.prop(self, "displace") + + def execute(self, context): + ob = context.active_object + me = ob.data + bm = bmesh.from_edit_mesh(me) + + bm.select_flush(True) + + fac = self.factor + rel = self.relative + dissolve = self.dissolve + displace = self.displace + + for v in bm.verts: + v.tag = False + + # Loop over edges to find those with both verts selected + for e in bm.edges[:]: + e.tag = e.select + if not e.select: + continue + elen = e.calc_length() + val = fac if rel else fac / elen + val = min(val, 0.5) + # Loop over the verts of the edge to split + for v in e.verts: + # if val == 0.5 and e.other_vert(v).tag: + # continue + en, vn = bmesh.utils.edge_split(e, v, val) + en.tag = vn.tag = True + val = 1.0 if val == 1.0 else val / (1.0 - val) + + # Get all verts which are selected but not created previously + verts = [v for v in bm.verts if v.select and not v.tag] + + # Loop over all verts to split their linked edges + for v in verts: + for e in v.link_edges[:]: + if e.tag: + continue + elen = e.calc_length() + val = fac if rel else fac / elen + bmesh.utils.edge_split(e, v, val) + + # Loop over all the loops of the vert + for l in v.link_loops: + # Split the face + bmesh.utils.face_split(l.face, + l.link_loop_next.vert, + l.link_loop_prev.vert) + + # Remove the vert or displace otherwise + if dissolve: + bmesh.utils.vert_dissolve(v) + else: + v.co += displace * v.normal + + me.calc_tessface() + + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/pkhg_faces.py b/mesh_extra_tools/pkhg_faces.py new file mode 100644 index 00000000..10b0ca97 --- /dev/null +++ b/mesh_extra_tools/pkhg_faces.py @@ -0,0 +1,824 @@ +# gpl author: PHKG + +bl_info = { + "name": "PKHG faces", + "author": "PKHG", + "version": (0, 0, 5), + "blender": (2, 7, 1), + "location": "View3D > Tools > PKHG (tab)", + "description": "Faces selected will become added faces of different style", + "warning": "", + "wiki_url": "", + "category": "Mesh", +} + +import bpy +import bmesh +from bpy.types import ( + Operator, + Panel + ) +from mathutils import Vector +from bpy.props import ( + BoolProperty, + StringProperty, + IntProperty, + FloatProperty, + EnumProperty, + ) + + +class MESH_OT_add_faces_to_object(Operator): + bl_idname = "mesh.add_faces_to_object" + bl_label = "Face Extrude" + bl_description = "Set parameters and build object with added faces" + bl_options = {'REGISTER', 'UNDO', 'PRESET'} + + reverse_faces = BoolProperty( + name="Reverse Faces", + default=False, + description="Revert the normals of selected faces" + ) + name_source_object = StringProperty( + name="Mesh", + description="Choose a Source Mesh", + default="Cube" + ) + remove_start_faces = BoolProperty( + name="Remove Start Faces", + default=True, + description="Make a choice about removal of Original Faces" + ) + base_height = FloatProperty( + name="Base Height", + min=-20, + soft_max=10, max=20, + default=0.2, + description="Set general Base Height" + ) + use_relative_base_height = BoolProperty( + name="rel.base_height", + default=False, + description="Relative or absolute Base Height" + ) + second_height = FloatProperty( + name="2nd height", min=-5, + soft_max=5, max=20, + default=0.2, + description="Second height for various shapes" + ) + width = FloatProperty( + name="Width Faces", + min=-20, max=20, + default=0.5, + description="Set general width" + ) + repeat_extrude = IntProperty( + name="Repeat", + min=1, + soft_max=5, max=20, + description="For longer base" + ) + move_inside = FloatProperty( + name="Move Inside", + min=0.0, + max=1.0, + default=0.5, + description="How much move to inside" + ) + thickness = FloatProperty( + name="Thickness", + soft_min=0.01, min=0, + soft_max=5.0, max=20.0, + default=0 + ) + depth = FloatProperty( + name="Depth", + min=-5, + soft_max=5.0, max=20.0, + default=0 + ) + collapse_edges = BoolProperty( + name="Make Point", + default=False, + description="Collapse the vertices of edges" + ) + spike_base_width = FloatProperty( + name="Spike Base Width", + default=0.4, + min=-4.0, + soft_max=1, max=20, + description="Base width of a spike" + ) + base_height_inset = FloatProperty( + name="Base Height Inset", + default=0.0, + min=-5, max=5, + description="To elevate or drop the Base height Inset" + ) + top_spike = FloatProperty( + name="Top Spike", + default=1.0, + min=-10.0, max=10.0, + description="The Base Height of a spike" + ) + + top_extra_height = FloatProperty( + name="Top Extra Height", + default=0.0, + min=-10.0, max=10.0, + description="Add extra height" + ) + step_with_real_spike = BoolProperty( + name="Step with Real Spike", + default=False, + description="In stepped, use a real spike" + ) + use_relative = BoolProperty( + name="Use Relative", + default=False, + description="Change size using area, min or max" + ) + face_types = EnumProperty( + name="Face Types", + description="Different types of Faces", + default="no", + items=[ + ('no', "Pick an Option", "Choose one of the available options"), + ('open_inset', "Open Inset", "Inset without closing faces (holes)"), + ('with_base', "With Base", "Base and ..."), + ('clsd_vertical', "Closed Vertical", "Closed Vertical"), + ('open_vertical', "Open Vertical", "Open Vertical"), + ('spiked', "Spiked", "Spike"), + ('stepped', "Stepped", "Stepped"), + ('boxed', "Boxed", "Boxed"), + ('bar', "Bar", "Bar"), + ] + ) + strange_boxed_effect = BoolProperty( + name="Strange Effect", + default=False, + description="Do not show one extrusion" + ) + use_boundary = BoolProperty( + name="Use Boundary", + default=True + ) + use_even_offset = BoolProperty( + name="Even Offset", + default=True + ) + use_relative_offset = BoolProperty( + name="Relative Offset", + default=True + ) + use_edge_rail = BoolProperty( + name="Edge Rail", + default=False + ) + use_outset = BoolProperty( + name="Outset", + default=False + ) + use_select_inset = BoolProperty( + name="Inset", + default=False + ) + use_interpolate = BoolProperty( + name="Interpolate", + default=True + ) + + @classmethod + def poll(cls, context): + result = False + active_object = context.active_object + if active_object: + mesh_objects_name = [el.name for el in bpy.data.objects if el.type == "MESH"] + if active_object.name in mesh_objects_name: + result = True + + return result + + def draw(self, context): + layout = self.layout + col = layout.column() + + col.separator() + col.label(text="Using Active Object", icon="INFO") + col.separator() + col.label("Face Types:") + col.prop(self, "face_types", text="") + col.separator() + col.prop(self, "use_relative") + + if self.face_types == "open_inset": + col.prop(self, "move_inside") + col.prop(self, "base_height") + + elif self.face_types == "with_base": + col.prop(self, "move_inside") + col.prop(self, "base_height") + col.prop(self, "second_height") + col.prop(self, "width") + + elif self.face_types == "clsd_vertical": + col.prop(self, "base_height") + + elif self.face_types == "open_vertical": + col.prop(self, "base_height") + + elif self.face_types == "boxed": + col.prop(self, "move_inside") + col.prop(self, "base_height") + col.prop(self, "top_spike") + col.prop(self, "strange_boxed_effect") + + elif self.face_types == "spiked": + col.prop(self, "spike_base_width") + col.prop(self, "base_height_inset") + col.prop(self, "top_spike") + + elif self.face_types == "bar": + col.prop(self, "spike_base_width") + col.prop(self, "top_spike") + col.prop(self, "top_extra_height") + + elif self.face_types == "stepped": + col.prop(self, "spike_base_width") + col.prop(self, "base_height_inset") + col.prop(self, "top_extra_height") + col.prop(self, "second_height") + col.prop(self, "step_with_real_spike") + + def execute(self, context): + obj_name = self.name_source_object + face_type = self.face_types + + is_selected = check_is_selected() + + if not is_selected: + self.report({'WARNING'}, + "Operation Cancelled. No selected Faces found on the Active Object") + return {'CANCELLED'} + + if face_type == "spiked": + Spiked(spike_base_width=self.spike_base_width, + base_height_inset=self.base_height_inset, + top_spike=self.top_spike, top_relative=self.use_relative) + + elif face_type == "boxed": + startinfo = prepare(self, context, self.remove_start_faces) + bm = startinfo['bm'] + top = self.top_spike + obj = startinfo['obj'] + obj_matrix_local = obj.matrix_local + + distance = None + base_heights = None + t = self.move_inside + areas = startinfo['areas'] + base_height = self.base_height + + if self.use_relative: + distance = [min(t * area, 1.0) for i, area in enumerate(areas)] + base_heights = [base_height * area for i, area in enumerate(areas)] + else: + distance = [t] * len(areas) + base_heights = [base_height] * len(areas) + + rings = startinfo['rings'] + centers = startinfo['centers'] + normals = startinfo['normals'] + for i in range(len(rings)): + make_one_inset(self, context, bm=bm, ringvectors=rings[i], + center=centers[i], normal=normals[i], + t=distance[i], base_height=base_heights[i]) + bpy.ops.mesh.select_mode(type="EDGE") + bpy.ops.mesh.select_more() + bpy.ops.mesh.select_more() + bpy.ops.object.mode_set(mode='OBJECT') + # PKHG>INFO base extrusion done and set to the mesh + + # PKHG>INFO if the extrusion is NOT done ... it'll look strange soon! + if not self.strange_boxed_effect: + bpy.ops.object.mode_set(mode='EDIT') + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + bmfaces = [face for face in bm.faces if face.select] + res = extrude_faces(self, context, bm=bm, face_l=bmfaces) + ring_edges = [face.edges[:] for face in res] + + bpy.ops.object.mode_set(mode='OBJECT') + + # PKHG>INFO now the extruded facec have to move in normal direction + bpy.ops.object.mode_set(mode='EDIT') + obj = bpy.context.scene.objects.active + bm = bmesh.from_edit_mesh(obj.data) + todo_faces = [face for face in bm.faces if face.select] + for face in todo_faces: + bmesh.ops.translate(bm, vec=face.normal * top, space=obj_matrix_local, + verts=face.verts) + bpy.ops.object.mode_set(mode='OBJECT') + + elif face_type == "stepped": + Stepped(spike_base_width=self.spike_base_width, + base_height_inset=self.base_height_inset, + top_spike=self.second_height, + top_extra_height=self.top_extra_height, + use_relative_offset=self.use_relative, with_spike=self.step_with_real_spike) + + elif face_type == "open_inset": + startinfo = prepare(self, context, self.remove_start_faces) + bm = startinfo['bm'] + + # PKHG>INFO adjust for relative, via areas + t = self.move_inside + areas = startinfo['areas'] + base_height = self.base_height + base_heights = None + distance = None + if self.use_relative: + distance = [min(t * area, 1.0) for i, area in enumerate(areas)] + base_heights = [base_height * area for i, area in enumerate(areas)] + else: + distance = [t] * len(areas) + base_heights = [base_height] * len(areas) + + rings = startinfo['rings'] + centers = startinfo['centers'] + normals = startinfo['normals'] + for i in range(len(rings)): + make_one_inset(self, context, bm=bm, ringvectors=rings[i], + center=centers[i], normal=normals[i], + t=distance[i], base_height=base_heights[i]) + bpy.ops.object.mode_set(mode='OBJECT') + + elif face_type == "with_base": + startinfo = prepare(self, context, self.remove_start_faces) + bm = startinfo['bm'] + obj = startinfo['obj'] + object_matrix = obj.matrix_local + + # PKHG>INFO for relative (using areas) + t = self.move_inside + areas = startinfo['areas'] + base_height = self.base_height + distance = None + base_heights = None + + if self.use_relative: + distance = [min(t * area, 1.0) for i, area in enumerate(areas)] + base_heights = [base_height * area for i, area in enumerate(areas)] + else: + distance = [t] * len(areas) + base_heights = [base_height] * len(areas) + + next_rings = [] + rings = startinfo['rings'] + centers = startinfo['centers'] + normals = startinfo['normals'] + for i in range(len(rings)): + next_rings.append(make_one_inset(self, context, bm=bm, ringvectors=rings[i], + center=centers[i], normal=normals[i], + t=distance[i], base_height=base_heights[i])) + + prepare_ring = extrude_edges(self, context, bm=bm, edge_l_l=next_rings) + + second_height = self.second_height + width = self.width + vectors = [[ele.verts[:] for ele in edge] for edge in prepare_ring] + n_ring_vecs = [] + + for rings in vectors: + v = [] + for edgv in rings: + v.extend(edgv) + # PKHF>INFO no double verts allowed, coming from two adjacents edges! + bm.verts.ensure_lookup_table() + vv = list(set([ele.index for ele in v])) + + vvv = [bm.verts[i].co for i in vv] + n_ring_vecs.append(vvv) + + for i, ring in enumerate(n_ring_vecs): + make_one_inset(self, context, bm=bm, ringvectors=ring, + center=centers[i], normal=normals[i], + t=width, base_height=base_heights[i] + second_height) + bpy.ops.object.mode_set(mode='OBJECT') + + else: + if face_type == "clsd_vertical": + obj_name = context.active_object.name + ClosedVertical(name=obj_name, base_height=self.base_height, + use_relative_base_height=self.use_relative) + + elif face_type == "open_vertical": + obj_name = context.active_object.name + OpenVertical(name=obj_name, base_height=self.base_height, + use_relative_base_height=self.use_relative) + + elif face_type == "bar": + startinfo = prepare(self, context, self.remove_start_faces) + + result = [] + bm = startinfo['bm'] + rings = startinfo['rings'] + centers = startinfo['centers'] + normals = startinfo['normals'] + spike_base_width = self.spike_base_width + for i, ring in enumerate(rings): + result.append(make_one_inset(self, context, bm=bm, + ringvectors=ring, center=centers[i], + normal=normals[i], t=spike_base_width)) + + next_ring_edges_list = extrude_edges(self, context, bm=bm, + edge_l_l=result) + top_spike = self.top_spike + fac = top_spike + object_matrix = startinfo['obj'].matrix_local + for i in range(len(next_ring_edges_list)): + translate_ONE_ring(self, context, bm=bm, + object_matrix=object_matrix, + ring_edges=next_ring_edges_list[i], + normal=normals[i], distance=fac) + + next_ring_edges_list_2 = extrude_edges(self, context, bm=bm, + edge_l_l=next_ring_edges_list) + + top_extra_height = self.top_extra_height + for i in range(len(next_ring_edges_list_2)): + move_corner_vecs_outside(self, context, bm=bm, + edge_list=next_ring_edges_list_2[i], + center=centers[i], normal=normals[i], + base_height_erlier=fac + top_extra_height, + distance=fac) + bpy.ops.mesh.select_mode(type="VERT") + bpy.ops.mesh.select_more() + + bpy.ops.object.mode_set(mode='OBJECT') + + return {'FINISHED'} + + +class VIEW3D_Faces_Panel(Panel): + bl_label = "Face Extrude" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Tools" + bl_options = {"DEFAULT_CLOSED"} + + @classmethod + def poll(cls, context): + result = False + active_object = context.active_object + if active_object: + mesh_objects_name = [el.name for el in bpy.data.objects if el.type == "MESH"] + if active_object.name in mesh_objects_name: + if active_object.mode == "OBJECT": + result = True + return result + + def draw(self, context): + layout = self.layout + + row = layout.split(0.8, align=True) + row.operator("mesh.add_faces_to_object", "Selected Faces") + row.operator("mesh.extra_tools_help", + icon="LAYER_USED").help_ids = "pkhg_faces" + + +def find_one_ring(sel_vertices): + ring0 = sel_vertices.pop(0) + to_delete = [] + + for i, edge in enumerate(sel_vertices): + len_nu = len(ring0) + if len(ring0 - edge) < len_nu: + to_delete.append(i) + ring0 = ring0.union(edge) + + to_delete.reverse() + + for el in to_delete: + sel_vertices.pop(el) + + return (ring0, sel_vertices) + + +class Stepped: + def __init__(self, spike_base_width=0.5, base_height_inset=0.0, top_spike=0.2, + top_relative=False, top_extra_height=0, use_relative_offset=False, with_spike=False): + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=True, use_relative_offset=False, + use_edge_rail=False, thickness=spike_base_width, depth=0, use_outset=True, + use_select_inset=False, use_individual=True, use_interpolate=True) + + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=True, use_relative_offset=use_relative_offset, + use_edge_rail=False, thickness=top_extra_height, depth=base_height_inset, + use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True) + + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=True, use_relative_offset=use_relative_offset, + use_edge_rail=False, thickness=spike_base_width, depth=0, use_outset=True, + use_select_inset=False, use_individual=True, use_interpolate=True) + + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=True, use_relative_offset=False, + use_edge_rail=False, thickness=0, depth=top_spike, use_outset=True, + use_select_inset=False, use_individual=True, use_interpolate=True) + if with_spike: + bpy.ops.mesh.merge(type='COLLAPSE') + + bpy.ops.object.mode_set(mode='OBJECT') + + +class Spiked: + def __init__(self, spike_base_width=0.5, base_height_inset=0.0, top_spike=0.2, top_relative=False): + + obj = bpy.context.active_object + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=True, use_relative_offset=False, + use_edge_rail=False, thickness=spike_base_width, depth=base_height_inset, + use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True) + + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=True, use_relative_offset=top_relative, + use_edge_rail=False, thickness=0, depth=top_spike, use_outset=True, + use_select_inset=False, use_individual=True, use_interpolate=True) + + bm = bmesh.from_edit_mesh(obj.data) + bpy.ops.mesh.merge(type='COLLAPSE') + bpy.ops.object.mode_set(mode='OBJECT') + + +class ClosedVertical: + def __init__(self, name="Plane", base_height=1, use_relative_base_height=False): + obj = bpy.data.objects[name] + + bm = bmesh.new() + bm.from_mesh(obj.data) + # PKHG>INFO deselect chosen faces + sel = [f for f in bm.faces if f.select] + for f in sel: + f.select = False + res = bmesh.ops.extrude_discrete_faces(bm, faces=sel) + # PKHG>INFO select extruded faces + for f in res['faces']: + f.select = True + + factor = base_height + for face in res['faces']: + if use_relative_base_height: + area = face.calc_area() + factor = area * base_height + else: + factor = base_height + for el in face.verts: + tmp = el.co + face.normal * factor + el.co = tmp + + me = bpy.data.meshes[name] + bm.to_mesh(me) + bm.free() + + +class OpenVertical: + def __init__(self, name="Plane", base_height=1, use_relative_base_height=False): + + obj = bpy.data.objects[name] + bm = bmesh.new() + bm.from_mesh(obj.data) + # PKHG>INFO deselect chosen faces + sel = [f for f in bm.faces if f.select] + for f in sel: + f.select = False + res = bmesh.ops.extrude_discrete_faces(bm, faces=sel) + # PKHG>INFO select extruded faces + for f in res['faces']: + f.select = True + + # PKHG>INFO adjust extrusion by a vector + factor = base_height + for face in res['faces']: + if use_relative_base_height: + area = face.calc_area() + factor = area * base_height + else: + factor = base_height + for el in face.verts: + tmp = el.co + face.normal * factor + el.co = tmp + + me = bpy.data.meshes[name] + bm.to_mesh(me) + bm.free() + + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.delete(type='FACE') + bpy.ops.object.editmode_toggle() + + +class StripFaces: + def __init__(self, use_boundary=True, use_even_offset=True, use_relative_offset=False, + use_edge_rail=True, thickness=0.0, depth=0.0, use_outset=False, + use_select_inset=False, use_individual=True, use_interpolate=True): + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.inset(use_boundary=use_boundary, use_even_offset=True, use_relative_offset=False, + use_edge_rail=True, thickness=thickness, depth=depth, use_outset=use_outset, + use_select_inset=use_select_inset, use_individual=use_individual, + use_interpolate=use_interpolate) + + bpy.ops.object.mode_set(mode='OBJECT') + + # PKHG>IMFO only 3 parameters inc execution context supported!! + if False: + bpy.ops.mesh.inset(use_boundary, use_even_offset, use_relative_offset, use_edge_rail, + thickness, depth, use_outset, use_select_inset, use_individual, use_interpolate) + elif type == 0: + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=True, use_relative_offset=False, + use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False, + use_select_inset=False, use_individual=True, use_interpolate=True) + elif type == 1: + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=True, use_relative_offset=False, + use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False, + use_select_inset=False, use_individual=True, use_interpolate=False) + + bpy.ops.mesh.delete(type='FACE') + + elif type == 2: + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=False, use_relative_offset=True, + use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False, + use_select_inset=False, use_individual=True, use_interpolate=False) + + bpy.ops.mesh.delete(type='FACE') + + elif type == 3: + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=False, use_relative_offset=True, + use_edge_rail=True, thickness=depth, depth=thickness, use_outset=False, + use_select_inset=False, use_individual=True, use_interpolate=True) + + bpy.ops.mesh.delete(type='FACE') + elif type == 4: + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=False, use_relative_offset=True, + use_edge_rail=True, thickness=thickness, depth=depth, use_outset=True, + use_select_inset=False, use_individual=True, use_interpolate=True) + + bpy.ops.mesh.inset(use_boundary=True, use_even_offset=False, use_relative_offset=True, + use_edge_rail=True, thickness=thickness, depth=depth, use_outset=True, + use_select_inset=False, use_individual=True, use_interpolate=True) + + bpy.ops.mesh.delete(type='FACE') + + bpy.ops.object.mode_set(mode='OBJECT') + + +def check_is_selected(): + is_selected = False + for face in bpy.context.active_object.data.polygons: + if face.select: + is_selected = True + break + return is_selected + + +def prepare(self, context, remove_start_faces=True): + """ + Start for a face selected change of faces + select an object of type mesh, with activated several (all) faces + """ + obj = bpy.context.scene.objects.active + bpy.ops.object.mode_set(mode='OBJECT') + selectedpolygons = [el for el in obj.data.polygons if el.select] + + # PKHG>INFO copies of the vectors are needed, otherwise Blender crashes! + centers = [face.center for face in selectedpolygons] + centers_copy = [Vector((el[0], el[1], el[2])) for el in centers] + normals = [face.normal for face in selectedpolygons] + normals_copy = [Vector((el[0], el[1], el[2])) for el in normals] + vertindicesofpolgons = [[vert for vert in face.vertices] for face in selectedpolygons] + vertVectorsOfSelectedFaces = [[obj.data.vertices[ind].co for ind in vertIndiceofface] + for vertIndiceofface in vertindicesofpolgons] + vertVectorsOfSelectedFaces_copy = [[Vector((el[0], el[1], el[2])) for el in listofvecs] + for listofvecs in vertVectorsOfSelectedFaces] + + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + selected_bm_faces = [ele for ele in bm.faces if ele.select] + selected_edges_per_face_ind = [[ele.index for ele in face.edges] for face in selected_bm_faces] + indices = [el.index for el in selectedpolygons] + selected_faces_areas = [bm.faces[:][i] for i in indices] + tmp_area = [el.calc_area() for el in selected_faces_areas] + + # PKHG>INFO, selected faces are removed, only their edges are used! + if remove_start_faces: + bpy.ops.mesh.delete(type='ONLY_FACE') + bpy.ops.object.mode_set(mode='OBJECT') + obj.data.update() + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + bm.verts.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + start_ring_raw = [[bm.verts[ind].index for ind in vertIndiceofface] + for vertIndiceofface in vertindicesofpolgons] + start_ring = [] + + for el in start_ring_raw: + start_ring.append(set(el)) + bm.edges.ensure_lookup_table() + + bm_selected_edges_l_l = [[bm.edges[i] for i in bm_ind_list] for bm_ind_list in selected_edges_per_face_ind] + + result = {'obj': obj, 'centers': centers_copy, 'normals': normals_copy, + 'rings': vertVectorsOfSelectedFaces_copy, 'bm': bm, + 'areas': tmp_area, 'startBMRingVerts': start_ring, + 'base_edges': bm_selected_edges_l_l} + return result + + +def make_one_inset(self, context, bm=None, ringvectors=None, center=None, + normal=None, t=None, base_height=0): + # a face will get 'inserted' faces to create (normaly) a hole if t is > 0 and < 1) + tmp = [] + + for el in ringvectors: + tmp.append((el * (1 - t) + center * t) + normal * base_height) + + tmp = [bm.verts.new(v) for v in tmp] # the new corner bmvectors + # PKHG>INFO so to say sentinells, to use ONE for ... + tmp.append(tmp[0]) + vectorsFace_i = [bm.verts.new(v) for v in ringvectors] + vectorsFace_i.append(vectorsFace_i[0]) + myres = [] + for ii in range(len(vectorsFace_i) - 1): + # PKHG>INFO next line: sequence is important! for added edge + bmvecs = [vectorsFace_i[ii], vectorsFace_i[ii + 1], tmp[ii + 1], tmp[ii]] + res = bm.faces.new(bmvecs) + myres.append(res.edges[2]) + myres[-1].select = True # PKHG>INFO to be used later selected! + return (myres) + + +def extrude_faces(self, context, bm=None, face_l=None): + # to make a ring extrusion + res = bmesh.ops.extrude_discrete_faces(bm, faces=face_l)['faces'] + + for face in res: + face.select = True + return res + + +def extrude_edges(self, context, bm=None, edge_l_l=None): + # to make a ring extrusion + all_results = [] + for edge_l in edge_l_l: + for edge in edge_l: + edge.select = False + res = bmesh.ops.extrude_edge_only(bm, edges=edge_l) + tmp = [ele for ele in res['geom'] if isinstance(ele, bmesh.types.BMEdge)] + for edge in tmp: + edge.select = True + all_results.append(tmp) + return all_results + + +def translate_ONE_ring(self, context, bm=None, object_matrix=None, ring_edges=None, + normal=(0, 0, 1), distance=0.5): + # translate a ring in given (normal?!) direction with given (global) amount + tmp = [] + for edge in ring_edges: + tmp.extend(edge.verts[:]) + # PKHG>INFO no double vertices allowed by bmesh! + tmp = set(tmp) + tmp = list(tmp) + bmesh.ops.translate(bm, vec=normal * distance, space=object_matrix, verts=tmp) + # PKHG>INFO relevant edges will stay selected + return ring_edges + + +def move_corner_vecs_outside(self, context, bm=None, edge_list=None, center=None, normal=None, + base_height_erlier=0.5, distance=0.5): + # move corners (outside meant mostly) dependent on the parameters + tmp = [] + for edge in edge_list: + tmp.extend([ele for ele in edge.verts if isinstance(ele, bmesh.types.BMVert)]) + # PKHG>INFO to remove vertices, they are all used twice in the ring! + tmp = set(tmp) + tmp = list(tmp) + + for i in range(len(tmp)): + vec = tmp[i].co + direction = vec + (vec - (normal * base_height_erlier + center)) * distance + tmp[i].co = direction + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/random_vertices.py b/mesh_extra_tools/random_vertices.py new file mode 100644 index 00000000..346f7abb --- /dev/null +++ b/mesh_extra_tools/random_vertices.py @@ -0,0 +1,139 @@ +# gpl authors: Oscurart, Greg + +bl_info = { + "name": "Random Vertices", + "author": "Oscurart, Greg", + "version": (1, 3), + "blender": (2, 6, 3), + "location": "Object > Transform > Random Vertices", + "description": "Randomize selected components of active object.", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + + +import bpy +from bpy.types import Operator +import random +import bmesh +from bpy.props import ( + BoolProperty, + FloatProperty, + IntVectorProperty, + ) + + +def add_object(self, context, valmin, valmax, factor, vgfilter): + # select an option with weight map or not + mode = bpy.context.active_object.mode + # generate variables + objact = bpy.context.active_object + listver = [] + warn_message = False + + # switch to edit mode + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + + # bmesh object + odata = bmesh.from_edit_mesh(objact.data) + odata.select_flush(False) + + # if the vertex is selected add to the list + for vertice in odata.verts[:]: + if vertice.select: + listver.append(vertice.index) + + # If the minimum value is greater than the maximum, it adds a value to the maximum + if valmin[0] >= valmax[0]: + valmax[0] = valmin[0] + 1 + + if valmin[1] >= valmax[1]: + valmax[1] = valmin[1] + 1 + + if valmin[2] >= valmax[2]: + valmax[2] = valmin[2] + 1 + + odata.verts.ensure_lookup_table() + + random_factor = factor + for vertice in listver: + odata.verts.ensure_lookup_table() + if odata.verts[vertice].select: + if vgfilter is True: + has_group = getattr(objact.data.vertices[vertice], "groups", None) + vertex_group = has_group[0] if has_group else None + vertexweight = getattr(vertex_group, "weight", None) + if vertexweight: + random_factor = factor * vertexweight + else: + random_factor = factor + warn_message = True + + odata.verts[vertice].co = ( + (((random.randrange(valmin[0], valmax[0], 1)) * random_factor) / 1000) + + odata.verts[vertice].co[0], + (((random.randrange(valmin[1], valmax[1], 1)) * random_factor) / 1000) + + odata.verts[vertice].co[1], + (((random.randrange(valmin[2], valmax[2], 1)) * random_factor) / 1000) + + odata.verts[vertice].co[2] + ) + + if warn_message: + self.report({'WARNING'}, + "Some of the Selected Vertices don't have a Group with Vertex Weight assigned") + bpy.ops.object.mode_set(mode=mode) + + +class MESH_OT_random_vertices(Operator): + bl_idname = "mesh.random_vertices" + bl_label = "Random Vertices" + bl_description = ("Randomize the location of vertices by a specified\n" + "Multiplier Factor and random values in the defined range\n" + "or a multiplication of them and the Vertex Weights") + bl_options = {'REGISTER', 'UNDO'} + + vgfilter = BoolProperty( + name="Vertex Group", + description="Use Vertex Weight defined in the Active Group", + default=False + ) + factor = FloatProperty( + name="Factor", + description="Base Multiplier of the randomization effect", + default=1 + ) + valmin = IntVectorProperty( + name="Min XYZ", + description="Define the minimum range of randomization values", + default=(0, 0, 0) + ) + valmax = IntVectorProperty( + name="Max XYZ", + description="Define the maximum range of randomization values", + default=(1, 1, 1) + ) + + @classmethod + def poll(cls, context): + return (context.object and context.object.type == "MESH" and + context.mode == "EDIT_MESH") + + def execute(self, context): + add_object(self, context, self.valmin, self.valmax, self.factor, self.vgfilter) + return {'FINISHED'} + + +# Registration + +def register(): + bpy.utils.register_class(MESH_OT_random_vertices) + + +def unregister(): + bpy.utils.unregister_class(MESH_OT_random_vertices) + + +if __name__ == '__main__': + register() diff --git a/mesh_extra_tools/split_solidify.py b/mesh_extra_tools/split_solidify.py new file mode 100644 index 00000000..147e096d --- /dev/null +++ b/mesh_extra_tools/split_solidify.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +bl_info = { + "name": "Split Solidify", + "author": "zmj100, updated by zeffii to BMesh", + "version": (0, 1, 2), + "blender": (2, 7, 7), + "location": "View3D > Tool Shelf", + "description": "", + "warning": "", + "wiki_url": "", + "tracker_url": "", + "category": "Mesh"} + +import bpy +import bmesh +from bpy.types import Operator +from bpy.props import ( + EnumProperty, + FloatProperty, + BoolProperty, + ) +import random +from math import cos + + +# define the functions +def solidify_split(self, list_0): + + loc_random = self.loc_random + random_dist = self.random_dist + distance = self.distance + thickness = self.thickness + normal_extr = self.normal_extr + + bm = self.bm + + for fi in list_0: + bm.faces.ensure_lookup_table() + f = bm.faces[fi] + list_1 = [] + list_2 = [] + + if loc_random: + d = random_dist * random.randrange(0, 10) + elif not loc_random: + d = distance + + # add new vertices + for vi in f.verts: + bm.verts.ensure_lookup_table() + v = bm.verts[vi.index] + + if normal_extr == 'opt0': + p1 = (v.co).copy() + ((f.normal).copy() * d) # out + p2 = (v.co).copy() + ((f.normal).copy() * (d - thickness)) # in + elif normal_extr == 'opt1': + ang = ((v.normal).copy()).angle((f.normal).copy()) + h = thickness / cos(ang) + p1 = (v.co).copy() + ((f.normal).copy() * d) + p2 = p1 + (-h * (v.normal).copy()) + + v1 = bm.verts.new(p1) + v2 = bm.verts.new(p2) + v1.select = False + v2.select = False + list_1.append(v1) + list_2.append(v2) + + # add new faces, allows faces with more than 4 verts + n = len(list_1) + + k = bm.faces.new(list_1) + k.select = False + for i in range(n): + j = (i + 1) % n + vseq = list_1[i], list_2[i], list_2[j], list_1[j] + k = bm.faces.new(vseq) + k.select = False + + list_2.reverse() + k = bm.faces.new(list_2) + k.select = False + bpy.ops.mesh.normals_make_consistent(inside=False) + + bmesh.update_edit_mesh(self.me, True) + + +class MESH_OT_split_solidify(Operator): + bl_idname = "mesh.split_solidify" + bl_label = "Split Solidify" + bl_description = "Split and Solidify selected Faces" + bl_options = {"REGISTER", "UNDO"} + + distance = FloatProperty( + name="", + description="Distance of the splitted Faces to the original geometry", + default=0.4, + min=-100.0, max=100.0, + step=1, + precision=3 + ) + thickness = FloatProperty( + name="", + description="Thickness of the splitted Faces", + default=0.04, + min=-100.0, max=100.0, + step=1, + precision=3 + ) + random_dist = FloatProperty( + name="", + description="Randomization factor of the splitted Faces' location", + default=0.06, + min=-10.0, max=10.0, + step=1, + precision=3 + ) + loc_random = BoolProperty( + name="Random", + description="Randomize the locations of splitted faces", + default=False + ) + del_original = BoolProperty( + name="Delete original faces", + default=True + ) + normal_extr = EnumProperty( + items=(('opt0', "Face", "Solidify along Face Normals"), + ('opt1', "Vertex", "Solidify along Vertex Normals")), + name="Normal", + default='opt0' + ) + + def draw(self, context): + layout = self.layout + layout.label("Normal:") + layout.prop(self, "normal_extr", expand=True) + layout.prop(self, "loc_random") + + if not self.loc_random: + layout.label("Distance:") + layout.prop(self, "distance") + elif self.loc_random: + layout.label("Random distance:") + layout.prop(self, "random_dist") + + layout.label("Thickness:") + layout.prop(self, "thickness") + layout.prop(self, "del_original") + + def execute(self, context): + obj = bpy.context.active_object + self.me = obj.data + self.bm = bmesh.from_edit_mesh(self.me) + self.me.update() + + list_0 = [f.index for f in self.bm.faces if f.select] + + if len(list_0) == 0: + self.report({'WARNING'}, "No suitable selection found. Operation cancelled") + + return {'CANCELLED'} + + elif len(list_0) != 0: + solidify_split(self, list_0) + context.tool_settings.mesh_select_mode = (True, True, True) + if self.del_original: + bpy.ops.mesh.delete(type='FACE') + else: + pass + + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(MESH_OT_split_solidify) + + +def unregister(): + bpy.utils.unregister_class(MESH_OT_split_solidify) + + +if __name__ == "__main__": + register() diff --git a/mesh_extra_tools/vfe_specials.py b/mesh_extra_tools/vfe_specials.py new file mode 100644 index 00000000..4a3e1518 --- /dev/null +++ b/mesh_extra_tools/vfe_specials.py @@ -0,0 +1,94 @@ +# gpl author: Stanislav Blinov + +bl_info = { + "name": "V/E/F Context Menu", + "author": "Stanislav Blinov", + "version": (1, 0, 0), + "blender": (2, 74, 0), + "description": "Vert Edge Face Double Right Click Edit Mode", + "category": "Mesh", +} + +import bpy +from bpy.types import ( + Menu, + Operator, + ) + + +class MESH_MT_CombinedMenu(Menu): + bl_idname = "mesh.addon_combined_component_menu" + bl_label = "Components" + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def draw(self, context): + layout = self.layout + + mode = context.tool_settings.mesh_select_mode + if mode[0]: + layout.menu("VIEW3D_MT_edit_mesh_vertices") + if mode[1]: + layout.menu("VIEW3D_MT_edit_mesh_edges") + if mode[2]: + layout.menu("VIEW3D_MT_edit_mesh_faces") + + +class MESH_OT_CallContextMenu(Operator): + bl_idname = "mesh.addon_call_context_menu" + bl_label = "Context Menu" + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def execute(self, context): + mode = context.tool_settings.mesh_select_mode + num = sum(int(m) for m in mode) + if num == 1: + if mode[0]: + return bpy.ops.wm.call_menu(name="VIEW3D_MT_edit_mesh_vertices") + if mode[1]: + return bpy.ops.wm.call_menu(name="VIEW3D_MT_edit_mesh_edges") + if mode[2]: + return bpy.ops.wm.call_menu(name="VIEW3D_MT_edit_mesh_faces") + else: + return bpy.ops.wm.call_menu(name=MESH_MT_CombinedMenu.bl_idname) + + +classes = [ + MESH_MT_CombinedMenu, + MESH_OT_CallContextMenu +] + +addon_keymaps = [] + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + wm = bpy.context.window_manager + km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new('mesh.addon_call_context_menu', 'RIGHTMOUSE', 'DOUBLE_CLICK') + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) + + wm = bpy.context.window_manager + + # remove multiselect keybinding + km = wm.keyconfigs.addon.keymaps['3D View'] + for kmi in km.keymap_items: + if kmi.idname == 'wm.call_menu': + if kmi.properties.name == "mesh.addon_call_context_menu": + km.keymap_items.remove(kmi) + break + + +if __name__ == "__main__": + register() |