mirror of
https://github.com/blender/blender-addons.git
synced 2025-08-16 15:35:05 +00:00
432 lines
13 KiB
Python
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()
|