Update for 2.8
[blender-addons.git] / mesh_extra_tools / face_inset_fillet.py
blob2f46115d0fb0e5ad6f5120dba2274940c1407b82
1 # -*- coding: utf-8 -*-
3 # ##### BEGIN 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 # based completely on addon by zmj100
22 # added some distance limits to prevent overlap - max12345
25 import bpy
26 import bmesh
27 from bpy.types import Operator
28 from bpy.props import (
29 FloatProperty,
30 IntProperty,
31 BoolProperty,
32 EnumProperty,
34 from math import (
35 sin, cos, tan,
36 degrees, radians,
38 from mathutils import Matrix
41 def edit_mode_out():
42 bpy.ops.object.mode_set(mode='OBJECT')
45 def edit_mode_in():
46 bpy.ops.object.mode_set(mode='EDIT')
49 def angle_rotation(rp, q, axis, angle):
50 # returns the vector made by the rotation of the vector q
51 # rp by angle around axis and then adds rp
53 return (Matrix.Rotation(angle, 3, axis) * (q - rp)) + rp
56 def face_inset_fillet(bme, face_index_list, inset_amount, distance,
57 number_of_sides, out, radius, type_enum, kp):
58 list_del = []
60 for faceindex in face_index_list:
62 bme.faces.ensure_lookup_table()
63 # loops through the faces...
64 f = bme.faces[faceindex]
65 f.select_set(0)
66 list_del.append(f)
67 f.normal_update()
68 vertex_index_list = [v.index for v in f.verts]
69 dict_0 = {}
70 orientation_vertex_list = []
71 n = len(vertex_index_list)
72 for i in range(n):
73 # loops through the vertices
74 dict_0[i] = []
75 bme.verts.ensure_lookup_table()
76 p = (bme.verts[vertex_index_list[i]].co).copy()
77 p1 = (bme.verts[vertex_index_list[(i - 1) % n]].co).copy()
78 p2 = (bme.verts[vertex_index_list[(i + 1) % n]].co).copy()
79 # copies some vert coordinates, always the 3 around i
80 dict_0[i].append(bme.verts[vertex_index_list[i]])
81 # appends the bmesh vert of the appropriate index to the dict
82 vec1 = p - p1
83 vec2 = p - p2
84 # vectors for the other corner points to the cornerpoint
85 # corresponding to i / p
86 angle = vec1.angle(vec2)
88 adj = inset_amount / tan(angle * 0.5)
89 h = (adj ** 2 + inset_amount ** 2) ** 0.5
90 if round(degrees(angle)) == 180 or round(degrees(angle)) == 0.0:
91 # if the corner is a straight line...
92 # I think this creates some new points...
93 if out is True:
94 val = ((f.normal).normalized() * inset_amount)
95 else:
96 val = -((f.normal).normalized() * inset_amount)
97 p6 = angle_rotation(p, p + val, vec1, radians(90))
98 else:
99 # if the corner is an actual corner
100 val = ((f.normal).normalized() * h)
101 if out is True:
102 # this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik...
103 p6 = angle_rotation(
104 p, p + val,
105 -(p - (vec2.normalized() * adj)),
106 -radians(90)
108 else:
109 p6 = angle_rotation(
110 p, p - val,
111 ((p - (vec1.normalized() * adj)) - (p - (vec2.normalized() * adj))),
112 -radians(90)
115 orientation_vertex_list.append(p6)
117 new_inner_face = []
118 orientation_vertex_list_length = len(orientation_vertex_list)
119 ovll = orientation_vertex_list_length
121 for j in range(ovll):
122 q = orientation_vertex_list[j]
123 q1 = orientation_vertex_list[(j - 1) % ovll]
124 q2 = orientation_vertex_list[(j + 1) % ovll]
125 # again, these are just vectors between somewhat displaced corner vertices
126 vec1_ = q - q1
127 vec2_ = q - q2
128 ang_ = vec1_.angle(vec2_)
130 # the angle between them
131 if round(degrees(ang_)) == 180 or round(degrees(ang_)) == 0.0:
132 # again... if it's really a line...
133 v = bme.verts.new(q)
134 new_inner_face.append(v)
135 dict_0[j].append(v)
136 else:
137 # s.a.
138 if radius is False:
139 h_ = distance * (1 / cos(ang_ * 0.5))
140 d = distance
141 elif radius is True:
142 h_ = distance / sin(ang_ * 0.5)
143 d = distance / tan(ang_ * 0.5)
144 # max(d) is vec1_.magnitude * 0.5
145 # or vec2_.magnitude * 0.5 respectively
147 # only functional difference v
148 if d > vec1_.magnitude * 0.5:
149 d = vec1_.magnitude * 0.5
151 if d > vec2_.magnitude * 0.5:
152 d = vec2_.magnitude * 0.5
153 # only functional difference ^
155 q3 = q - (vec1_.normalized() * d)
156 q4 = q - (vec2_.normalized() * d)
157 # these are new verts somewhat offset from the corners
158 rp_ = q - ((q - ((q3 + q4) * 0.5)).normalized() * h_)
159 # reference point inside the curvature
160 axis_ = vec1_.cross(vec2_)
161 # this should really be just the face normal
162 vec3_ = rp_ - q3
163 vec4_ = rp_ - q4
164 rot_ang = vec3_.angle(vec4_)
165 cornerverts = []
167 for o in range(number_of_sides + 1):
168 # this calculates the actual new vertices
169 q5 = angle_rotation(rp_, q4, axis_, rot_ang * o / number_of_sides)
170 v = bme.verts.new(q5)
172 # creates new bmesh vertices from it
173 bme.verts.index_update()
175 dict_0[j].append(v)
176 cornerverts.append(v)
178 cornerverts.reverse()
179 new_inner_face.extend(cornerverts)
181 if out is False:
182 f = bme.faces.new(new_inner_face)
183 f.select_set(True)
184 elif out is True and kp is True:
185 f = bme.faces.new(new_inner_face)
186 f.select_set(True)
188 n2_ = len(dict_0)
189 # these are the new side faces, those that don't depend on cornertype
190 for o in range(n2_):
191 list_a = dict_0[o]
192 list_b = dict_0[(o + 1) % n2_]
193 bme.faces.new([list_a[0], list_b[0], list_b[-1], list_a[1]])
194 bme.faces.index_update()
195 # cornertype 1 - ngon faces
196 if type_enum == 'opt0':
197 for k in dict_0:
198 if len(dict_0[k]) > 2:
199 bme.faces.new(dict_0[k])
200 bme.faces.index_update()
201 # cornertype 2 - triangulated faces
202 if type_enum == 'opt1':
203 for k_ in dict_0:
204 q_ = dict_0[k_][0]
205 dict_0[k_].pop(0)
206 n3_ = len(dict_0[k_])
207 for kk in range(n3_ - 1):
208 bme.faces.new([dict_0[k_][kk], dict_0[k_][(kk + 1) % n3_], q_])
209 bme.faces.index_update()
211 del_ = [bme.faces.remove(f) for f in list_del]
213 if del_:
214 del del_
217 # Operator
219 class MESH_OT_face_inset_fillet(Operator):
220 bl_idname = "mesh.face_inset_fillet"
221 bl_label = "Face Inset Fillet"
222 bl_description = ("Inset selected and Fillet (make round) the corners \n"
223 "of the newly created Faces")
224 bl_options = {"REGISTER", "UNDO"}
226 # inset amount
227 inset_amount = FloatProperty(
228 name="Inset amount",
229 description="Define the size of the Inset relative to the selection",
230 default=0.04,
231 min=0, max=100.0,
232 step=1,
233 precision=3
235 # number of sides
236 number_of_sides = IntProperty(
237 name="Number of sides",
238 description="Define the roundness of the corners by specifying\n"
239 "the subdivision count",
240 default=4,
241 min=1, max=100,
242 step=1
244 distance = FloatProperty(
245 name="",
246 description="Use distance or radius for corners' size calculation",
247 default=0.04,
248 min=0.00001, max=100.0,
249 step=1,
250 precision=3
252 out = BoolProperty(
253 name="Outside",
254 description="Inset the Faces outwards in relation to the selection\n"
255 "Note: depending on the geometry, can give unsatisfactory results",
256 default=False
258 radius = BoolProperty(
259 name="Radius",
260 description="Use radius for corners' size calculation",
261 default=False
263 type_enum = EnumProperty(
264 items=(('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
265 ('opt1', "Triangle", "Triangulate corners")),
266 name="Corner Type",
267 default="opt0"
269 kp = BoolProperty(
270 name="Keep faces",
271 description="Do not delete the inside Faces\n"
272 "Only available if the Out option is checked",
273 default=False
276 def draw(self, context):
277 layout = self.layout
279 layout.label("Corner Type:")
281 row = layout.row()
282 row.prop(self, "type_enum", text="")
284 row = layout.row(align=True)
285 row.prop(self, "out")
287 if self.out is True:
288 row.prop(self, "kp")
290 row = layout.row()
291 row.prop(self, "inset_amount")
293 row = layout.row()
294 row.prop(self, "number_of_sides")
296 row = layout.row()
297 row.prop(self, "radius")
299 row = layout.row()
300 dist_rad = "Radius" if self.radius else "Distance"
301 row.prop(self, "distance", text=dist_rad)
303 def execute(self, context):
304 # this really just prepares everything for the main function
305 inset_amount = self.inset_amount
306 number_of_sides = self.number_of_sides
307 distance = self.distance
308 out = self.out
309 radius = self.radius
310 type_enum = self.type_enum
311 kp = self.kp
313 edit_mode_out()
314 ob_act = context.active_object
315 bme = bmesh.new()
316 bme.from_mesh(ob_act.data)
317 # this
318 face_index_list = [f.index for f in bme.faces if f.select and f.is_valid]
320 if len(face_index_list) == 0:
321 self.report({'WARNING'},
322 "No suitable Face selection found. Operation cancelled")
323 edit_mode_in()
325 return {'CANCELLED'}
327 elif len(face_index_list) != 0:
328 face_inset_fillet(bme, face_index_list,
329 inset_amount, distance, number_of_sides,
330 out, radius, type_enum, kp)
332 bme.to_mesh(ob_act.data)
333 edit_mode_in()
335 return {'FINISHED'}