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
11 from bpy
.types
import Operator
12 from bpy
.props
import (
22 from mathutils
import Matrix
26 bpy
.ops
.object.mode_set(mode
='OBJECT')
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
):
44 for faceindex
in face_index_list
:
46 bme
.faces
.ensure_lookup_table()
47 # loops through the faces...
48 f
= bme
.faces
[faceindex
]
52 vertex_index_list
= [v
.index
for v
in f
.verts
]
54 orientation_vertex_list
= []
55 n
= len(vertex_index_list
)
57 # loops through the vertices
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
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...
78 val
= ((f
.normal
).normalized() * inset_amount
)
80 val
= -((f
.normal
).normalized() * inset_amount
)
81 p6
= angle_rotation(p
, p
+ val
, vec1
, radians(90))
83 # if the corner is an actual corner
84 val
= ((f
.normal
).normalized() * h
)
86 # this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik...
89 -(p
- (vec2
.normalized() * adj
)),
95 ((p
- (vec1
.normalized() * adj
)) - (p
- (vec2
.normalized() * adj
))),
99 orientation_vertex_list
.append(p6
)
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
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...
118 new_inner_face
.append(v
)
123 h_
= distance
* (1 / cos(ang_
* 0.5))
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
148 rot_ang
= vec3_
.angle(vec4_
)
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()
160 cornerverts
.append(v
)
162 cornerverts
.reverse()
163 new_inner_face
.extend(cornerverts
)
166 f
= bme
.faces
.new(new_inner_face
)
168 elif out
is True and kp
is True:
169 f
= bme
.faces
.new(new_inner_face
)
173 # these are the new side faces, those that don't depend on cornertype
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':
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':
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
]
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"}
211 inset_amount
: FloatProperty(
213 description
="Define the size of the Inset relative to the selection",
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",
228 distance
: FloatProperty(
230 description
="Use distance or radius for corners' size calculation",
232 min=0.00001, max=100.0,
238 description
="Inset the Faces outwards in relation to the selection\n"
239 "Note: depending on the geometry, can give unsatisfactory results",
242 radius
: BoolProperty(
244 description
="Use radius for corners' size calculation",
247 type_enum
: EnumProperty(
248 items
=(('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
249 ('opt1', "Triangle", "Triangulate corners")),
255 description
="Do not delete the inside Faces\n"
256 "Only available if the Out option is checked",
260 def draw(self
, context
):
263 layout
.label(text
="Corner Type:")
266 row
.prop(self
, "type_enum", text
="")
268 row
= layout
.row(align
=True)
269 row
.prop(self
, "out")
275 row
.prop(self
, "inset_amount")
278 row
.prop(self
, "number_of_sides")
281 row
.prop(self
, "radius")
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
294 type_enum
= self
.type_enum
298 ob_act
= context
.active_object
300 bme
.from_mesh(ob_act
.data
)
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")
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
)