From 682d48cefcc35a6784d23c59b5d3333ffb1415c4 Mon Sep 17 00:00:00 2001 From: meta-androcto Date: Sat, 15 Jun 2019 14:06:26 +1000 Subject: mesh_tools: restore to release: T63750 9e99e90f08c9 --- mesh_tools/__init__.py | 1165 ++++++++++++++++++++ mesh_tools/face_inset_fillet.py | 335 ++++++ mesh_tools/mesh_cut_faces.py | 241 ++++ mesh_tools/mesh_edge_roundifier.py | 1397 ++++++++++++++++++++++++ mesh_tools/mesh_edges_floor_plan.py | 384 +++++++ mesh_tools/mesh_edges_length.py | 341 ++++++ mesh_tools/mesh_edgetools.py | 1880 ++++++++++++++++++++++++++++++++ mesh_tools/mesh_extrude_and_reshape.py | 377 +++++++ mesh_tools/mesh_filletplus.py | 430 ++++++++ mesh_tools/mesh_mextrude_plus.py | 370 +++++++ mesh_tools/mesh_offset_edges.py | 791 ++++++++++++++ mesh_tools/mesh_vertex_chamfer.py | 164 +++ mesh_tools/pkhg_faces.py | 842 ++++++++++++++ mesh_tools/random_vertices.py | 140 +++ mesh_tools/split_solidify.py | 203 ++++ mesh_tools/vertex_align.py | 301 +++++ 16 files changed, 9361 insertions(+) create mode 100644 mesh_tools/__init__.py create mode 100644 mesh_tools/face_inset_fillet.py create mode 100644 mesh_tools/mesh_cut_faces.py create mode 100644 mesh_tools/mesh_edge_roundifier.py create mode 100644 mesh_tools/mesh_edges_floor_plan.py create mode 100644 mesh_tools/mesh_edges_length.py create mode 100644 mesh_tools/mesh_edgetools.py create mode 100644 mesh_tools/mesh_extrude_and_reshape.py create mode 100644 mesh_tools/mesh_filletplus.py create mode 100644 mesh_tools/mesh_mextrude_plus.py create mode 100644 mesh_tools/mesh_offset_edges.py create mode 100644 mesh_tools/mesh_vertex_chamfer.py create mode 100644 mesh_tools/pkhg_faces.py create mode 100644 mesh_tools/random_vertices.py create mode 100644 mesh_tools/split_solidify.py create mode 100644 mesh_tools/vertex_align.py (limited to 'mesh_tools') 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 :) + + # 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 . +# +# ##### END GPL LICENSE BLOCK ##### + +# + +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 . +# +# ##### 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 ##### + +# + +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() -- cgit v1.2.3