mirror of
https://github.com/blender/blender-addons.git
synced 2025-08-16 15:35:05 +00:00
318 lines
11 KiB
Python
318 lines
11 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
# based completely on addon by zmj100
|
|
# added some distance limits to prevent overlap - max12345
|
|
|
|
|
|
import bpy
|
|
import bmesh
|
|
from bpy.types import Operator
|
|
from bpy.props import (
|
|
FloatProperty,
|
|
IntProperty,
|
|
BoolProperty,
|
|
EnumProperty,
|
|
)
|
|
from math import (
|
|
sin, cos, tan,
|
|
degrees, radians,
|
|
)
|
|
from mathutils import Matrix
|
|
|
|
|
|
def edit_mode_out():
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
|
|
def edit_mode_in():
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
|
|
def angle_rotation(rp, q, axis, angle):
|
|
# returns the vector made by the rotation of the vector q
|
|
# rp by angle around axis and then adds rp
|
|
|
|
return (Matrix.Rotation(angle, 3, axis) * (q - rp)) + rp
|
|
|
|
|
|
def face_inset_fillet(bme, face_index_list, inset_amount, distance,
|
|
number_of_sides, out, radius, type_enum, kp):
|
|
list_del = []
|
|
|
|
for faceindex in face_index_list:
|
|
|
|
bme.faces.ensure_lookup_table()
|
|
# loops through the faces...
|
|
f = bme.faces[faceindex]
|
|
f.select_set(False)
|
|
list_del.append(f)
|
|
f.normal_update()
|
|
vertex_index_list = [v.index for v in f.verts]
|
|
dict_0 = {}
|
|
orientation_vertex_list = []
|
|
n = len(vertex_index_list)
|
|
for i in range(n):
|
|
# loops through the vertices
|
|
dict_0[i] = []
|
|
bme.verts.ensure_lookup_table()
|
|
p = (bme.verts[vertex_index_list[i]].co).copy()
|
|
p1 = (bme.verts[vertex_index_list[(i - 1) % n]].co).copy()
|
|
p2 = (bme.verts[vertex_index_list[(i + 1) % n]].co).copy()
|
|
# copies some vert coordinates, always the 3 around i
|
|
dict_0[i].append(bme.verts[vertex_index_list[i]])
|
|
# appends the bmesh vert of the appropriate index to the dict
|
|
vec1 = p - p1
|
|
vec2 = p - p2
|
|
# vectors for the other corner points to the cornerpoint
|
|
# corresponding to i / p
|
|
angle = vec1.angle(vec2)
|
|
|
|
adj = inset_amount / tan(angle * 0.5)
|
|
h = (adj ** 2 + inset_amount ** 2) ** 0.5
|
|
if round(degrees(angle)) == 180 or round(degrees(angle)) == 0.0:
|
|
# if the corner is a straight line...
|
|
# I think this creates some new points...
|
|
if out is True:
|
|
val = ((f.normal).normalized() * inset_amount)
|
|
else:
|
|
val = -((f.normal).normalized() * inset_amount)
|
|
p6 = angle_rotation(p, p + val, vec1, radians(90))
|
|
else:
|
|
# if the corner is an actual corner
|
|
val = ((f.normal).normalized() * h)
|
|
if out is True:
|
|
# this -(p - (vec2.normalized() * adj))) is just the freaking axis afaik...
|
|
p6 = angle_rotation(
|
|
p, p + val,
|
|
-(p - (vec2.normalized() * adj)),
|
|
-radians(90)
|
|
)
|
|
else:
|
|
p6 = angle_rotation(
|
|
p, p - val,
|
|
((p - (vec1.normalized() * adj)) - (p - (vec2.normalized() * adj))),
|
|
-radians(90)
|
|
)
|
|
|
|
orientation_vertex_list.append(p6)
|
|
|
|
new_inner_face = []
|
|
orientation_vertex_list_length = len(orientation_vertex_list)
|
|
ovll = orientation_vertex_list_length
|
|
|
|
for j in range(ovll):
|
|
q = orientation_vertex_list[j]
|
|
q1 = orientation_vertex_list[(j - 1) % ovll]
|
|
q2 = orientation_vertex_list[(j + 1) % ovll]
|
|
# again, these are just vectors between somewhat displaced corner vertices
|
|
vec1_ = q - q1
|
|
vec2_ = q - q2
|
|
ang_ = vec1_.angle(vec2_)
|
|
|
|
# the angle between them
|
|
if round(degrees(ang_)) == 180 or round(degrees(ang_)) == 0.0:
|
|
# again... if it's really a line...
|
|
v = bme.verts.new(q)
|
|
new_inner_face.append(v)
|
|
dict_0[j].append(v)
|
|
else:
|
|
# s.a.
|
|
if radius is False:
|
|
h_ = distance * (1 / cos(ang_ * 0.5))
|
|
d = distance
|
|
elif radius is True:
|
|
h_ = distance / sin(ang_ * 0.5)
|
|
d = distance / tan(ang_ * 0.5)
|
|
# max(d) is vec1_.magnitude * 0.5
|
|
# or vec2_.magnitude * 0.5 respectively
|
|
|
|
# only functional difference v
|
|
if d > vec1_.magnitude * 0.5:
|
|
d = vec1_.magnitude * 0.5
|
|
|
|
if d > vec2_.magnitude * 0.5:
|
|
d = vec2_.magnitude * 0.5
|
|
# only functional difference ^
|
|
|
|
q3 = q - (vec1_.normalized() * d)
|
|
q4 = q - (vec2_.normalized() * d)
|
|
# these are new verts somewhat offset from the corners
|
|
rp_ = q - ((q - ((q3 + q4) * 0.5)).normalized() * h_)
|
|
# reference point inside the curvature
|
|
axis_ = vec1_.cross(vec2_)
|
|
# this should really be just the face normal
|
|
vec3_ = rp_ - q3
|
|
vec4_ = rp_ - q4
|
|
rot_ang = vec3_.angle(vec4_)
|
|
cornerverts = []
|
|
|
|
for o in range(number_of_sides + 1):
|
|
# this calculates the actual new vertices
|
|
q5 = angle_rotation(rp_, q4, axis_, rot_ang * o / number_of_sides)
|
|
v = bme.verts.new(q5)
|
|
|
|
# creates new bmesh vertices from it
|
|
bme.verts.index_update()
|
|
|
|
dict_0[j].append(v)
|
|
cornerverts.append(v)
|
|
|
|
cornerverts.reverse()
|
|
new_inner_face.extend(cornerverts)
|
|
|
|
if out is False:
|
|
f = bme.faces.new(new_inner_face)
|
|
f.select_set(True)
|
|
elif out is True and kp is True:
|
|
f = bme.faces.new(new_inner_face)
|
|
f.select_set(True)
|
|
|
|
n2_ = len(dict_0)
|
|
# these are the new side faces, those that don't depend on cornertype
|
|
for o in range(n2_):
|
|
list_a = dict_0[o]
|
|
list_b = dict_0[(o + 1) % n2_]
|
|
bme.faces.new([list_a[0], list_b[0], list_b[-1], list_a[1]])
|
|
bme.faces.index_update()
|
|
# cornertype 1 - ngon faces
|
|
if type_enum == 'opt0':
|
|
for k in dict_0:
|
|
if len(dict_0[k]) > 2:
|
|
bme.faces.new(dict_0[k])
|
|
bme.faces.index_update()
|
|
# cornertype 2 - triangulated faces
|
|
if type_enum == 'opt1':
|
|
for k_ in dict_0:
|
|
q_ = dict_0[k_][0]
|
|
dict_0[k_].pop(0)
|
|
n3_ = len(dict_0[k_])
|
|
for kk in range(n3_ - 1):
|
|
bme.faces.new([dict_0[k_][kk], dict_0[k_][(kk + 1) % n3_], q_])
|
|
bme.faces.index_update()
|
|
|
|
del_ = [bme.faces.remove(f) for f in list_del]
|
|
|
|
if del_:
|
|
del del_
|
|
|
|
|
|
# Operator
|
|
|
|
class MESH_OT_face_inset_fillet(Operator):
|
|
bl_idname = "mesh.face_inset_fillet"
|
|
bl_label = "Face Inset Fillet"
|
|
bl_description = ("Inset selected and Fillet (make round) the corners \n"
|
|
"of the newly created Faces")
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
# inset amount
|
|
inset_amount: FloatProperty(
|
|
name="Inset amount",
|
|
description="Define the size of the Inset relative to the selection",
|
|
default=0.04,
|
|
min=0, max=100.0,
|
|
step=1,
|
|
precision=3
|
|
)
|
|
# number of sides
|
|
number_of_sides: IntProperty(
|
|
name="Number of sides",
|
|
description="Define the roundness of the corners by specifying\n"
|
|
"the subdivision count",
|
|
default=4,
|
|
min=1, max=100,
|
|
step=1
|
|
)
|
|
distance: FloatProperty(
|
|
name="",
|
|
description="Use distance or radius for corners' size calculation",
|
|
default=0.04,
|
|
min=0.00001, max=100.0,
|
|
step=1,
|
|
precision=3
|
|
)
|
|
out: BoolProperty(
|
|
name="Outside",
|
|
description="Inset the Faces outwards in relation to the selection\n"
|
|
"Note: depending on the geometry, can give unsatisfactory results",
|
|
default=False
|
|
)
|
|
radius: BoolProperty(
|
|
name="Radius",
|
|
description="Use radius for corners' size calculation",
|
|
default=False
|
|
)
|
|
type_enum: EnumProperty(
|
|
items=(('opt0', "N-gon", "N-gon corners - Keep the corner Faces uncut"),
|
|
('opt1', "Triangle", "Triangulate corners")),
|
|
name="Corner Type",
|
|
default="opt0"
|
|
)
|
|
kp: BoolProperty(
|
|
name="Keep faces",
|
|
description="Do not delete the inside Faces\n"
|
|
"Only available if the Out option is checked",
|
|
default=False
|
|
)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.label(text="Corner Type:")
|
|
|
|
row = layout.row()
|
|
row.prop(self, "type_enum", text="")
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(self, "out")
|
|
|
|
if self.out is True:
|
|
row.prop(self, "kp")
|
|
|
|
row = layout.row()
|
|
row.prop(self, "inset_amount")
|
|
|
|
row = layout.row()
|
|
row.prop(self, "number_of_sides")
|
|
|
|
row = layout.row()
|
|
row.prop(self, "radius")
|
|
|
|
row = layout.row()
|
|
dist_rad = "Radius" if self.radius else "Distance"
|
|
row.prop(self, "distance", text=dist_rad)
|
|
|
|
def execute(self, context):
|
|
# this really just prepares everything for the main function
|
|
inset_amount = self.inset_amount
|
|
number_of_sides = self.number_of_sides
|
|
distance = self.distance
|
|
out = self.out
|
|
radius = self.radius
|
|
type_enum = self.type_enum
|
|
kp = self.kp
|
|
|
|
edit_mode_out()
|
|
ob_act = context.active_object
|
|
bme = bmesh.new()
|
|
bme.from_mesh(ob_act.data)
|
|
# this
|
|
face_index_list = [f.index for f in bme.faces if f.select and f.is_valid]
|
|
|
|
if len(face_index_list) == 0:
|
|
self.report({'WARNING'},
|
|
"No suitable Face selection found. Operation cancelled")
|
|
edit_mode_in()
|
|
|
|
return {'CANCELLED'}
|
|
|
|
elif len(face_index_list) != 0:
|
|
face_inset_fillet(bme, face_index_list,
|
|
inset_amount, distance, number_of_sides,
|
|
out, radius, type_enum, kp)
|
|
|
|
bme.to_mesh(ob_act.data)
|
|
edit_mode_in()
|
|
|
|
return {'FINISHED'}
|