Files
blender-addons/mesh_tools/mesh_filletplus.py
2020-03-05 12:14:43 +11:00

432 lines
13 KiB
Python

# -*- 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": "",
"doc_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()