Fix T62695: OBJ mtllib fails when filename contains spaces
[blender-addons.git] / mesh_extra_tools / mesh_filletplus.py
bloba54eba7b2b7cbdb4b4d3b761fd5457b69313a729
1 # -*- coding: utf-8 -*-
3 # ##### END GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
21 bl_info = {
22 "name": "FilletPlus",
23 "author": "Gert De Roost - original by zmj100",
24 "version": (0, 4, 3),
25 "blender": (2, 61, 0),
26 "location": "View3D > Tool Shelf",
27 "description": "",
28 "warning": "",
29 "wiki_url": "",
30 "category": "Mesh"}
33 import bpy
34 from bpy.props import (
35 FloatProperty,
36 IntProperty,
37 BoolProperty,
39 from bpy.types import Operator
40 import bmesh
41 from mathutils import Matrix
42 from math import (
43 cos, pi, sin,
44 degrees, tan,
48 def list_clear_(l):
49 if l:
50 del l[:]
51 return l
54 def get_adj_v_(list_):
55 tmp = {}
56 for i in list_:
57 try:
58 tmp[i[0]].append(i[1])
59 except KeyError:
60 tmp[i[0]] = [i[1]]
61 try:
62 tmp[i[1]].append(i[0])
63 except KeyError:
64 tmp[i[1]] = [i[0]]
65 return tmp
68 class f_buf():
69 # one of the angles was not 0 or 180
70 check = False
73 def fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius):
74 try:
75 dict_0 = get_adj_v_(list_0)
76 list_1 = [[dict_0[i][0], i, dict_0[i][1]] for i in dict_0 if (len(dict_0[i]) == 2)][0]
77 list_3 = []
78 for elem in list_1:
79 list_3.append(bm.verts[elem])
80 list_2 = []
82 p_ = list_3[1]
83 p = (list_3[1].co).copy()
84 p1 = (list_3[0].co).copy()
85 p2 = (list_3[2].co).copy()
87 vec1 = p - p1
88 vec2 = p - p2
90 ang = vec1.angle(vec2, any)
91 check_angle = round(degrees(ang))
93 if check_angle == 180 or check_angle == 0.0:
94 return False
95 else:
96 f_buf.check = True
98 opp = adj
100 if radius is False:
101 h = adj * (1 / cos(ang * 0.5))
102 adj_ = adj
103 elif radius is True:
104 h = opp / sin(ang * 0.5)
105 adj_ = opp / tan(ang * 0.5)
107 p3 = p - (vec1.normalized() * adj_)
108 p4 = p - (vec2.normalized() * adj_)
109 rp = p - ((p - ((p3 + p4) * 0.5)).normalized() * h)
111 vec3 = rp - p3
112 vec4 = rp - p4
114 axis = vec1.cross(vec2)
116 if out is False:
117 if flip is False:
118 rot_ang = vec3.angle(vec4)
119 elif flip is True:
120 rot_ang = vec1.angle(vec2)
121 elif out is True:
122 rot_ang = (2 * pi) - vec1.angle(vec2)
124 for j in range(n + 1):
125 new_angle = rot_ang * j / n
126 mtrx = Matrix.Rotation(new_angle, 3, axis)
127 if out is False:
128 if flip is False:
129 tmp = p4 - rp
130 tmp1 = mtrx * tmp
131 tmp2 = tmp1 + rp
132 elif flip is True:
133 p3 = p - (vec1.normalized() * opp)
134 tmp = p3 - p
135 tmp1 = mtrx * tmp
136 tmp2 = tmp1 + p
137 elif out is True:
138 p4 = p - (vec2.normalized() * opp)
139 tmp = p4 - p
140 tmp1 = mtrx * tmp
141 tmp2 = tmp1 + p
143 v = bm.verts.new(tmp2)
144 list_2.append(v)
146 if flip is True:
147 list_3[1:2] = list_2
148 else:
149 list_2.reverse()
150 list_3[1:2] = list_2
152 list_clear_(list_2)
154 n1 = len(list_3)
156 for t in range(n1 - 1):
157 bm.edges.new([list_3[t], list_3[(t + 1) % n1]])
159 v = bm.verts.new(p)
160 bm.edges.new([v, p_])
162 bm.edges.ensure_lookup_table()
164 if face is not None:
165 for l in face.loops:
166 if l.vert == list_3[0]:
167 startl = l
168 break
169 vertlist2 = []
171 if startl.link_loop_next.vert == startv:
172 l = startl.link_loop_prev
173 while len(vertlist) > 0:
174 vertlist2.insert(0, l.vert)
175 vertlist.pop(vertlist.index(l.vert))
176 l = l.link_loop_prev
177 else:
178 l = startl.link_loop_next
179 while len(vertlist) > 0:
180 vertlist2.insert(0, l.vert)
181 vertlist.pop(vertlist.index(l.vert))
182 l = l.link_loop_next
184 for v in list_3:
185 vertlist2.append(v)
186 bm.faces.new(vertlist2)
187 if startv.is_valid:
188 bm.verts.remove(startv)
189 else:
190 print("\n[Function fillets Error]\n"
191 "Starting vertex (startv var) couldn't be removed\n")
192 return False
193 bm.verts.ensure_lookup_table()
194 bm.edges.ensure_lookup_table()
195 bm.faces.ensure_lookup_table()
196 list_3[1].select = 1
197 list_3[-2].select = 1
198 bm.edges.get([list_3[0], list_3[1]]).select = 1
199 bm.edges.get([list_3[-1], list_3[-2]]).select = 1
200 bm.verts.index_update()
201 bm.edges.index_update()
202 bm.faces.index_update()
204 me.update(calc_edges=True, calc_loop_triangles=True)
205 bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
207 except Exception as e:
208 print("\n[Function fillets Error]\n{}\n".format(e))
209 return False
212 def do_filletplus(self, pair):
213 is_finished = True
214 try:
215 startv = None
216 global inaction
217 global flip
218 list_0 = [list([e.verts[0].index, e.verts[1].index]) for e in pair]
220 vertset = set([])
221 bm.verts.ensure_lookup_table()
222 bm.edges.ensure_lookup_table()
223 bm.faces.ensure_lookup_table()
224 vertset.add(bm.verts[list_0[0][0]])
225 vertset.add(bm.verts[list_0[0][1]])
226 vertset.add(bm.verts[list_0[1][0]])
227 vertset.add(bm.verts[list_0[1][1]])
229 v1, v2, v3 = vertset
231 if len(list_0) != 2:
232 self.report({'WARNING'}, "Two adjacent edges must be selected")
233 is_finished = False
234 else:
235 inaction = 1
236 vertlist = []
237 found = 0
238 for f in v1.link_faces:
239 if v2 in f.verts and v3 in f.verts:
240 found = 1
241 if not found:
242 for v in [v1, v2, v3]:
243 if v.index in list_0[0] and v.index in list_0[1]:
244 startv = v
245 face = None
246 else:
247 for f in v1.link_faces:
248 if v2 in f.verts and v3 in f.verts:
249 for v in f.verts:
250 if not(v in vertset):
251 vertlist.append(v)
252 if (v in vertset and v.link_loops[0].link_loop_prev.vert in vertset and
253 v.link_loops[0].link_loop_next.vert in vertset):
254 startv = v
255 face = f
256 if out is True:
257 flip = False
258 if startv:
259 fills = fillets(list_0, startv, vertlist, face, adj, n, out, flip, radius)
260 if not fills:
261 is_finished = False
262 else:
263 is_finished = False
264 except Exception as e:
265 print("\n[Function do_filletplus Error]\n{}\n".format(e))
266 is_finished = False
267 return is_finished
270 def check_is_not_coplanar(bm_data):
271 from mathutils import Vector
272 check = False
273 angles, norm_angle = 0, 0
274 z_vec = Vector((0, 0, 1))
275 try:
276 bm_data.faces.ensure_lookup_table()
278 for f in bm_data.faces:
279 norm_angle = f.normal.angle(z_vec)
280 if angles == 0:
281 angles = norm_angle
282 if angles != norm_angle:
283 check = True
284 break
285 except Exception as e:
286 print("\n[Function check_is_not_coplanar Error]\n{}\n".format(e))
287 check = True
288 return check
291 # Operator
293 class MESH_OT_fillet_plus(Operator):
294 bl_idname = "mesh.fillet_plus"
295 bl_label = "Fillet Plus"
296 bl_description = ("Fillet adjoining edges\n"
297 "Note: Works on a mesh whose all faces share the same normal")
298 bl_options = {"REGISTER", "UNDO"}
300 adj: FloatProperty(
301 name="",
302 description="Size of the filleted corners",
303 default=0.1,
304 min=0.00001, max=100.0,
305 step=1,
306 precision=3
308 n: IntProperty(
309 name="",
310 description="Subdivision of the filleted corners",
311 default=3,
312 min=1, max=50,
313 step=1
315 out: BoolProperty(
316 name="Outside",
317 description="Fillet towards outside",
318 default=False
320 flip: BoolProperty(
321 name="Flip",
322 description="Flip the direction of the Fillet\n"
323 "Only available if Outside option is not active",
324 default=False
326 radius: BoolProperty(
327 name="Radius",
328 description="Use radius for the size of the filleted corners",
329 default=False
332 @classmethod
333 def poll(cls, context):
334 obj = context.active_object
335 return (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH')
337 def draw(self, context):
338 layout = self.layout
340 if f_buf.check is False:
341 layout.label(text="Angle is equal to 0 or 180", icon="INFO")
342 layout.label(text="Can not fillet", icon="BLANK1")
343 else:
344 layout.prop(self, "radius")
345 if self.radius is True:
346 layout.label(text="Radius:")
347 elif self.radius is False:
348 layout.label(text="Distance:")
349 layout.prop(self, "adj")
350 layout.label(text="Number of sides:")
351 layout.prop(self, "n")
353 if self.n > 1:
354 row = layout.row(align=False)
355 row.prop(self, "out")
356 if self.out is False:
357 row.prop(self, "flip")
359 def execute(self, context):
360 global inaction
361 global bm, me, adj, n, out, flip, radius
363 adj = self.adj
364 n = self.n
365 out = self.out
366 flip = self.flip
367 radius = self.radius
369 inaction = 0
370 f_buf.check = False
372 ob_act = context.active_object
373 try:
374 me = ob_act.data
375 bm = bmesh.from_edit_mesh(me)
376 warn_obj = bool(check_is_not_coplanar(bm))
377 if warn_obj is False:
378 tempset = set([])
379 bm.verts.ensure_lookup_table()
380 bm.edges.ensure_lookup_table()
381 bm.faces.ensure_lookup_table()
382 for v in bm.verts:
383 if v.select and v.is_boundary:
384 tempset.add(v)
385 for v in tempset:
386 edgeset = set([])
387 for e in v.link_edges:
388 if e.select and e.is_boundary:
389 edgeset.add(e)
390 if len(edgeset) == 2:
391 is_finished = do_filletplus(self, edgeset)
392 if not is_finished:
393 break
395 if inaction == 1:
396 bpy.ops.mesh.select_all(action="DESELECT")
397 for v in bm.verts:
398 if len(v.link_edges) == 0:
399 bm.verts.remove(v)
400 bpy.ops.object.editmode_toggle()
401 bpy.ops.object.editmode_toggle()
402 else:
403 self.report({'WARNING'}, "Filletplus operation could not be performed")
404 return {'CANCELLED'}
405 else:
406 self.report({'WARNING'}, "Mesh is not a coplanar surface. Operation cancelled")
407 return {'CANCELLED'}
408 except:
409 self.report({'WARNING'}, "Filletplus operation could not be performed")
410 return {'CANCELLED'}
412 return {'FINISHED'}