Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormeta-androcto <meta.androcto1@gmail.com>2017-03-20 02:33:38 +0300
committermeta-androcto <meta.androcto1@gmail.com>2017-03-20 02:33:38 +0300
commit8e3bfa5506ea110fe6793401b53da17c61061167 (patch)
tree8e53f830a72b434928b07b99befe8762431a4ed3
parent9007bcd10713e55168235e9e8420b17172674638 (diff)
initial commit mesh edit tools: T50680
-rw-r--r--mesh_extra_tools/__init__.py762
-rw-r--r--mesh_extra_tools/face_inset_fillet.py323
-rw-r--r--mesh_extra_tools/mesh_cut_faces.py265
-rw-r--r--mesh_extra_tools/mesh_edge_roundifier.py1315
-rw-r--r--mesh_extra_tools/mesh_edges_length.py330
-rw-r--r--mesh_extra_tools/mesh_edgetools.py1829
-rw-r--r--mesh_extra_tools/mesh_extrude_and_reshape.py365
-rw-r--r--mesh_extra_tools/mesh_fastloop.py113
-rw-r--r--mesh_extra_tools/mesh_filletplus.py411
-rw-r--r--mesh_extra_tools/mesh_help.py215
-rw-r--r--mesh_extra_tools/mesh_mextrude_plus.py239
-rw-r--r--mesh_extra_tools/mesh_offset_edges.py823
-rw-r--r--mesh_extra_tools/mesh_pen_tool.py553
-rw-r--r--mesh_extra_tools/mesh_select_tools/__init__.py68
-rw-r--r--mesh_extra_tools/mesh_select_tools/mesh_index_select.py169
-rw-r--r--mesh_extra_tools/mesh_select_tools/mesh_info_select.py108
-rw-r--r--mesh_extra_tools/mesh_select_tools/mesh_select_by_direction.py220
-rw-r--r--mesh_extra_tools/mesh_select_tools/mesh_select_by_edge_length.py248
-rw-r--r--mesh_extra_tools/mesh_select_tools/mesh_select_by_pi.py210
-rw-r--r--mesh_extra_tools/mesh_select_tools/mesh_select_by_type.py78
-rw-r--r--mesh_extra_tools/mesh_select_tools/mesh_select_connected_faces.py146
-rw-r--r--mesh_extra_tools/mesh_select_tools/mesh_selection_topokit.py628
-rw-r--r--mesh_extra_tools/mesh_to_wall.py252
-rw-r--r--mesh_extra_tools/mesh_vertex_chamfer.py160
-rw-r--r--mesh_extra_tools/pkhg_faces.py824
-rw-r--r--mesh_extra_tools/random_vertices.py139
-rw-r--r--mesh_extra_tools/split_solidify.py203
-rw-r--r--mesh_extra_tools/vfe_specials.py94
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()