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>2019-06-15 07:06:26 +0300
committermeta-androcto <meta.androcto1@gmail.com>2019-06-15 07:06:26 +0300
commit682d48cefcc35a6784d23c59b5d3333ffb1415c4 (patch)
tree0c791215b66b7a45484b995d7563a6af759a8bb0 /mesh_tools
parentc7b5ffcfd924a358b37aa16b2bb576b6244fe1d3 (diff)
mesh_tools: restore to release: T63750 9e99e90f08c9
Diffstat (limited to 'mesh_tools')
-rw-r--r--mesh_tools/__init__.py1165
-rw-r--r--mesh_tools/face_inset_fillet.py335
-rw-r--r--mesh_tools/mesh_cut_faces.py241
-rw-r--r--mesh_tools/mesh_edge_roundifier.py1397
-rw-r--r--mesh_tools/mesh_edges_floor_plan.py384
-rw-r--r--mesh_tools/mesh_edges_length.py341
-rw-r--r--mesh_tools/mesh_edgetools.py1880
-rw-r--r--mesh_tools/mesh_extrude_and_reshape.py377
-rw-r--r--mesh_tools/mesh_filletplus.py430
-rw-r--r--mesh_tools/mesh_mextrude_plus.py370
-rw-r--r--mesh_tools/mesh_offset_edges.py791
-rw-r--r--mesh_tools/mesh_vertex_chamfer.py164
-rw-r--r--mesh_tools/pkhg_faces.py842
-rw-r--r--mesh_tools/random_vertices.py140
-rw-r--r--mesh_tools/split_solidify.py203
-rw-r--r--mesh_tools/vertex_align.py301
16 files changed, 9361 insertions, 0 deletions
diff --git a/mesh_tools/__init__.py b/mesh_tools/__init__.py
new file mode 100644
index 00000000..d5b34e42
--- /dev/null
+++ b/mesh_tools/__init__.py
@@ -0,0 +1,1165 @@
+# ##### 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, Gert De Roost, TrumanBlending, PKHG, #
+# Oscurart, Greg, Stanislav Blinov, komi3D, BlenderLab, Paul Marshall (brikbot), #
+# metalliandy, macouno, CoDEmanX, dustractor, Liero, lijenstina, Germano Cavalcante #
+# Pistiwique, Jimmy Hazevoet #
+
+bl_info = {
+ "name": "Edit Mesh Tools",
+ "author": "Meta-Androcto",
+ "version": (0, 3, 6),
+ "blender": (2, 80, 0),
+ "location": "View3D > Toolbar and View3D > Context Menu",
+ "warning": "",
+ "description": "Mesh modelling toolkit. Several tools to aid modelling",
+ "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/"
+ "Py/Scripts/Modeling/Extra_Tools",
+ "category": "Mesh",
+}
+
+# Import From Files
+if "bpy" in locals():
+ import importlib
+ importlib.reload(mesh_offset_edges)
+ importlib.reload(split_solidify)
+ importlib.reload(mesh_filletplus)
+ importlib.reload(mesh_vertex_chamfer)
+ importlib.reload(random_vertices)
+ importlib.reload(mesh_extrude_and_reshape)
+ importlib.reload(mesh_edge_roundifier)
+ importlib.reload(mesh_edgetools)
+ importlib.reload(mesh_edges_floor_plan)
+ importlib.reload(mesh_edges_length)
+ importlib.reload(pkhg_faces)
+ importlib.reload(mesh_cut_faces)
+
+else:
+ from . import mesh_offset_edges
+ from . import split_solidify
+ from . import mesh_filletplus
+ from . import mesh_vertex_chamfer
+ from . import random_vertices
+ from . import mesh_extrude_and_reshape
+ from . import mesh_edge_roundifier
+ from . import mesh_edgetools
+ from . import mesh_edges_floor_plan
+ from . import mesh_edges_length
+ from . import pkhg_faces
+ from . import mesh_cut_faces
+
+
+import bmesh
+import bpy
+import collections
+import mathutils
+import random
+from math import (
+ sin, cos, tan,
+ degrees, radians, pi,
+ )
+from random import gauss
+from mathutils import Matrix, Euler, Vector
+from bpy_extras import view3d_utils
+from bpy.types import (
+ Operator,
+ Menu,
+ Panel,
+ PropertyGroup,
+ AddonPreferences,
+ )
+from bpy.props import (
+ BoolProperty,
+ BoolVectorProperty,
+ EnumProperty,
+ FloatProperty,
+ FloatVectorProperty,
+ IntVectorProperty,
+ PointerProperty,
+ StringProperty,
+ IntProperty
+ )
+
+# ########################################
+# ##### General functions ################
+# ########################################
+
+
+# Multi extrude
+def gloc(self, r):
+ return Vector((self.offx, self.offy, self.offz))
+
+
+def vloc(self, r):
+ random.seed(self.ran + r)
+ return self.off * (1 + gauss(0, self.var1 / 3))
+
+
+def nrot(self, n):
+ return Euler((radians(self.nrotx) * n[0],
+ radians(self.nroty) * n[1],
+ radians(self.nrotz) * n[2]), 'XYZ')
+
+
+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 + gauss(0, self.var3 / 3))
+
+
+class ME_OT_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", "PRESET"}
+
+ off : FloatProperty(
+ name="Offset",
+ soft_min=0.001, soft_max=10,
+ min=-100, max=100,
+ default=1.0,
+ description="Translation"
+ )
+ offx : FloatProperty(
+ name="Loc X",
+ soft_min=-10.0, soft_max=10.0,
+ min=-100.0, max=100.0,
+ default=0.0,
+ description="Global Translation X"
+ )
+ offy : FloatProperty(
+ name="Loc Y",
+ soft_min=-10.0, soft_max=10.0,
+ min=-100.0, max=100.0,
+ default=0.0,
+ description="Global Translation Y"
+ )
+ offz : FloatProperty(
+ name="Loc Z",
+ soft_min=-10.0, soft_max=10.0,
+ min=-100.0, max=100.0,
+ default=0.0,
+ description="Global Translation Z"
+ )
+ 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"
+ )
+ nrotx : FloatProperty(
+ name="N Rot X",
+ min=-85, max=85,
+ soft_min=-30, soft_max=30,
+ default=0,
+ description="Normal X Rotation"
+ )
+ nroty : FloatProperty(
+ name="N Rot Y",
+ min=-85, max=85,
+ soft_min=-30, soft_max=30,
+ default=0,
+ description="Normal Y Rotation"
+ )
+ nrotz : FloatProperty(
+ name="N Rot Z",
+ min=-85, max=85,
+ soft_min=-30, soft_max=30,
+ default=-0,
+ description="Normal Z Rotation"
+ )
+ sca : FloatProperty(
+ name="Scale",
+ min=0.01, max=10,
+ soft_min=0.5, soft_max=1.5,
+ default=1.0,
+ description="Scaling of the selected faces after extrusion"
+ )
+ var1 : FloatProperty(
+ name="Offset Var", min=-10, max=10,
+ soft_min=-1, soft_max=1,
+ default=0,
+ description="Offset variation"
+ )
+ var2 : FloatProperty(
+ name="Rotation Var",
+ min=-10, max=10,
+ soft_min=-1, soft_max=1,
+ default=0,
+ description="Rotation variation"
+ )
+ var3 : FloatProperty(
+ name="Scale Noise",
+ min=-10, max=10,
+ soft_min=-1, soft_max=1,
+ default=0,
+ description="Scaling noise"
+ )
+ var4 : IntProperty(
+ name="Probability",
+ min=0, max=100,
+ default=100,
+ description="Probability, chance of extruding a face"
+ )
+ num : IntProperty(
+ name="Repeat",
+ min=1, max=500,
+ soft_max=100,
+ default=1,
+ description="Repetitions"
+ )
+ ran : IntProperty(
+ name="Seed",
+ min=-9999, max=9999,
+ default=0,
+ description="Seed to feed random values"
+ )
+ opt1 : BoolProperty(
+ name="Polygon coordinates",
+ default=True,
+ description="Polygon coordinates, Object coordinates"
+ )
+ opt2 : BoolProperty(
+ name="Proportional offset",
+ default=False,
+ description="Scale * Offset"
+ )
+ opt3 : BoolProperty(
+ name="Per step rotation noise",
+ default=False,
+ description="Per step rotation noise, Initial rotation noise"
+ )
+ opt4 : BoolProperty(
+ name="Per step scale noise",
+ default=False,
+ description="Per step scale noise, Initial scale noise"
+ )
+
+ @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, "offx", slider=True)
+ col.prop(self, "offy", slider=True)
+ col.prop(self, "offz", slider=True)
+
+ col = layout.column(align=True)
+ col.prop(self, "rotx", slider=True)
+ col.prop(self, "roty", slider=True)
+ col.prop(self, "rotz", slider=True)
+ col.prop(self, "nrotx", slider=True)
+ col.prop(self, "nroty", slider=True)
+ col.prop(self, "nrotz", slider=True)
+ col = layout.column(align=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, "var4", slider=True)
+ col.prop(self, "ran")
+ col = layout.column(align=False)
+ col.prop(self, 'num')
+
+ col = layout.column(align=True)
+ col.label(text="Options:")
+ col.prop(self, "opt1")
+ col.prop(self, "opt2")
+ col.prop(self, "opt3")
+ col.prop(self, "opt4")
+
+ def execute(self, context):
+ obj = bpy.context.object
+ om = obj.mode
+ bpy.context.tool_settings.mesh_select_mode = [False, False, True]
+ origin = Vector([0.0, 0.0, 0.0])
+
+ # 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):
+ nro = nrot(self, of.normal)
+ off = vloc(self, i)
+ loc = gloc(self, i)
+ of.normal_update()
+
+ # initial rotation noise
+ if self.opt3 is False:
+ rot = vrot(self, i)
+ # initial scale noise
+ if self.opt4 is False:
+ s = vsca(self, i)
+
+ # extrusion loop
+ for r in range(self.num):
+ # random probability % for extrusions
+ if self.var4 > int(random.random() * 100):
+ nf = of.copy()
+ nf.normal_update()
+ no = nf.normal.copy()
+
+ # face/obj coordinates
+ if self.opt1 is True:
+ ce = nf.calc_center_bounds()
+ else:
+ ce = origin
+
+ # per step rotation noise
+ if self.opt3 is True:
+ rot = vrot(self, i + r)
+ # per step scale noise
+ if self.opt4 is True:
+ s = vsca(self, i + r)
+
+ # proportional, scale * offset
+ if self.opt2 is True:
+ off = s * off
+
+ for v in nf.verts:
+ v.co -= ce
+ v.co.rotate(nro)
+ v.co.rotate(rot)
+ v.co += ce + loc + 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:
+ if f not in sel:
+ f.select = True
+ else:
+ f.select = False
+
+ 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'}
+
+# Face inset fillet
+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(False)
+ 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 \n"
+ "of the newly created Faces")
+ bl_options = {"REGISTER", "UNDO"}
+
+ # inset amount
+ inset_amount : bpy.props.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 : bpy.props.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 : bpy.props.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 : bpy.props.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 : bpy.props.BoolProperty(
+ name="Radius",
+ description="Use radius for corners' size calculation",
+ default=False
+ )
+ type_enum : bpy.props.EnumProperty(
+ items=[('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
+ ('opt1', "Triangle", "Triangulate corners")],
+ name="Corner Type",
+ default="opt0"
+ )
+ kp : bpy.props.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(text="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'}
+
+# ********** Edit Multiselect **********
+class VIEW3D_MT_Edit_MultiMET(Menu):
+ bl_label = "Multi Select"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator_context = 'INVOKE_REGION_WIN'
+
+ layout.operator("multiedit.allselect", text="All Select Modes", icon='RESTRICT_SELECT_OFF')
+
+
+# Select Tools
+class VIEW3D_MT_Select_Vert(Menu):
+ bl_label = "Select Vert"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator_context = 'INVOKE_REGION_WIN'
+
+ layout.operator("multiedit.vertexselect", text="Vertex Select Mode", icon='VERTEXSEL')
+ layout.operator("multiedit.vertedgeselect", text="Vert & Edge Select", icon='EDGESEL')
+ layout.operator("multiedit.vertfaceselect", text="Vert & Face Select", icon='FACESEL')
+
+
+class VIEW3D_MT_Select_Edge(Menu):
+ bl_label = "Select Edge"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator_context = 'INVOKE_REGION_WIN'
+
+ layout.operator("multiedit.edgeselect", text="Edge Select Mode", icon='EDGESEL')
+ layout.operator("multiedit.vertedgeselect", text="Edge & Vert Select", icon='VERTEXSEL')
+ layout.operator("multiedit.edgefaceselect", text="Edge & Face Select", icon='FACESEL')
+
+
+class VIEW3D_MT_Select_Face(Menu):
+ bl_label = "Select Face"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator_context = 'INVOKE_REGION_WIN'
+
+ layout.operator("multiedit.faceselect", text="Face Select Mode", icon='FACESEL')
+ layout.operator("multiedit.vertfaceselect", text="Face & Vert Select", icon='VERTEXSEL')
+ layout.operator("multiedit.edgefaceselect", text="Face & Edge Select", icon='EDGESEL')
+
+
+ # multiple edit select modes.
+class VIEW3D_OT_multieditvertex(Operator):
+ bl_idname = "multiedit.vertexselect"
+ bl_label = "Vertex Mode"
+ bl_description = "Vert Select Mode On"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ if context.object.mode != "EDIT":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
+ if bpy.ops.mesh.select_mode != "EDGE, FACE":
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
+ return {'FINISHED'}
+
+
+class VIEW3D_OT_multieditedge(Operator):
+ bl_idname = "multiedit.edgeselect"
+ bl_label = "Edge Mode"
+ bl_description = "Edge Select Mode On"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ if context.object.mode != "EDIT":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
+ if bpy.ops.mesh.select_mode != "VERT, FACE":
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
+ return {'FINISHED'}
+
+
+class VIEW3D_OT_multieditface(Operator):
+ bl_idname = "multiedit.faceselect"
+ bl_label = "Multiedit Face"
+ bl_description = "Face Select Mode On"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ if context.object.mode != "EDIT":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
+ if bpy.ops.mesh.select_mode != "VERT, EDGE":
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
+ return {'FINISHED'}
+
+class VIEW3D_OT_multieditvertedge(Operator):
+ bl_idname = "multiedit.vertedgeselect"
+ bl_label = "Multiedit Face"
+ bl_description = "Vert & Edge Select Modes On"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ if context.object.mode != "EDIT":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
+ if bpy.ops.mesh.select_mode != "VERT, EDGE, FACE":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
+ bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='EDGE')
+ return {'FINISHED'}
+
+class VIEW3D_OT_multieditvertface(Operator):
+ bl_idname = "multiedit.vertfaceselect"
+ bl_label = "Multiedit Face"
+ bl_description = "Vert & Face Select Modes On"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ if context.object.mode != "EDIT":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
+ if bpy.ops.mesh.select_mode != "VERT, EDGE, FACE":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
+ bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='FACE')
+ return {'FINISHED'}
+
+
+class VIEW3D_OT_multieditedgeface(Operator):
+ bl_idname = "multiedit.edgefaceselect"
+ bl_label = "Mode Face Edge"
+ bl_description = "Edge & Face Select Modes On"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ if context.object.mode != "EDIT":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
+ if bpy.ops.mesh.select_mode != "VERT, EDGE, FACE":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
+ bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='FACE')
+ return {'FINISHED'}
+
+
+class VIEW3D_OT_multieditall(Operator):
+ bl_idname = "multiedit.allselect"
+ bl_label = "All Edit Select Modes"
+ bl_description = "Vert & Edge & Face Select Modes On"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ if context.object.mode != "EDIT":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
+ if bpy.ops.mesh.select_mode != "VERT, EDGE, FACE":
+ bpy.ops.object.mode_set(mode="EDIT")
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
+ bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='EDGE')
+ bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='FACE')
+ return {'FINISHED'}
+
+
+# ########################################
+# ##### GUI and registration #############
+# ########################################
+
+# menu containing all tools
+class VIEW3D_MT_edit_mesh_tools(Menu):
+ bl_label = "Mesh Tools"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator("mesh.remove_doubles")
+ layout.operator("mesh.dissolve_limited")
+ layout.operator("mesh.flip_normals")
+ props = layout.operator("mesh.quads_convert_to_tris")
+ props.quad_method = props.ngon_method = 'BEAUTY'
+ layout.operator("mesh.tris_convert_to_quads")
+ layout.operator('mesh.vertex_chamfer', text="Vertex Chamfer")
+ layout.operator("mesh.bevel", text="Bevel Vertices").vertex_only = True
+ layout.operator('mesh.offset_edges', text="Offset Edges")
+ layout.operator('mesh.fillet_plus', text="Fillet Edges")
+ layout.operator("mesh.face_inset_fillet",
+ text="Face Inset Fillet")
+ layout.operator("mesh.extrude_reshape",
+ text="Push/Pull Faces")
+ layout.operator("object.mextrude",
+ text="Multi Extrude")
+ layout.operator('mesh.split_solidify', text="Split Solidify")
+
+
+
+# panel containing all tools
+class VIEW3D_PT_edit_mesh_tools(Panel):
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_category = 'Edit'
+ bl_context = "mesh_edit"
+ bl_label = "Mesh Tools"
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw(self, context):
+ layout = self.layout
+ col = layout.column(align=True)
+ et = context.window_manager.edittools
+
+ # vert - first line
+ split = col.split(factor=0.80, align=True)
+ if et.display_vert:
+ split.prop(et, "display_vert", text="Vert Tools", icon='DOWNARROW_HLT')
+ else:
+ split.prop(et, "display_vert", text="Vert tools", icon='RIGHTARROW')
+ split.menu("VIEW3D_MT_Select_Vert", text="", icon='VERTEXSEL')
+ # vert - settings
+ if et.display_vert:
+ box = col.column(align=True).box().column()
+ col_top = box.column(align=True)
+ row = col_top.row(align=True)
+ row.operator('mesh.vertex_chamfer', text="Vertex Chamfer")
+ row = col_top.row(align=True)
+ row.operator("mesh.extrude_vertices_move", text="Extrude Vertices")
+ row = col_top.row(align=True)
+ row.operator("mesh.random_vertices", text="Random Vertices")
+ row = col_top.row(align=True)
+ row.operator("mesh.bevel", text="Bevel Vertices").vertex_only = True
+
+ # edge - first line
+ split = col.split(factor=0.80, align=True)
+ if et.display_edge:
+ split.prop(et, "display_edge", text="Edge Tools", icon='DOWNARROW_HLT')
+ else:
+ split.prop(et, "display_edge", text="Edge Tools", icon='RIGHTARROW')
+ split.menu("VIEW3D_MT_Select_Edge", text="", icon='EDGESEL')
+ # Edge - settings
+ if et.display_edge:
+ box = col.column(align=True).box().column()
+ col_top = box.column(align=True)
+ row = col_top.row(align=True)
+ row.operator('mesh.offset_edges', text="Offset Edges")
+ row = col_top.row(align=True)
+ row.operator('mesh.fillet_plus', text="Fillet Edges")
+ row = col_top.row(align=True)
+ row.operator('mesh.edge_roundifier', text="Edge Roundify")
+ row = col_top.row(align=True)
+ row.operator('object.mesh_edge_length_set', text="Set Edge Length")
+ row = col_top.row(align=True)
+ row.operator('mesh.edges_floor_plan', text="Edges Floor Plan")
+ row = col_top.row(align=True)
+ row.operator("mesh.extrude_edges_move", text="Extrude Edges")
+ row = col_top.row(align=True)
+ row.operator("mesh.bevel", text="Bevel Edges").vertex_only = False
+
+ # face - first line
+ split = col.split(factor=0.80, align=True)
+ if et.display_face:
+ split.prop(et, "display_face", text="Face Tools", icon='DOWNARROW_HLT')
+ else:
+ split.prop(et, "display_face", text="Face Tools", icon='RIGHTARROW')
+ split.menu("VIEW3D_MT_Select_Face", text="", icon='FACESEL')
+ # face - settings
+ if et.display_face:
+ box = col.column(align=True).box().column()
+ col_top = box.column(align=True)
+ row = col_top.row(align=True)
+ row.operator("mesh.face_inset_fillet",
+ text="Face Inset Fillet")
+ row = col_top.row(align=True)
+ row.operator("mesh.ext_cut_faces",
+ text="Cut Faces")
+ row = col_top.row(align=True)
+ row.operator("mesh.extrude_reshape",
+ text="Push/Pull Faces")
+ row = col_top.row(align=True)
+ row.operator("object.mextrude",
+ text="Multi Extrude")
+ row = col_top.row(align=True)
+ row.operator('mesh.split_solidify', text="Split Solidify")
+ row = col_top.row(align=True)
+ row.operator('mesh.add_faces_to_object', text="Face Shape")
+ row = col_top.row(align=True)
+ row.operator("mesh.inset")
+ row = col_top.row(align=True)
+ row.operator("mesh.extrude_faces_move", text="Extrude Individual Faces")
+
+ # util - first line
+ split = col.split(factor=0.80, align=True)
+ if et.display_util:
+ split.prop(et, "display_util", text="Utility Tools", icon='DOWNARROW_HLT')
+ else:
+ split.prop(et, "display_util", text="Utility Tools", icon='RIGHTARROW')
+ split.menu("VIEW3D_MT_Edit_MultiMET", text="", icon='RESTRICT_SELECT_OFF')
+ # util - settings
+ if et.display_util:
+ box = col.column(align=True).box().column()
+ col_top = box.column(align=True)
+ row = col_top.row(align=True)
+ row.operator("mesh.subdivide")
+ row = col_top.row(align=True)
+ row.operator("mesh.remove_doubles")
+ row = col_top.row(align=True)
+ row.operator("mesh.dissolve_limited")
+ row = col_top.row(align=True)
+ row.operator("mesh.flip_normals")
+ row = col_top.row(align=True)
+ props = row.operator("mesh.quads_convert_to_tris")
+ props.quad_method = props.ngon_method = 'BEAUTY'
+ row = col_top.row(align=True)
+ row.operator("mesh.tris_convert_to_quads")
+
+# property group containing all properties for the gui in the panel
+class EditToolsProps(PropertyGroup):
+ """
+ Fake module like class
+ bpy.context.window_manager.edittools
+ """
+ # general display properties
+ display_vert: BoolProperty(
+ name="Bridge settings",
+ description="Display settings of the Vert tool",
+ default=False
+ )
+ display_edge: BoolProperty(
+ name="Edge settings",
+ description="Display settings of the Edge tool",
+ default=False
+ )
+ display_face: BoolProperty(
+ name="Face settings",
+ description="Display settings of the Face tool",
+ default=False
+ )
+ display_util: BoolProperty(
+ name="Face settings",
+ description="Display settings of the Face tool",
+ default=False
+ )
+
+# draw function for integration in menus
+def menu_func(self, context):
+ self.layout.menu("VIEW3D_MT_edit_mesh_tools")
+ self.layout.separator()
+
+# Add-ons Preferences Update Panel
+
+# Define Panel classes for updating
+panels = (
+ VIEW3D_PT_edit_mesh_tools,
+ )
+
+
+def update_panel(self, context):
+ message = "LoopTools: Updating Panel locations has failed"
+ try:
+ for panel in panels:
+ if "bl_rna" in panel.__dict__:
+ bpy.utils.unregister_class(panel)
+
+ for panel in panels:
+ panel.bl_category = context.preferences.addons[__name__].preferences.category
+ bpy.utils.register_class(panel)
+
+ except Exception as e:
+ print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
+ pass
+
+
+class EditToolsPreferences(AddonPreferences):
+ # this must match the addon name, use '__package__'
+ # when defining this in a submodule of a python package.
+ bl_idname = __name__
+
+ category: StringProperty(
+ name="Tab Category",
+ description="Choose a name for the category of the panel",
+ default="Edit",
+ update=update_panel
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ row = layout.row()
+ col = row.column()
+ col.label(text="Tab Category:")
+ col.prop(self, "category", text="")
+
+
+# define classes for registration
+classes = (
+ VIEW3D_MT_edit_mesh_tools,
+ VIEW3D_PT_edit_mesh_tools,
+ VIEW3D_MT_Edit_MultiMET,
+ VIEW3D_MT_Select_Vert,
+ VIEW3D_MT_Select_Edge,
+ VIEW3D_MT_Select_Face,
+ EditToolsProps,
+ EditToolsPreferences,
+ MESH_OT_face_inset_fillet,
+ ME_OT_MExtrude,
+ VIEW3D_OT_multieditvertex,
+ VIEW3D_OT_multieditedge,
+ VIEW3D_OT_multieditface,
+ VIEW3D_OT_multieditvertedge,
+ VIEW3D_OT_multieditvertface,
+ VIEW3D_OT_multieditedgeface,
+ VIEW3D_OT_multieditall
+ )
+
+
+# registering and menu integration
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+ bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
+ bpy.types.WindowManager.edittools = PointerProperty(type=EditToolsProps)
+ update_panel(None, bpy.context)
+
+ mesh_filletplus.register()
+ mesh_offset_edges.register()
+ split_solidify.register()
+ mesh_vertex_chamfer.register()
+ random_vertices.register()
+ mesh_extrude_and_reshape.register()
+ mesh_edge_roundifier.register()
+ mesh_edgetools.register()
+ mesh_edges_floor_plan.register()
+ mesh_edges_length.register()
+ pkhg_faces.register()
+ mesh_cut_faces.register()
+
+
+# unregistering and removing menus
+def unregister():
+ for cls in reversed(classes):
+ bpy.utils.unregister_class(cls)
+ bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
+ try:
+ del bpy.types.WindowManager.edittools
+ except Exception as e:
+ print('unregister fail:\n', e)
+ pass
+
+ mesh_filletplus.unregister()
+ mesh_offset_edges.unregister()
+ split_solidify.unregister()
+ mesh_vertex_chamfer.unregister()
+ random_vertices.unregister()
+ mesh_extrude_and_reshape.unregister()
+ mesh_edge_roundifier.unregister()
+ mesh_edgetools.unregister()
+ mesh_edges_floor_plan.unregister()
+ mesh_edges_length.unregister()
+ pkhg_faces.unregister()
+ mesh_cut_faces.unregister()
+
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/face_inset_fillet.py b/mesh_tools/face_inset_fillet.py
new file mode 100644
index 00000000..8af709c1
--- /dev/null
+++ b/mesh_tools/face_inset_fillet.py
@@ -0,0 +1,335 @@
+# -*- 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 LICENSE 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 (
+ sin, cos, tan,
+ degrees, radians,
+ )
+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(False)
+ 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 \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(text="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_tools/mesh_cut_faces.py b/mesh_tools/mesh_cut_faces.py
new file mode 100644
index 00000000..a5297c9f
--- /dev/null
+++ b/mesh_tools/mesh_cut_faces.py
@@ -0,0 +1,241 @@
+bl_info = {
+ "name" : "Cut Faces",
+ "author" : "Stanislav Blinov",
+ "version" : (1, 0, 0),
+ "blender" : (2, 80, 0),
+ "description" : "Cut Faces and Deselect Boundary operators",
+ "category" : "Mesh",}
+
+import bpy
+import bmesh
+
+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(bpy.types.Operator):
+ """Deselect boundary edges of selected faces"""
+ bl_idname = "mesh.ext_deselect_boundary"
+ bl_label = "Deselect Boundary"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ keep_cap_edges: bpy.props.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 selection found")
+ 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(bpy.types.Operator):
+ """Cut selected faces, connecting through their adjacent edges"""
+ bl_idname = "mesh.ext_cut_faces"
+ bl_label = "Cut Faces"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ # from bmesh_operators.h
+ INNERVERT = 0
+ PATH = 1
+ FAN = 2
+ STRAIGHT_CUT = 3
+
+ num_cuts: bpy.props.IntProperty(
+ name = "Number of Cuts",
+ default = 1,
+ min = 1,
+ max = 100,
+ subtype = 'UNSIGNED')
+
+ use_single_edge: bpy.props.BoolProperty(
+ name = "Quad/Tri Mode",
+ description = "Cut boundary faces",
+ default = False)
+
+ corner_type: bpy.props.EnumProperty(
+ items = [('INNER_VERT', "Inner Vert", ""),
+ ('PATH', "Path", ""),
+ ('FAN', "Fan", ""),
+ ('STRAIGHT_CUT', "Straight Cut", ""),],
+ name = "Quad Corner Type",
+ description = "How to subdivide quad corners",
+ default = 'STRAIGHT_CUT')
+
+ use_grid_fill: bpy.props.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 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 selection found")
+ 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 = str(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 menu_deselect_boundary(self, context):
+ self.layout.operator(MESH_xOT_deselect_boundary.bl_idname)
+
+def menu_cut_faces(self, context):
+ self.layout.operator(MESH_xOT_cut_faces.bl_idname)
+
+def register():
+ bpy.utils.register_class(MESH_xOT_deselect_boundary)
+ bpy.utils.register_class(MESH_xOT_cut_faces)
+
+ if __name__ != "__main__":
+ bpy.types.VIEW3D_MT_select_edit_mesh.append(menu_deselect_boundary)
+ bpy.types.VIEW3D_MT_edit_mesh_faces.append(menu_cut_faces)
+
+def unregister():
+ bpy.utils.unregister_class(MESH_xOT_deselect_boundary)
+ bpy.utils.unregister_class(MESH_xOT_cut_faces)
+
+ if __name__ != "__main__":
+ bpy.types.VIEW3D_MT_select_edit_mesh.remove(menu_deselect_boundary)
+ bpy.types.VIEW3D_MT_edit_mesh_faces.remove(menu_cut_faces)
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/mesh_edge_roundifier.py b/mesh_tools/mesh_edge_roundifier.py
new file mode 100644
index 00000000..704a260d
--- /dev/null
+++ b/mesh_tools/mesh_edge_roundifier.py
@@ -0,0 +1,1397 @@
+# ##### 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": "Edge Roundifier",
+ "category": "Mesh",
+ "author": "Piotr Komisarczyk (komi3D), PKHG",
+ "version": (1, 0, 2),
+ "blender": (2, 80, 0),
+ "location": "SPACE > Edge Roundifier or CTRL-E > "
+ "Edge Roundifier or Tools > Addons > Edge Roundifier",
+ "description": "Mesh editing script allowing edge rounding",
+ "wiki_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 calculation 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 None # Replaced NaN with None
+ 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
+ # komi3D> Roundifier to work on Normal and View coordinate systems
+ 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 parameters 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):
+ obj = context.active_object
+ return (obj and obj.type == 'MESH' and
+ obj.mode == 'EDIT')
+
+ def prepareMesh(self, context):
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ mesh = context.view_layer.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(factor=percent)
+ col = split.column()
+
+ col.label(text=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(text=label)
+ row2 = layout.row(align=alignment)
+ if property2:
+ split = row2.split(factor=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(factor=percent)
+ col = split.column()
+
+ col.label(text=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.view_layer.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='EDGES')
+
+ if parameters["removeScaledEdges"] and self.edgeScaleFactor != 1.0:
+ bmesh.ops.delete(bm, geom=scaledEdges, context='EDGES')
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bm.to_mesh(mesh)
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.remove_doubles()
+
+ 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),
+ orient_type='GLOBAL',
+ mirror=False,
+ use_proportional_edit=False,
+ )
+ 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),
+ orient_type='GLOBAL',
+ mirror=False,
+ use_proportional_edit=False,
+ )
+ 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):
+ # in case on XZ or YZ there are no arcs drawn
+ if arcs[i] is None or arcs[i + 1] is None:
+ 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"]:
+ # center gets added as last vert of arc
+ lastVertIdOfLastArc = lastVertIdOfLastArc - 1
+
+ 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 = 'VERTS')
+ 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.view_layer.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
+
+
+ @classmethod
+ def poll(cls, context):
+ return (context.view_layer.objects.active.type == 'MESH') and (context.view_layer.objects.active.mode == 'EDIT')
+
+def draw_item(self, context):
+ self.layout.operator_context = 'INVOKE_DEFAULT'
+ self.layout.operator('mesh.edge_roundifier')
+
+
+classes = (
+ EdgeRoundifier,
+ )
+
+reg_cls, unreg_cls = bpy.utils.register_classes_factory(classes)
+
+
+def register():
+ reg_cls()
+ bpy.types.VIEW3D_MT_edit_mesh_edges.append(draw_item)
+
+
+def unregister():
+ unreg_cls()
+ bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_item)
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/mesh_edges_floor_plan.py b/mesh_tools/mesh_edges_floor_plan.py
new file mode 100644
index 00000000..1bbd7748
--- /dev/null
+++ b/mesh_tools/mesh_edges_floor_plan.py
@@ -0,0 +1,384 @@
+# ##### 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 #####
+
+# based upon the functionality of Mesh to wall by luxuy_BlenderCN
+# thanks to meta-androcto
+
+bl_info = {
+ "name": "Edge Floor Plan",
+ "author": "lijenstina",
+ "version": (0, 2),
+ "blender": (2, 78, 0),
+ "location": "View3D > EditMode > Mesh",
+ "description": "Make a Floor Plan from Edges",
+ "wiki_url": "",
+ "category": "Mesh"}
+
+import bpy
+import bmesh
+from bpy.types import Operator
+from bpy.props import (
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+ FloatVectorProperty,
+ IntProperty,
+ )
+
+
+# Handle error notifications
+def error_handlers(self, error, reports="ERROR"):
+ if self and reports:
+ self.report({'WARNING'}, reports + " (See Console for more info)")
+
+ print("\n[mesh.edges_floor_plan]\nError: {}\n".format(error))
+
+
+class MESH_OT_edges_floor_plan(Operator):
+ bl_idname = "mesh.edges_floor_plan"
+ bl_label = "Edges Floor Plan"
+ bl_description = "Top View, Extrude Flat Along Edges"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ wid: FloatProperty(
+ name="Wall width:",
+ description="Set the width of the generated walls\n",
+ default=0.1,
+ min=0.001, max=30000
+ )
+ depth: FloatProperty(
+ name="Inner height:",
+ description="Set the height of the inner wall edges",
+ default=0.0,
+ min=0, max=10
+ )
+ connect_ends: BoolProperty(
+ name="Connect Ends",
+ description="Connect the ends of the boundary Edge loops",
+ default=False
+ )
+ repeat_cleanup: IntProperty(
+ name="Recursive Prepare",
+ description="Number of times that the preparation phase runs\n"
+ "at the start of the script\n"
+ "If parts of the mesh are not modified, increase this value",
+ min=1, max=20,
+ default=1
+ )
+ fill_items = [
+ ('EDGE_NET', "Edge Net",
+ "Edge Net Method for mesh preparation - Initial Fill\n"
+ "The filled in faces will be Inset individually\n"
+ "Supports simple 3D objects"),
+ ('SINGLE_FACE', "Single Face",
+ "Single Face Method for mesh preparation - Initial Fill\n"
+ "The produced face will be Triangulated before Inset Region\n"
+ "Good for edges forming a circle, avoid 3D objects"),
+ ('SOLIDIFY', "Solidify",
+ "Extrude and Solidify Method\n"
+ "Useful for complex meshes, however works best on flat surfaces\n"
+ "as the extrude direction has to be defined")
+ ]
+ fill_type: EnumProperty(
+ name="Fill Type",
+ items=fill_items,
+ description="Choose the method for creating geometry",
+ default='SOLIDIFY'
+ )
+ keep_faces: BoolProperty(
+ name="Keep Faces",
+ description="Keep or not the fill faces\n"
+ "Can depend on Remove Ngons state",
+ default=False
+ )
+ tri_faces: BoolProperty(
+ name="Triangulate Faces",
+ description="Triangulate the created fill faces\n"
+ "Sometimes can lead to unsatisfactory results",
+ default=False
+ )
+ initial_extrude: FloatVectorProperty(
+ name="Initial Extrude",
+ description="",
+ default=(0.0, 0.0, 0.1),
+ min=-20.0, max=20.0,
+ subtype='XYZ',
+ precision=3,
+ size=3
+ )
+ remove_ngons: BoolProperty(
+ name="Remove Ngons",
+ description="Keep or not the Ngon Faces\n"
+ "Note about limitations:\n"
+ "Sometimes the kept Faces could be Ngons\n"
+ "Removing the Ngons can lead to no geometry created",
+ default=True
+ )
+ offset: FloatProperty(
+ name="Wall Offset:",
+ description="Set the offset for the Solidify modifier",
+ default=0.0,
+ min=-1.0, max=1.0
+ )
+ only_rim: BoolProperty(
+ name="Rim Only",
+ description="Solidify Fill Rim only option",
+ default=False
+ )
+
+ @classmethod
+ def poll(cls, context):
+ ob = context.active_object
+ return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def check_edge(self, context):
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.mode_set(mode='EDIT')
+ obj = bpy.context.object
+ me_check = obj.data
+ if len(me_check.edges) < 1:
+ return False
+
+ return True
+
+ @staticmethod
+ def ensure(bm):
+ if bm:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ def solidify_mod(self, context, ob, wid, offset, only_rim):
+ try:
+ mods = ob.modifiers.new(
+ name="_Mesh_Solidify_Wall", type='SOLIDIFY'
+ )
+ mods.thickness = wid
+ mods.use_quality_normals = True
+ mods.offset = offset
+ mods.use_even_offset = True
+ mods.use_rim = True
+ mods.use_rim_only = only_rim
+ mods.show_on_cage = True
+
+ bpy.ops.object.modifier_apply(
+ modifier="_Mesh_Solidify_Wall"
+ )
+ except Exception as e:
+ error_handlers(self, e,
+ reports="Adding a Solidify Modifier failed")
+ pass
+
+ def draw(self, context):
+ layout = self.layout
+
+ box = layout.box()
+ box.label(text="Choose Method:", icon="NONE")
+ box.prop(self, "fill_type")
+
+ col = box.column(align=True)
+
+ if self.fill_type == 'EDGE_NET':
+ col.prop(self, "repeat_cleanup")
+ col.prop(self, "remove_ngons", toggle=True)
+
+ elif self.fill_type == 'SOLIDIFY':
+ col.prop(self, "offset", slider=True)
+ col.prop(self, "initial_extrude")
+
+ else:
+ col.prop(self, "remove_ngons", toggle=True)
+ col.prop(self, "tri_faces", toggle=True)
+
+ box = layout.box()
+ box.label(text="Settings:", icon="NONE")
+
+ col = box.column(align=True)
+ col.prop(self, "wid")
+
+ if self.fill_type != 'SOLIDIFY':
+ col.prop(self, "depth")
+ col.prop(self, "connect_ends", toggle=True)
+ col.prop(self, "keep_faces", toggle=True)
+ else:
+ col.prop(self, "only_rim", toggle=True)
+
+ def execute(self, context):
+ if not self.check_edge(context):
+ self.report({'WARNING'},
+ "Operation Cancelled. Needs a Mesh with at least one edge")
+ return {'CANCELLED'}
+
+ wid = self.wid * 0.1
+ depth = self.depth * 0.1
+ offset = self.offset * 0.1
+ store_selection_mode = context.tool_settings.mesh_select_mode
+ # Note: the remove_doubles called after bmesh creation would make
+ # blender crash with certain meshes - keep it in mind for the future
+ bpy.ops.mesh.remove_doubles(threshold=0.003)
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.mode_set(mode='EDIT')
+ ob = bpy.context.object
+
+ me = ob.data
+ bm = bmesh.from_edit_mesh(me)
+
+ bmesh.ops.delete(bm, geom=bm.faces, context='FACES_ONLY')
+ self.ensure(bm)
+ context.tool_settings.mesh_select_mode = (False, True, False)
+ original_edges = [edge.index for edge in bm.edges]
+ original_verts = [vert.index for vert in bm.verts]
+ self.ensure(bm)
+ bpy.ops.mesh.select_all(action='DESELECT')
+
+ if self.fill_type == 'EDGE_NET':
+ for i in range(self.repeat_cleanup):
+ bmesh.ops.edgenet_prepare(bm, edges=bm.edges)
+ self.ensure(bm)
+ bmesh.ops.edgenet_fill(bm, edges=bm.edges, mat_nr=0, use_smooth=True, sides=0)
+ self.ensure(bm)
+ if self.remove_ngons:
+ ngons = [face for face in bm.faces if len(face.edges) > 4]
+ self.ensure(bm)
+ bmesh.ops.delete(bm, geom=ngons, context='FACES') # 5 - delete faces
+ del ngons
+ self.ensure(bm)
+
+ elif self.fill_type == 'SOLIDIFY':
+ for vert in bm.verts:
+ vert.normal_update()
+ self.ensure(bm)
+ bmesh.ops.extrude_edge_only(
+ bm, edges=bm.edges, use_select_history=False
+ )
+ self.ensure(bm)
+ verts_extrude = [vert for vert in bm.verts if vert.index in original_verts]
+ self.ensure(bm)
+ bmesh.ops.translate(
+ bm,
+ verts=verts_extrude,
+ vec=(self.initial_extrude)
+ )
+ self.ensure(bm)
+ del verts_extrude
+ self.ensure(bm)
+
+ for edge in bm.edges:
+ if edge.is_boundary:
+ edge.select = True
+
+ bm = bmesh.update_edit_mesh(ob.data, 1, 1)
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+ self.solidify_mod(context, ob, wid, offset, self.only_rim)
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ context.tool_settings.mesh_select_mode = store_selection_mode
+
+ return {'FINISHED'}
+
+ else:
+ bm.faces.new(bm.verts)
+ self.ensure(bm)
+
+ if self.tri_faces:
+ bmesh.ops.triangle_fill(
+ bm, use_beauty=True, use_dissolve=False, edges=bm.edges
+ )
+ self.ensure(bm)
+
+ if self.remove_ngons and self.fill_type != 'EDGE_NET':
+ ngons = [face for face in bm.faces if len(face.edges) > 4]
+ self.ensure(bm)
+ bmesh.ops.delete(bm, geom=ngons, context='FACES') # 5 - delete faces
+ del ngons
+ self.ensure(bm)
+
+ del_boundary = [edge for edge in bm.edges if edge.index not in original_edges]
+ self.ensure(bm)
+
+ del original_edges
+ self.ensure(bm)
+
+ if self.fill_type == 'EDGE_NET':
+ extrude_inner = bmesh.ops.inset_individual(
+ bm, faces=bm.faces, thickness=wid, depth=depth,
+ use_even_offset=True, use_interpolate=False,
+ use_relative_offset=False
+ )
+ else:
+ extrude_inner = bmesh.ops.inset_region(
+ bm, faces=bm.faces, faces_exclude=[], use_boundary=True,
+ use_even_offset=True, use_interpolate=False,
+ use_relative_offset=False, use_edge_rail=False,
+ thickness=wid, depth=depth, use_outset=False
+ )
+ self.ensure(bm)
+
+ del_faces = [faces for faces in bm.faces if faces not in extrude_inner["faces"]]
+ self.ensure(bm)
+ del extrude_inner
+ self.ensure(bm)
+
+ if not self.keep_faces:
+ bmesh.ops.delete(bm, geom=del_faces, context='FACES') # 5 delete faces
+ del del_faces
+ self.ensure(bm)
+
+ face_del = set()
+ for face in bm.faces:
+ for edge in del_boundary:
+ if isinstance(edge, bmesh.types.BMEdge):
+ if edge in face.edges:
+ face_del.add(face)
+ self.ensure(bm)
+ face_del = list(face_del)
+ self.ensure(bm)
+
+ del del_boundary
+ self.ensure(bm)
+
+ if not self.connect_ends:
+ bmesh.ops.delete(bm, geom=face_del, context='FACES')
+ self.ensure(bm)
+
+ del face_del
+ self.ensure(bm)
+
+ for edge in bm.edges:
+ if edge.is_boundary:
+ edge.select = True
+
+ bm = bmesh.update_edit_mesh(ob.data, 1, 1)
+
+ context.tool_settings.mesh_select_mode = store_selection_mode
+
+ return {'FINISHED'}
+
+
+def register():
+ bpy.utils.register_class(MESH_OT_edges_floor_plan)
+
+
+def unregister():
+ bpy.utils.unregister_class(MESH_OT_edges_floor_plan)
+
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/mesh_edges_length.py b/mesh_tools/mesh_edges_length.py
new file mode 100644
index 00000000..f4f1d067
--- /dev/null
+++ b/mesh_tools/mesh_edges_length.py
@@ -0,0 +1,341 @@
+# 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, 71, 0),
+ "location": "Toolbar > Tools > Mesh Tools: set Length(Shit+Alt+E)",
+ "warning": "",
+ "wiki_url": "",
+ "category": "Mesh",
+ }
+
+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 length 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_length_type: EnumProperty(
+ items=[
+ ('manual', "Manual",
+ "Input manually the desired Target Length"),
+ ('existing', "Existing Length",
+ "Use existing geometry Edges' characteristics"),
+ ],
+ name="Set Type of Input",
+ )
+ target_length: FloatProperty(
+ name="Target Length",
+ description="Input a value for an Edges Length target",
+ default=1.00,
+ unit='LENGTH',
+ precision=5
+ )
+ existing_length: 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 length 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 length"
+ )
+ 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_lengths = []
+ 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(text="Original Active length is: {:.3f}".format(self.old_length))
+
+ layout.label(text="Input Mode:")
+ layout.prop(self, "set_length_type", expand=True)
+ if self.set_length_type == 'manual':
+ layout.prop(self, "target_length")
+ else:
+ layout.prop(self, "existing_length", text="")
+
+ layout.label(text="Mode:")
+ layout.prop(self, "mode", text="")
+
+ layout.label(text="Resize Behavior:")
+ layout.prop(self, "behaviour", text="")
+
+ def get_existing_edge_length(self, bm):
+ if self.existing_length != "active":
+ if self.existing_length == "min":
+ return min(self.edge_lengths)
+ if self.existing_length == "max":
+ return max(self.edge_lengths)
+ elif self.existing_length == "average":
+ return sum(self.edge_lengths) / 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_lengths.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_length_type == 'manual':
+ vector.length = abs(self.target_length)
+ else:
+ get_lengths = self.get_existing_edge_length(bm)
+ # check for edit mode
+ if not get_lengths:
+ self.report({'WARNING'},
+ "Operation Cancelled. "
+ "Active Edge could not be determined (needs selection in Edit Mode)")
+ return {'CANCELLED'}
+
+ vector.length = get_lengths
+
+ if vector.length == 0.0:
+ self.report({'ERROR'}, "Operation cancelled. Target length 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_tools/mesh_edgetools.py b/mesh_tools/mesh_edgetools.py
new file mode 100644
index 00000000..6fd98b75
--- /dev/null
+++ b/mesh_tools/mesh_edgetools.py
@@ -0,0 +1,1880 @@
+# The Blender Edgetools is to bring CAD tools to Blender.
+# Copyright (C) 2012 Paul Marshall
+
+# ##### 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 #####
+
+# <pep8 compliant>
+
+bl_info = {
+ "name": "EdgeTools",
+ "author": "Paul Marshall",
+ "version": (0, 9, 2),
+ "blender": (2, 80, 0),
+ "location": "View3D > Toolbar and View3D > Specials (W-key)",
+ "warning": "",
+ "description": "CAD style edge manipulation tools",
+ "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
+ "Scripts/Modeling/EdgeTools",
+ "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
+Removed the test operator since version 0.9.2
+added general error handling
+"""
+
+# Enable debug
+# Set to True to have the 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
+
+
+# Handle error notifications
+def error_handlers(self, op_name, error, reports="ERROR", func=False):
+ if self and reports:
+ self.report({'WARNING'}, reports + " (See Console for more info)")
+
+ is_func = "Function" if func else "Operator"
+ print("\n[Mesh EdgeTools]\n{}: {}\nError: {}\n".format(is_func, op_name, error))
+
+
+def flip_edit_mode():
+ bpy.ops.object.editmode_toggle()
+ bpy.ops.object.editmode_toggle()
+
+
+# check the appropriate selection condition
+# to prevent crashes with the index out of range errors
+# pass the bEdges and bVerts based selection tables here
+# types: Edge, Vertex, All
+def is_selected_enough(self, bEdges, bVerts, edges_n=1, verts_n=0, types="Edge"):
+ check = False
+ try:
+ if bEdges and types == "Edge":
+ check = (len(bEdges) >= edges_n)
+ elif bVerts and types == "Vertex":
+ check = (len(bVerts) >= verts_n)
+ elif bEdges and bVerts and types == "All":
+ check = (len(bEdges) >= edges_n and len(bVerts) >= verts_n)
+
+ if check is False:
+ strings = "%s Vertices and / or " % verts_n if verts_n != 0 else ""
+ self.report({'WARNING'},
+ "Needs at least " + strings + "%s Edge(s) selected. "
+ "Operation Cancelled" % edges_n)
+ flip_edit_mode()
+
+ return check
+
+ except Exception as e:
+ error_handlers(self, "is_selected_enough", e,
+ "No appropriate selection. Operation Cancelled", func=True)
+ return False
+
+ return False
+
+
+# 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 defined 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:
+ if ENABLE_DEBUG:
+ 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
+ if ENABLE_DEBUG:
+ 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
+
+ row = layout.row(align=True)
+ row.prop(self, "di1", toggle=True)
+ row.prop(self, "di2", toggle=True)
+
+ 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):
+ try:
+ 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 not is_selected_enough(self, edges, 0, edges_n=1, verts_n=0, types="Edge"):
+ return {'CANCELLED'}
+
+ 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)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_extend", e,
+ reports="Extend Operator failed", func=False)
+ return {'CANCELLED'}
+
+ 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(text="Edge 1:")
+ split = layout.split(factor=0.8, align=True)
+ split.prop(self, "ten1")
+ split.prop(self, "flip1", text="Flip1", toggle=True)
+
+ layout.label(text="Edge 2:")
+ split = layout.split(factor=0.8, align=True)
+ split.prop(self, "ten2")
+ split.prop(self, "flip2", text="Flip2", toggle=True)
+
+ @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):
+ try:
+ 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]
+
+ if not is_selected_enough(self, edges, 0, edges_n=2, verts_n=0, types="Edge"):
+ return {'CANCELLED'}
+
+ 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)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_spline", e,
+ reports="Spline Operator failed", func=False)
+ return {'CANCELLED'}
+
+ 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(text="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(text="Direction:")
+ row = layout.row(align=False)
+ row.alignment = 'EXPAND'
+ row.prop(self, "pos")
+ row.prop(self, "neg")
+
+ layout.separator()
+
+ col = layout.column(align=True)
+ col.prop(self, "angle")
+ col.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):
+ try:
+ 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 = []
+
+ if not is_selected_enough(self, edges, 0, edges_n=2, verts_n=0, types="Edge"):
+ 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)
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_ortho", e,
+ reports="Angle Off Edge Operator failed", func=False)
+ return {'CANCELLED'}
+
+ 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 perceived 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):
+ try:
+ 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 not is_selected_enough(self, edges, 0, edges_n=1, verts_n=0, types="Edge"):
+ 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 initial 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:
+ flip_edit_mode()
+ self.report({'WARNING'},
+ "Active geometry is not an edge. Operation Cancelled")
+ 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:
+ flip_edit_mode()
+ self.report({'WARNING'},
+ "Active geometry is not an edge. Operation Cancelled")
+ 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 origin 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 origin 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 origin 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 origin 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)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_shaft", e,
+ reports="Shaft Operator failed", func=False)
+ return {'CANCELLED'}
+
+ 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 splitting 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(text="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):
+ try:
+ 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:
+ flip_edit_mode()
+ self.report({'WARNING'},
+ "Please select a face as the cutting plane. Operation Cancelled")
+ 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)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_slice", e,
+ reports="Slice Operator failed", func=False)
+ return {'CANCELLED'}
+
+ 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):
+ try:
+ 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)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_project", e,
+ reports="Project Operator failed", func=False)
+
+ return {'CANCELLED'}
+
+ 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):
+ try:
+ 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)
+
+ except Exception as e:
+ error_handlers(self, "mesh.edgetools_project_end", e,
+ reports="Project (End Point) Operator failed", func=False)
+ return {'CANCELLED'}
+
+ 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")
+
+def menu_func(self, context):
+ self.layout.menu("VIEW3D_MT_edit_mesh_edgetools")
+
+# define classes for registration
+classes = (
+ VIEW3D_MT_edit_mesh_edgetools,
+ Extend,
+ Spline,
+ Ortho,
+ Shaft,
+ Slice,
+ Project,
+ Project_End,
+ )
+
+
+# registering and menu integration
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+ bpy.types.VIEW3D_MT_edit_mesh_context_menu.prepend(menu_func)
+
+# unregistering and removing menus
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+ bpy.types.VIEW3D_MT_edit_mesh_context_menu.remove(menu_func)
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/mesh_extrude_and_reshape.py b/mesh_tools/mesh_extrude_and_reshape.py
new file mode 100644
index 00000000..f4245ac2
--- /dev/null
+++ b/mesh_tools/mesh_extrude_and_reshape.py
@@ -0,0 +1,377 @@
+# ##### 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, 80, 0),
+ "location": "View3D > UI > 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",
+ "category": "Mesh"}
+
+import bpy
+import bmesh
+from mathutils.geometry import intersect_line_line
+from bpy.types import Operator
+
+
+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]:
+ 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
+
+ return new_edges1, new_edges2, targetmap
+
+
+class ER_OT_Extrude_and_Reshape(Operator):
+ bl_idname = "mesh.extrude_reshape"
+ bl_label = "Extrude and Reshape"
+ bl_description = "Push and pull face entities to sculpt 3d models"
+ bl_options = {'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
+
+ @classmethod
+ def poll(cls, context):
+ if context.mode=='EDIT_MESH':
+ return True
+
+ 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 is 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, loop_triangles=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 is 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, loop_triangles=True, destructive=True)
+ bpy.ops.transform.translate(
+ 'INVOKE_DEFAULT', constraint_axis=(False, False, True),
+ orient_type='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")
+
+
+def register():
+ bpy.utils.register_class(ER_OT_Extrude_and_Reshape)
+
+
+def unregister():
+ bpy.utils.unregister_class(ER_OT_Extrude_and_Reshape)
+
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/mesh_filletplus.py b/mesh_tools/mesh_filletplus.py
new file mode 100644
index 00000000..01f2f67e
--- /dev/null
+++ b/mesh_tools/mesh_filletplus.py
@@ -0,0 +1,430 @@
+# -*- coding: utf-8 -*-
+
+# ##### END 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": "FilletPlus",
+ "author": "Gert De Roost - original by zmj100",
+ "version": (0, 4, 3),
+ "blender": (2, 80, 0),
+ "location": "View3D > Tool Shelf",
+ "description": "",
+ "warning": "",
+ "wiki_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, sin,
+ degrees, 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_loop_triangles=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 is equal to 0 or 180", icon="INFO")
+ layout.label(text="Can not fillet", icon="BLANK1")
+ else:
+ layout.prop(self, "radius")
+ if self.radius is True:
+ layout.label(text="Radius:")
+ elif self.radius is False:
+ layout.label(text="Distance:")
+ layout.prop(self, "adj")
+ layout.label(text="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, f_buf
+
+ 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'}
+
+# define classes for registration
+classes = (
+ MESH_OT_fillet_plus,
+ )
+
+# registering and menu integration
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+
+
+# unregistering and removing menus
+def unregister():
+ for cls in reversed(classes):
+ bpy.utils.unregister_class(cls)
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/mesh_mextrude_plus.py b/mesh_tools/mesh_mextrude_plus.py
new file mode 100644
index 00000000..5fa2aa2b
--- /dev/null
+++ b/mesh_tools/mesh_mextrude_plus.py
@@ -0,0 +1,370 @@
+# ##### 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
+# Original code by liero
+# Update by Jimmy Hazevoet 03/2017 for Blender 2.79
+# normal rotation, probability, scaled offset, object coords, initial and per step noise
+
+
+bl_info = {
+ "name": "MExtrude Plus1",
+ "author": "liero, Jimmy Hazevoet",
+ "version": (1, 3, 0),
+ "blender": (2, 77, 0),
+ "location": "View3D > Tool Shelf",
+ "description": "Repeat extrusions from faces to create organic shapes",
+ "warning": "",
+ "wiki_url": "",
+ "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, Vector,
+ )
+from bpy.props import (
+ FloatProperty,
+ IntProperty,
+ BoolProperty,
+ )
+
+
+def gloc(self, r):
+ return Vector((self.offx, self.offy, self.offz))
+
+
+def vloc(self, r):
+ random.seed(self.ran + r)
+ return self.off * (1 + gauss(0, self.var1 / 3))
+
+
+def nrot(self, n):
+ return Euler((radians(self.nrotx) * n[0],
+ radians(self.nroty) * n[1],
+ radians(self.nrotz) * n[2]), 'XYZ')
+
+
+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 + 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", "PRESET"}
+
+ off: FloatProperty(
+ name="Offset",
+ soft_min=0.001, soft_max=10,
+ min=-100, max=100,
+ default=1.0,
+ description="Translation"
+ )
+ offx: FloatProperty(
+ name="Loc X",
+ soft_min=-10.0, soft_max=10.0,
+ min=-100.0, max=100.0,
+ default=0.0,
+ description="Global Translation X"
+ )
+ offy: FloatProperty(
+ name="Loc Y",
+ soft_min=-10.0, soft_max=10.0,
+ min=-100.0, max=100.0,
+ default=0.0,
+ description="Global Translation Y"
+ )
+ offz: FloatProperty(
+ name="Loc Z",
+ soft_min=-10.0, soft_max=10.0,
+ min=-100.0, max=100.0,
+ default=0.0,
+ description="Global Translation Z"
+ )
+ 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"
+ )
+ nrotx: FloatProperty(
+ name="N Rot X",
+ min=-85, max=85,
+ soft_min=-30, soft_max=30,
+ default=0,
+ description="Normal X Rotation"
+ )
+ nroty: FloatProperty(
+ name="N Rot Y",
+ min=-85, max=85,
+ soft_min=-30, soft_max=30,
+ default=0,
+ description="Normal Y Rotation"
+ )
+ nrotz: FloatProperty(
+ name="N Rot Z",
+ min=-85, max=85,
+ soft_min=-30, soft_max=30,
+ default=-0,
+ description="Normal Z Rotation"
+ )
+ sca: FloatProperty(
+ name="Scale",
+ min=0.01, max=10,
+ soft_min=0.5, soft_max=1.5,
+ default=1.0,
+ description="Scaling of the selected faces after extrusion"
+ )
+ var1: FloatProperty(
+ name="Offset Var", min=-10, max=10,
+ soft_min=-1, soft_max=1,
+ default=0,
+ description="Offset variation"
+ )
+ var2: FloatProperty(
+ name="Rotation Var",
+ min=-10, max=10,
+ soft_min=-1, soft_max=1,
+ default=0,
+ description="Rotation variation"
+ )
+ var3: FloatProperty(
+ name="Scale Noise",
+ min=-10, max=10,
+ soft_min=-1, soft_max=1,
+ default=0,
+ description="Scaling noise"
+ )
+ var4: IntProperty(
+ name="Probability",
+ min=0, max=100,
+ default=100,
+ description="Probability, chance of extruding a face"
+ )
+ num: IntProperty(
+ name="Repeat",
+ min=1, max=500,
+ soft_max=100,
+ default=5,
+ description="Repetitions"
+ )
+ ran: IntProperty(
+ name="Seed",
+ min=-9999, max=9999,
+ default=0,
+ description="Seed to feed random values"
+ )
+ opt1: BoolProperty(
+ name="Polygon coordinates",
+ default=True,
+ description="Polygon coordinates, Object coordinates"
+ )
+ opt2: BoolProperty(
+ name="Proportional offset",
+ default=False,
+ description="Scale * Offset"
+ )
+ opt3: BoolProperty(
+ name="Per step rotation noise",
+ default=False,
+ description="Per step rotation noise, Initial rotation noise"
+ )
+ opt4: BoolProperty(
+ name="Per step scale noise",
+ default=False,
+ description="Per step scale noise, Initial scale noise"
+ )
+
+ @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, "offx", slider=True)
+ col.prop(self, "offy", slider=True)
+ col.prop(self, "offz", slider=True)
+
+ col = layout.column(align=True)
+ col.prop(self, "rotx", slider=True)
+ col.prop(self, "roty", slider=True)
+ col.prop(self, "rotz", slider=True)
+ col.prop(self, "nrotx", slider=True)
+ col.prop(self, "nroty", slider=True)
+ col.prop(self, "nrotz", slider=True)
+ col = layout.column(align=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, "var4", slider=True)
+ col.prop(self, "ran")
+ col = layout.column(align=False)
+ col.prop(self, 'num')
+
+ col = layout.column(align=True)
+ col.label(text="Options:")
+ col.prop(self, "opt1")
+ col.prop(self, "opt2")
+ col.prop(self, "opt3")
+ col.prop(self, "opt4")
+
+ def execute(self, context):
+ obj = bpy.context.object
+ om = obj.mode
+ bpy.context.tool_settings.mesh_select_mode = [False, False, True]
+ origin = Vector([0.0, 0.0, 0.0])
+
+ # 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):
+ nro = nrot(self, of.normal)
+ off = vloc(self, i)
+ loc = gloc(self, i)
+ of.normal_update()
+
+ # initial rotation noise
+ if self.opt3 is False:
+ rot = vrot(self, i)
+ # initial scale noise
+ if self.opt4 is False:
+ s = vsca(self, i)
+
+ # extrusion loop
+ for r in range(self.num):
+ # random probability % for extrusions
+ if self.var4 > int(random.random() * 100):
+ nf = of.copy()
+ nf.normal_update()
+ no = nf.normal.copy()
+
+ # face/obj coördinates
+ if self.opt1 is True:
+ ce = nf.calc_center_bounds()
+ else:
+ ce = origin
+
+ # per step rotation noise
+ if self.opt3 is True:
+ rot = vrot(self, i + r)
+ # per step scale noise
+ if self.opt4 is True:
+ s = vsca(self, i + r)
+
+ # proportional, scale * offset
+ if self.opt2 is True:
+ off = s * off
+
+ for v in nf.verts:
+ v.co -= ce
+ v.co.rotate(nro)
+ v.co.rotate(rot)
+ v.co += ce + loc + 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:
+ if f not in sel:
+ f.select = True
+ else:
+ f.select = False
+
+ 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_tools/mesh_offset_edges.py b/mesh_tools/mesh_offset_edges.py
new file mode 100644
index 00000000..524076a5
--- /dev/null
+++ b/mesh_tools/mesh_offset_edges.py
@@ -0,0 +1,791 @@
+# ***** 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, Veezen fix 2.8 (temporary)",
+ #i tried edit newest version, but got some errors, works only on 0,2,6
+ "version": (0, 2, 6),
+ "blender": (2, 80, 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 math
+from math import sin, cos, pi, copysign, radians
+import bpy
+from bpy_extras import view3d_utils
+import bmesh
+from mathutils import Vector
+from time import perf_counter
+
+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
+
+
+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 precedance.
+ if not f.hide and f.normal != ZERO_VEC:
+ adj_exist = True
+ 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
+ #assert len(verts) == len(verts_ex)
+ 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='EDGES')
+ 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_axis[0]:
+ mirror_planes.append((loc, norm_x, merge_limit))
+ if m.use_axis[1]:
+ mirror_planes.append((loc, norm_y, merge_limit))
+ if m.use_axis[2]:
+ 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]
+ e = edges[i]
+ 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
+
+def use_cashes(self, context):
+ self.caches_valid = True
+
+angle_presets = {'0°': 0,
+ '15°': radians(15),
+ '30°': radians(30),
+ '45°': radians(45),
+ '60°': radians(60),
+ '75°': radians(75),
+ '90°': radians(90),}
+def assign_angle_presets(self, context):
+ use_cashes(self, context)
+ self.angle = angle_presets[self.angle_presets]
+
+class OffsetEdges(bpy.types.Operator):
+ """Offset Edges."""
+ bl_idname = "mesh.offset_edges"
+ bl_label = "Offset Edges"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ geometry_mode: bpy.props.EnumProperty(
+ items=[('offset', "Offset", "Offset edges"),
+ ('extrude', "Extrude", "Extrude edges"),
+ ('move', "Move", "Move selected edges")],
+ name="Geometory mode", default='offset',
+ update=use_cashes)
+ width: bpy.props.FloatProperty(
+ name="Width", default=.2, precision=4, step=1, update=use_cashes)
+ flip_width: bpy.props.BoolProperty(
+ name="Flip Width", default=False,
+ description="Flip width direction", update=use_cashes)
+ depth: bpy.props.FloatProperty(
+ name="Depth", default=.0, precision=4, step=1, update=use_cashes)
+ flip_depth: bpy.props.BoolProperty(
+ name="Flip Depth", default=False,
+ description="Flip depth direction", update=use_cashes)
+ depth_mode: bpy.props.EnumProperty(
+ items=[('angle', "Angle", "Angle"),
+ ('depth', "Depth", "Depth")],
+ name="Depth mode", default='angle', update=use_cashes)
+ angle: bpy.props.FloatProperty(
+ name="Angle", default=0, precision=3, step=.1,
+ min=-2*pi, max=2*pi, subtype='ANGLE',
+ description="Angle", update=use_cashes)
+ flip_angle: bpy.props.BoolProperty(
+ name="Flip Angle", default=False,
+ description="Flip Angle", update=use_cashes)
+ follow_face: bpy.props.BoolProperty(
+ name="Follow Face", default=False,
+ description="Offset along faces around")
+ mirror_modifier: bpy.props.BoolProperty(
+ name="Mirror Modifier", default=False,
+ description="Take into account of Mirror modifier")
+ edge_rail: bpy.props.BoolProperty(
+ name="Edge Rail", default=False,
+ description="Align vertices along inner edges")
+ edge_rail_only_end: bpy.props.BoolProperty(
+ name="Edge Rail Only End", default=False,
+ description="Apply edge rail to end verts only")
+ threshold: bpy.props.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: bpy.props.BoolProperty(
+ name="Caches Valid", default=False,
+ options={'HIDDEN'})
+ angle_presets: bpy.props.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="")
+ #layout.prop(self, 'geometry_mode', expand=True)
+
+ 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
+
+ time = perf_counter()
+
+ set_edges_orig = collect_edges(bm)
+ if set_edges_orig is None:
+ self.report({'WARNING'},
+ "No edges selected.")
+ 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:
+ #self.report({'WARNING'},
+ # "All selected edges are on mirror planes.")
+ 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-overlap 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)
+
+ 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.
+
+ #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.
+
+ #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)
+
+class OffsetEdgesMenu(bpy.types.Menu):
+ bl_idname = "VIEW3D_MT_edit_mesh_offset_edges"
+ bl_label = "Offset Edges"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator_context = 'INVOKE_DEFAULT'
+
+ off = layout.operator('mesh.offset_edges', text='Offset')
+ off.geometry_mode = 'offset'
+
+ ext = layout.operator('mesh.offset_edges', text='Extrude')
+ ext.geometry_mode = 'extrude'
+
+ mov = layout.operator('mesh.offset_edges', text='Move')
+ mov.geometry_mode = 'move'
+
+classes = (
+OffsetEdges,
+OffsetEdgesMenu,
+)
+
+def draw_item(self, context):
+ self.layout.menu("VIEW3D_MT_edit_mesh_offset_edges")
+
+
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+ bpy.types.VIEW3D_MT_edit_mesh_edges.prepend(draw_item)
+
+
+def unregister():
+ for cls in reversed(classes):
+ bpy.utils.unregister_class(cls)
+ bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_item)
+
+
+if __name__ == '__main__':
+ register()
diff --git a/mesh_tools/mesh_vertex_chamfer.py b/mesh_tools/mesh_vertex_chamfer.py
new file mode 100644
index 00000000..9ac3f814
--- /dev/null
+++ b/mesh_tools/mesh_vertex_chamfer.py
@@ -0,0 +1,164 @@
+# ##### 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": "",
+ "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 length",
+ 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_loop_triangles()
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ return {'FINISHED'}
+
+
+def register():
+ bpy.utils.register_class(VertexChamfer)
+
+
+def unregister():
+ bpy.utils.unregister_class(VertexChamfer)
+
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/pkhg_faces.py b/mesh_tools/pkhg_faces.py
new file mode 100644
index 00000000..2b93d4d7
--- /dev/null
+++ b/mesh_tools/pkhg_faces.py
@@ -0,0 +1,842 @@
+# gpl author: PHKG
+
+bl_info = {
+ "name": "PKHG faces",
+ "author": "PKHG",
+ "version": (0, 0, 6),
+ "blender": (2, 71, 0),
+ "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
+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 Shape"
+ 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="Relative 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(text="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.view_layer.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'}
+
+
+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]
+ bpy.ops.object.mode_set(mode='OBJECT')
+ 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]
+ bpy.ops.object.mode_set(mode='OBJECT')
+ 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.view_layer.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 (normally) 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
+
+# define classes for registration
+classes = (
+ MESH_OT_add_faces_to_object,
+ )
+
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+
+
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+
+
+if __name__ == "__main__":
+ register()
diff --git a/mesh_tools/random_vertices.py b/mesh_tools/random_vertices.py
new file mode 100644
index 00000000..51f8be24
--- /dev/null
+++ b/mesh_tools/random_vertices.py
@@ -0,0 +1,140 @@
+# gpl authors: Oscurart, Greg
+
+bl_info = {
+ "name": "Random Vertices",
+ "author": "Oscurart, Greg",
+ "version": (1, 3),
+ "blender": (2, 63, 0),
+ "location": "Object > Transform > Random Vertices",
+ "description": "Randomize selected components of active object",
+ "warning": "",
+ "wiki_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_tools/split_solidify.py b/mesh_tools/split_solidify.py
new file mode 100644
index 00000000..690557ed
--- /dev/null
+++ b/mesh_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 LICENSE BLOCK #####
+
+bl_info = {
+ "name": "Split Solidify",
+ "author": "zmj100, updated by zeffii to BMesh",
+ "version": (0, 1, 2),
+ "blender": (2, 80, 0),
+ "location": "View3D > Tool Shelf",
+ "description": "",
+ "warning": "",
+ "wiki_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 * (f.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(text="Normal:")
+ layout.prop(self, "normal_extr", expand=True)
+ layout.prop(self, "loc_random")
+
+ if not self.loc_random:
+ layout.label(text="Distance:")
+ layout.prop(self, "distance")
+ elif self.loc_random:
+ layout.label(text="Random distance:")
+ layout.prop(self, "random_dist")
+
+ layout.label(text="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_tools/vertex_align.py b/mesh_tools/vertex_align.py
new file mode 100644
index 00000000..eb66d747
--- /dev/null
+++ b/mesh_tools/vertex_align.py
@@ -0,0 +1,301 @@
+# -*- 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 LICENSE BLOCK #####
+
+# Note: Property group was moved to __init__
+
+bl_info = {
+ "name": "Vertex Align",
+ "author": "",
+ "version": (0, 1, 7),
+ "blender": (2, 61, 0),
+ "location": "View3D > Tool Shelf",
+ "description": "",
+ "warning": "",
+ "wiki_url": "",
+ "category": "Mesh"}
+
+
+import bpy
+from bpy.props import (
+ BoolVectorProperty,
+ FloatVectorProperty,
+ )
+from mathutils import Vector
+from bpy.types import Operator
+
+
+# Edit Mode Toggle
+def edit_mode_out():
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+
+def edit_mode_in():
+ bpy.ops.object.mode_set(mode='EDIT')
+
+
+def get_mesh_data_():
+ edit_mode_out()
+ ob_act = bpy.context.active_object
+ me = ob_act.data
+ edit_mode_in()
+ return me
+
+
+def list_clear_(l):
+ l[:] = []
+ return l
+
+
+class va_buf():
+ list_v = []
+ list_0 = []
+
+
+# Store The Vertex coordinates
+class Vertex_align_store(Operator):
+ bl_idname = "vertex_align.store_id"
+ bl_label = "Active Vertex"
+ bl_description = ("Store Selected Vertex coordinates as an align point\n"
+ "Single Selected Vertex only")
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def execute(self, context):
+ try:
+ me = get_mesh_data_()
+ list_0 = [v.index for v in me.vertices if v.select]
+
+ if len(list_0) == 1:
+ list_clear_(va_buf.list_v)
+ for v in me.vertices:
+ if v.select:
+ va_buf.list_v.append(v.index)
+ bpy.ops.mesh.select_all(action='DESELECT')
+ else:
+ self.report({'WARNING'}, "Please select just One Vertex")
+ return {'CANCELLED'}
+ except:
+ self.report({'WARNING'}, "Storing selection could not be completed")
+ return {'CANCELLED'}
+
+ self.report({'INFO'}, "Selected Vertex coordinates are stored")
+
+ return {'FINISHED'}
+
+
+# Align to original
+class Vertex_align_original(Operator):
+ bl_idname = "vertex_align.align_original"
+ bl_label = "Align to original"
+ bl_description = "Align selection to stored single vertex coordinates"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @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
+ layout.label(text="Axis:")
+
+ row = layout.row(align=True)
+ row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
+ text="X", index=0, toggle=True)
+ row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
+ text="Y", index=1, toggle=True)
+ row.prop(context.scene.mesh_extra_tools, "vert_align_axis",
+ text="Z", index=2, toggle=True)
+
+ def execute(self, context):
+ edit_mode_out()
+ ob_act = context.active_object
+ me = ob_act.data
+ cen1 = context.scene.mesh_extra_tools.vert_align_axis
+ list_0 = [v.index for v in me.vertices if v.select]
+
+ if len(va_buf.list_v) == 0:
+ self.report({'INFO'},
+ "Original vertex not stored in memory. Operation Cancelled")
+ edit_mode_in()
+ return {'CANCELLED'}
+
+ elif len(va_buf.list_v) != 0:
+ if len(list_0) == 0:
+ self.report({'INFO'}, "No vertices selected. Operation Cancelled")
+ edit_mode_in()
+ return {'CANCELLED'}
+
+ elif len(list_0) != 0:
+ vo = (me.vertices[va_buf.list_v[0]].co).copy()
+ if cen1[0] is True:
+ for i in list_0:
+ v = (me.vertices[i].co).copy()
+ me.vertices[i].co = Vector((vo[0], v[1], v[2]))
+ if cen1[1] is True:
+ for i in list_0:
+ v = (me.vertices[i].co).copy()
+ me.vertices[i].co = Vector((v[0], vo[1], v[2]))
+ if cen1[2] is True:
+ for i in list_0:
+ v = (me.vertices[i].co).copy()
+ me.vertices[i].co = Vector((v[0], v[1], vo[2]))
+ edit_mode_in()
+
+ return {'FINISHED'}
+
+
+# Align to custom coordinates
+class Vertex_align_coord_list(Operator):
+ bl_idname = "vertex_align.coord_list_id"
+ bl_label = ""
+ bl_description = "Align to custom coordinates"
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
+
+ def execute(self, context):
+ edit_mode_out()
+ ob_act = context.active_object
+ me = ob_act.data
+ list_clear_(va_buf.list_0)
+ va_buf.list_0 = [v.index for v in me.vertices if v.select][:]
+
+ if len(va_buf.list_0) == 0:
+ self.report({'INFO'}, "No vertices selected. Operation Cancelled")
+ edit_mode_in()
+ return {'CANCELLED'}
+
+ elif len(va_buf.list_0) != 0:
+ bpy.ops.vertex_align.coord_menu_id('INVOKE_DEFAULT')
+
+ edit_mode_in()
+
+ return {'FINISHED'}
+
+
+# Align to custom coordinates menu
+class Vertex_align_coord_menu(Operator):
+ bl_idname = "vertex_align.coord_menu_id"
+ bl_label = "Tweak custom coordinates"
+ bl_description = "Change the custom coordinates for aligning"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def_axis_coord: FloatVectorProperty(
+ name="",
+ description="Enter the values of coordinates",
+ default=(0.0, 0.0, 0.0),
+ min=-100.0, max=100.0,
+ step=1, size=3,
+ subtype='XYZ',
+ precision=3
+ )
+ use_axis_coord = BoolVectorProperty(
+ name="Axis",
+ description="Choose Custom Coordinates axis",
+ default=(False,) * 3,
+ size=3,
+ )
+ is_not_undo = False
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ return (obj and obj.type == 'MESH')
+
+ def using_store(self, context):
+ scene = context.scene
+ return scene.mesh_extra_tools.vert_align_use_stored
+
+ def draw(self, context):
+ layout = self.layout
+
+ if self.using_store(context) and self.is_not_undo:
+ layout.label(text="Using Stored Coordinates", icon="INFO")
+
+ row = layout.split(0.25)
+ row.prop(self, "use_axis_coord", index=0, text="X")
+ row.prop(self, "def_axis_coord", index=0)
+
+ row = layout.split(0.25)
+ row.prop(self, "use_axis_coord", index=1, text="Y")
+ row.prop(self, "def_axis_coord", index=1)
+
+ row = layout.split(0.25)
+ row.prop(self, "use_axis_coord", index=2, text="Z")
+ row.prop(self, "def_axis_coord", index=2)
+
+ def invoke(self, context, event):
+ self.is_not_undo = True
+ scene = context.scene
+ if self.using_store(context):
+ self.def_axis_coord = scene.mesh_extra_tools.vert_align_store_axis
+
+ return context.window_manager.invoke_props_dialog(self, width=200)
+
+ def execute(self, context):
+ self.is_not_undo = False
+ edit_mode_out()
+ ob_act = context.active_object
+ me = ob_act.data
+
+ for i in va_buf.list_0:
+ v = (me.vertices[i].co).copy()
+ tmp = Vector((v[0], v[1], v[2]))
+
+ if self.use_axis_coord[0] is True:
+ tmp[0] = self.def_axis_coord[0]
+ if self.use_axis_coord[1] is True:
+ tmp[1] = self.def_axis_coord[1]
+ if self.use_axis_coord[2] is True:
+ tmp[2] = self.def_axis_coord[2]
+ me.vertices[i].co = tmp
+
+ edit_mode_in()
+
+ return {'FINISHED'}
+
+
+# Register
+classes = (
+ Vertex_align_store,
+ Vertex_align_original,
+ Vertex_align_coord_list,
+ Vertex_align_coord_menu,
+ )
+
+
+def register():
+ for cls in classes:
+ bpy.utils.register_class(cls)
+
+
+def unregister():
+ for cls in classes:
+ bpy.utils.unregister_class(cls)
+
+
+if __name__ == "__main__":
+ register()