Fix #105067: Node Wrangler: cannot preview multi-user material group
[blender-addons.git] / mesh_tools / face_inset_fillet.py
blobdbf1c99ae77364a5bc30b607e81613003af4d5d8
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # based completely on addon by zmj100
6 # added some distance limits to prevent overlap - max12345
9 import bpy
10 import bmesh
11 from bpy.types import Operator
12 from bpy.props import (
13 FloatProperty,
14 IntProperty,
15 BoolProperty,
16 EnumProperty,
18 from math import (
19 sin, cos, tan,
20 degrees, radians,
22 from mathutils import Matrix
25 def edit_mode_out():
26 bpy.ops.object.mode_set(mode='OBJECT')
29 def edit_mode_in():
30 bpy.ops.object.mode_set(mode='EDIT')
33 def angle_rotation(rp, q, axis, angle):
34 # returns the vector made by the rotation of the vector q
35 # rp by angle around axis and then adds rp
37 return (Matrix.Rotation(angle, 3, axis) * (q - rp)) + rp
40 def face_inset_fillet(bme, face_index_list, inset_amount, distance,
41 number_of_sides, out, radius, type_enum, kp):
42 list_del = []
44 for faceindex in face_index_list:
46 bme.faces.ensure_lookup_table()
47 # loops through the faces...
48 f = bme.faces[faceindex]
49 f.select_set(False)
50 list_del.append(f)
51 f.normal_update()
52 vertex_index_list = [v.index for v in f.verts]
53 dict_0 = {}
54 orientation_vertex_list = []
55 n = len(vertex_index_list)
56 for i in range(n):
57 # loops through the vertices
58 dict_0[i] = []
59 bme.verts.ensure_lookup_table()
60 p = (bme.verts[vertex_index_list[i]].co).copy()
61 p1 = (bme.verts[vertex_index_list[(i - 1) % n]].co).copy()
62 p2 = (bme.verts[vertex_index_list[(i + 1) % n]].co).copy()
63 # copies some vert coordinates, always the 3 around i
64 dict_0[i].append(bme.verts[vertex_index_list[i]])
65 # appends the bmesh vert of the appropriate index to the dict
66 vec1 = p - p1
67 vec2 = p - p2
68 # vectors for the other corner points to the cornerpoint
69 # corresponding to i / p
70 angle = vec1.angle(vec2)
72 adj = inset_amount / tan(angle * 0.5)
73 h = (adj ** 2 + inset_amount ** 2) ** 0.5
74 if round(degrees(angle)) == 180 or round(degrees(angle)) == 0.0:
75 # if the corner is a straight line...
76 # I think this creates some new points...
77 if out is True:
78 val = ((f.normal).normalized() * inset_amount)
79 else:
80 val = -((f.normal).normalized() * inset_amount)
81 p6 = angle_rotation(p, p + val, vec1, radians(90))
82 else:
83 # if the corner is an actual corner
84 val = ((f.normal).normalized() * h)
85 if out is True:
86 # this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik...
87 p6 = angle_rotation(
88 p, p + val,
89 -(p - (vec2.normalized() * adj)),
90 -radians(90)
92 else:
93 p6 = angle_rotation(
94 p, p - val,
95 ((p - (vec1.normalized() * adj)) - (p - (vec2.normalized() * adj))),
96 -radians(90)
99 orientation_vertex_list.append(p6)
101 new_inner_face = []
102 orientation_vertex_list_length = len(orientation_vertex_list)
103 ovll = orientation_vertex_list_length
105 for j in range(ovll):
106 q = orientation_vertex_list[j]
107 q1 = orientation_vertex_list[(j - 1) % ovll]
108 q2 = orientation_vertex_list[(j + 1) % ovll]
109 # again, these are just vectors between somewhat displaced corner vertices
110 vec1_ = q - q1
111 vec2_ = q - q2
112 ang_ = vec1_.angle(vec2_)
114 # the angle between them
115 if round(degrees(ang_)) == 180 or round(degrees(ang_)) == 0.0:
116 # again... if it's really a line...
117 v = bme.verts.new(q)
118 new_inner_face.append(v)
119 dict_0[j].append(v)
120 else:
121 # s.a.
122 if radius is False:
123 h_ = distance * (1 / cos(ang_ * 0.5))
124 d = distance
125 elif radius is True:
126 h_ = distance / sin(ang_ * 0.5)
127 d = distance / tan(ang_ * 0.5)
128 # max(d) is vec1_.magnitude * 0.5
129 # or vec2_.magnitude * 0.5 respectively
131 # only functional difference v
132 if d > vec1_.magnitude * 0.5:
133 d = vec1_.magnitude * 0.5
135 if d > vec2_.magnitude * 0.5:
136 d = vec2_.magnitude * 0.5
137 # only functional difference ^
139 q3 = q - (vec1_.normalized() * d)
140 q4 = q - (vec2_.normalized() * d)
141 # these are new verts somewhat offset from the corners
142 rp_ = q - ((q - ((q3 + q4) * 0.5)).normalized() * h_)
143 # reference point inside the curvature
144 axis_ = vec1_.cross(vec2_)
145 # this should really be just the face normal
146 vec3_ = rp_ - q3
147 vec4_ = rp_ - q4
148 rot_ang = vec3_.angle(vec4_)
149 cornerverts = []
151 for o in range(number_of_sides + 1):
152 # this calculates the actual new vertices
153 q5 = angle_rotation(rp_, q4, axis_, rot_ang * o / number_of_sides)
154 v = bme.verts.new(q5)
156 # creates new bmesh vertices from it
157 bme.verts.index_update()
159 dict_0[j].append(v)
160 cornerverts.append(v)
162 cornerverts.reverse()
163 new_inner_face.extend(cornerverts)
165 if out is False:
166 f = bme.faces.new(new_inner_face)
167 f.select_set(True)
168 elif out is True and kp is True:
169 f = bme.faces.new(new_inner_face)
170 f.select_set(True)
172 n2_ = len(dict_0)
173 # these are the new side faces, those that don't depend on cornertype
174 for o in range(n2_):
175 list_a = dict_0[o]
176 list_b = dict_0[(o + 1) % n2_]
177 bme.faces.new([list_a[0], list_b[0], list_b[-1], list_a[1]])
178 bme.faces.index_update()
179 # cornertype 1 - ngon faces
180 if type_enum == 'opt0':
181 for k in dict_0:
182 if len(dict_0[k]) > 2:
183 bme.faces.new(dict_0[k])
184 bme.faces.index_update()
185 # cornertype 2 - triangulated faces
186 if type_enum == 'opt1':
187 for k_ in dict_0:
188 q_ = dict_0[k_][0]
189 dict_0[k_].pop(0)
190 n3_ = len(dict_0[k_])
191 for kk in range(n3_ - 1):
192 bme.faces.new([dict_0[k_][kk], dict_0[k_][(kk + 1) % n3_], q_])
193 bme.faces.index_update()
195 del_ = [bme.faces.remove(f) for f in list_del]
197 if del_:
198 del del_
201 # Operator
203 class MESH_OT_face_inset_fillet(Operator):
204 bl_idname = "mesh.face_inset_fillet"
205 bl_label = "Face Inset Fillet"
206 bl_description = ("Inset selected and Fillet (make round) the corners \n"
207 "of the newly created Faces")
208 bl_options = {"REGISTER", "UNDO"}
210 # inset amount
211 inset_amount: FloatProperty(
212 name="Inset amount",
213 description="Define the size of the Inset relative to the selection",
214 default=0.04,
215 min=0, max=100.0,
216 step=1,
217 precision=3
219 # number of sides
220 number_of_sides: IntProperty(
221 name="Number of sides",
222 description="Define the roundness of the corners by specifying\n"
223 "the subdivision count",
224 default=4,
225 min=1, max=100,
226 step=1
228 distance: FloatProperty(
229 name="",
230 description="Use distance or radius for corners' size calculation",
231 default=0.04,
232 min=0.00001, max=100.0,
233 step=1,
234 precision=3
236 out: BoolProperty(
237 name="Outside",
238 description="Inset the Faces outwards in relation to the selection\n"
239 "Note: depending on the geometry, can give unsatisfactory results",
240 default=False
242 radius: BoolProperty(
243 name="Radius",
244 description="Use radius for corners' size calculation",
245 default=False
247 type_enum: EnumProperty(
248 items=(('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
249 ('opt1', "Triangle", "Triangulate corners")),
250 name="Corner Type",
251 default="opt0"
253 kp: BoolProperty(
254 name="Keep faces",
255 description="Do not delete the inside Faces\n"
256 "Only available if the Out option is checked",
257 default=False
260 def draw(self, context):
261 layout = self.layout
263 layout.label(text="Corner Type:")
265 row = layout.row()
266 row.prop(self, "type_enum", text="")
268 row = layout.row(align=True)
269 row.prop(self, "out")
271 if self.out is True:
272 row.prop(self, "kp")
274 row = layout.row()
275 row.prop(self, "inset_amount")
277 row = layout.row()
278 row.prop(self, "number_of_sides")
280 row = layout.row()
281 row.prop(self, "radius")
283 row = layout.row()
284 dist_rad = "Radius" if self.radius else "Distance"
285 row.prop(self, "distance", text=dist_rad)
287 def execute(self, context):
288 # this really just prepares everything for the main function
289 inset_amount = self.inset_amount
290 number_of_sides = self.number_of_sides
291 distance = self.distance
292 out = self.out
293 radius = self.radius
294 type_enum = self.type_enum
295 kp = self.kp
297 edit_mode_out()
298 ob_act = context.active_object
299 bme = bmesh.new()
300 bme.from_mesh(ob_act.data)
301 # this
302 face_index_list = [f.index for f in bme.faces if f.select and f.is_valid]
304 if len(face_index_list) == 0:
305 self.report({'WARNING'},
306 "No suitable Face selection found. Operation cancelled")
307 edit_mode_in()
309 return {'CANCELLED'}
311 elif len(face_index_list) != 0:
312 face_inset_fillet(bme, face_index_list,
313 inset_amount, distance, number_of_sides,
314 out, radius, type_enum, kp)
316 bme.to_mesh(ob_act.data)
317 edit_mode_in()
319 return {'FINISHED'}