Merge branch 'blender-v4.0-release'
[blender-addons.git] / mesh_tools / mesh_filletplus.py
blobd4e41a9e6bb510305e4ba221ec284b46e03ef3cc
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name": "FilletPlus",
7 "author": "Gert De Roost - original by zmj100",
8 "version": (0, 4, 3),
9 "blender": (2, 80, 0),
10 "location": "View3D > Tool Shelf",
11 "description": "",
12 "warning": "",
13 "doc_url": "",
14 "category": "Mesh",
18 import bpy
19 from bpy.props import (
20 FloatProperty,
21 IntProperty,
22 BoolProperty,
24 from bpy.types import Operator
25 import bmesh
26 from mathutils import Matrix
27 from math import (
28 cos, pi, sin,
29 degrees, tan,
33 def list_clear_(l):
34 if l:
35 del l[:]
36 return l
39 def get_adj_v_(list_):
40 tmp = {}
41 for i in list_:
42 try:
43 tmp[i[0]].append(i[1])
44 except KeyError:
45 tmp[i[0]] = [i[1]]
46 try:
47 tmp[i[1]].append(i[0])
48 except KeyError:
49 tmp[i[1]] = [i[0]]
50 return tmp
52 class f_buf():
53 # one of the angles was not 0 or 180
54 check = False
57 def fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius):
58 try:
59 dict_0 = get_adj_v_(list_0)
60 list_1 = [[dict_0[i][0], i, dict_0[i][1]] for i in dict_0 if (len(dict_0[i]) == 2)][0]
61 list_3 = []
62 for elem in list_1:
63 list_3.append(bm.verts[elem])
64 list_2 = []
66 p_ = list_3[1]
67 p = (list_3[1].co).copy()
68 p1 = (list_3[0].co).copy()
69 p2 = (list_3[2].co).copy()
71 vec1 = p - p1
72 vec2 = p - p2
74 ang = vec1.angle(vec2, any)
75 check_angle = round(degrees(ang))
77 if check_angle == 180 or check_angle == 0.0:
78 return False
79 else:
80 f_buf.check = True
82 opp = adj
84 if radius is False:
85 h = adj * (1 / cos(ang * 0.5))
86 adj_ = adj
87 elif radius is True:
88 h = opp / sin(ang * 0.5)
89 adj_ = opp / tan(ang * 0.5)
91 p3 = p - (vec1.normalized() * adj_)
92 p4 = p - (vec2.normalized() * adj_)
93 rp = p - ((p - ((p3 + p4) * 0.5)).normalized() * h)
95 vec3 = rp - p3
96 vec4 = rp - p4
98 axis = vec1.cross(vec2)
100 if out is False:
101 if flip is False:
102 rot_ang = vec3.angle(vec4)
103 elif flip is True:
104 rot_ang = vec1.angle(vec2)
105 elif out is True:
106 rot_ang = (2 * pi) - vec1.angle(vec2)
108 for j in range(n + 1):
109 new_angle = rot_ang * j / n
110 mtrx = Matrix.Rotation(new_angle, 3, axis)
111 if out is False:
112 if flip is False:
113 tmp = p4 - rp
114 tmp1 = mtrx @ tmp
115 tmp2 = tmp1 + rp
116 elif flip is True:
117 p3 = p - (vec1.normalized() * opp)
118 tmp = p3 - p
119 tmp1 = mtrx @ tmp
120 tmp2 = tmp1 + p
121 elif out is True:
122 p4 = p - (vec2.normalized() * opp)
123 tmp = p4 - p
124 tmp1 = mtrx @ tmp
125 tmp2 = tmp1 + p
127 v = bm.verts.new(tmp2)
128 list_2.append(v)
130 if flip is True:
131 list_3[1:2] = list_2
132 else:
133 list_2.reverse()
134 list_3[1:2] = list_2
136 list_clear_(list_2)
138 n1 = len(list_3)
140 for t in range(n1 - 1):
141 bm.edges.new([list_3[t], list_3[(t + 1) % n1]])
143 v = bm.verts.new(p)
144 bm.edges.new([v, p_])
146 bm.edges.ensure_lookup_table()
148 if face is not None:
149 for l in face.loops:
150 if l.vert == list_3[0]:
151 startl = l
152 break
153 vertlist2 = []
155 if startl.link_loop_next.vert == startv:
156 l = startl.link_loop_prev
157 while len(vertlist) > 0:
158 vertlist2.insert(0, l.vert)
159 vertlist.pop(vertlist.index(l.vert))
160 l = l.link_loop_prev
161 else:
162 l = startl.link_loop_next
163 while len(vertlist) > 0:
164 vertlist2.insert(0, l.vert)
165 vertlist.pop(vertlist.index(l.vert))
166 l = l.link_loop_next
168 for v in list_3:
169 vertlist2.append(v)
170 bm.faces.new(vertlist2)
171 if startv.is_valid:
172 bm.verts.remove(startv)
173 else:
174 print("\n[Function fillets Error]\n"
175 "Starting vertex (startv var) couldn't be removed\n")
176 return False
177 bm.verts.ensure_lookup_table()
178 bm.edges.ensure_lookup_table()
179 bm.faces.ensure_lookup_table()
180 list_3[1].select = 1
181 list_3[-2].select = 1
182 bm.edges.get([list_3[0], list_3[1]]).select = 1
183 bm.edges.get([list_3[-1], list_3[-2]]).select = 1
184 bm.verts.index_update()
185 bm.edges.index_update()
186 bm.faces.index_update()
188 me.update(calc_edges=True, calc_loop_triangles=True)
189 bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
191 except Exception as e:
192 print("\n[Function fillets Error]\n{}\n".format(e))
193 return False
196 def do_filletplus(self, pair):
197 is_finished = True
198 try:
199 startv = None
200 global inaction
201 global flip
202 list_0 = [list([e.verts[0].index, e.verts[1].index]) for e in pair]
204 vertset = set([])
205 bm.verts.ensure_lookup_table()
206 bm.edges.ensure_lookup_table()
207 bm.faces.ensure_lookup_table()
208 vertset.add(bm.verts[list_0[0][0]])
209 vertset.add(bm.verts[list_0[0][1]])
210 vertset.add(bm.verts[list_0[1][0]])
211 vertset.add(bm.verts[list_0[1][1]])
213 v1, v2, v3 = vertset
215 if len(list_0) != 2:
216 self.report({'WARNING'}, "Two adjacent edges must be selected")
217 is_finished = False
218 else:
219 inaction = 1
220 vertlist = []
221 found = 0
222 for f in v1.link_faces:
223 if v2 in f.verts and v3 in f.verts:
224 found = 1
225 if not found:
226 for v in [v1, v2, v3]:
227 if v.index in list_0[0] and v.index in list_0[1]:
228 startv = v
229 face = None
230 else:
231 for f in v1.link_faces:
232 if v2 in f.verts and v3 in f.verts:
233 for v in f.verts:
234 if not(v in vertset):
235 vertlist.append(v)
236 if (v in vertset and v.link_loops[0].link_loop_prev.vert in vertset and
237 v.link_loops[0].link_loop_next.vert in vertset):
238 startv = v
239 face = f
240 if out is True:
241 flip = False
242 if startv:
243 fills = fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius)
244 if not fills:
245 is_finished = False
246 else:
247 is_finished = False
248 except Exception as e:
249 print("\n[Function do_filletplus Error]\n{}\n".format(e))
250 is_finished = False
251 return is_finished
254 def check_is_not_coplanar(bm_data):
255 from mathutils import Vector
256 check = False
257 angles, norm_angle = 0, 0
258 z_vec = Vector((0, 0, 1))
259 try:
260 bm_data.faces.ensure_lookup_table()
262 for f in bm_data.faces:
263 norm_angle = f.normal.angle(z_vec)
264 if angles == 0:
265 angles = norm_angle
266 if angles != norm_angle:
267 check = True
268 break
269 except Exception as e:
270 print("\n[Function check_is_not_coplanar Error]\n{}\n".format(e))
271 check = True
272 return check
275 # Operator
277 class MESH_OT_fillet_plus(Operator):
278 bl_idname = "mesh.fillet_plus"
279 bl_label = "Fillet Plus"
280 bl_description = ("Fillet adjoining edges\n"
281 "Note: Works on a mesh whose all faces share the same normal")
282 bl_options = {"REGISTER", "UNDO"}
284 adj: FloatProperty(
285 name="",
286 description="Size of the filleted corners",
287 default=0.1,
288 min=0.00001, max=100.0,
289 step=1,
290 precision=3
292 n: IntProperty(
293 name="",
294 description="Subdivision of the filleted corners",
295 default=3,
296 min=1, max=50,
297 step=1
299 out: BoolProperty(
300 name="Outside",
301 description="Fillet towards outside",
302 default=False
304 flip: BoolProperty(
305 name="Flip",
306 description="Flip the direction of the Fillet\n"
307 "Only available if Outside option is not active",
308 default=False
310 radius: BoolProperty(
311 name="Radius",
312 description="Use radius for the size of the filleted corners",
313 default=False
316 @classmethod
317 def poll(cls, context):
318 obj = context.active_object
319 return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
321 def draw(self, context):
322 layout = self.layout
324 if f_buf.check is False:
325 layout.label(text="Angle is equal to 0 or 180", icon="INFO")
326 layout.label(text="Can not fillet", icon="BLANK1")
327 else:
328 layout.prop(self, "radius")
329 if self.radius is True:
330 layout.label(text="Radius:")
331 elif self.radius is False:
332 layout.label(text="Distance:")
333 layout.prop(self, "adj")
334 layout.label(text="Number of sides:")
335 layout.prop(self, "n")
337 if self.n > 1:
338 row = layout.row(align=False)
339 row.prop(self, "out")
340 if self.out is False:
341 row.prop(self, "flip")
343 def execute(self, context):
344 global inaction
345 global bm, me, adj, n, out, flip, radius, f_buf
347 adj = self.adj
348 n = self.n
349 out = self.out
350 flip = self.flip
351 radius = self.radius
353 inaction = 0
354 f_buf.check = False
356 ob_act = context.active_object
357 try:
358 me = ob_act.data
359 bm = bmesh.from_edit_mesh(me)
360 warn_obj = bool(check_is_not_coplanar(bm))
361 if warn_obj is False:
362 tempset = set([])
363 bm.verts.ensure_lookup_table()
364 bm.edges.ensure_lookup_table()
365 bm.faces.ensure_lookup_table()
366 for v in bm.verts:
367 if v.select and v.is_boundary:
368 tempset.add(v)
369 for v in tempset:
370 edgeset = set([])
371 for e in v.link_edges:
372 if e.select and e.is_boundary:
373 edgeset.add(e)
374 if len(edgeset) == 2:
375 is_finished = do_filletplus(self, edgeset)
376 if not is_finished:
377 break
379 if inaction == 1:
380 bpy.ops.mesh.select_all(action="DESELECT")
381 for v in bm.verts:
382 if len(v.link_edges) == 0:
383 bm.verts.remove(v)
384 bpy.ops.object.editmode_toggle()
385 bpy.ops.object.editmode_toggle()
386 else:
387 self.report({'WARNING'}, "Filletplus operation could not be performed")
388 return {'CANCELLED'}
389 else:
390 self.report({'WARNING'}, "Mesh is not a coplanar surface. Operation cancelled")
391 return {'CANCELLED'}
392 except:
393 self.report({'WARNING'}, "Filletplus operation could not be performed")
394 return {'CANCELLED'}
396 return {'FINISHED'}
398 # define classes for registration
399 classes = (
400 MESH_OT_fillet_plus,
403 # registering and menu integration
404 def register():
405 for cls in classes:
406 bpy.utils.register_class(cls)
409 # unregistering and removing menus
410 def unregister():
411 for cls in reversed(classes):
412 bpy.utils.unregister_class(cls)
414 if __name__ == "__main__":
415 register()