- 添加Blender插件核心文件:__init__.py、ui.py、property.py、preference.py - 添加插件工具模块:g.py、loop.py、generate_loop.py、const.py、op.py - 添加翻译工具:utils/trans.py - 添加PuLP线性规划库及其依赖文件,包括CBC求解器二进制文件 - 添加.gitignore和VSCode配置文件
1146 lines
36 KiB
Python
1146 lines
36 KiB
Python
import bmesh
|
||
from .. import g
|
||
import mathutils
|
||
from .. import const
|
||
import bpy
|
||
import heapq
|
||
import math
|
||
import numpy as np
|
||
from bpy_extras import view3d_utils
|
||
import random
|
||
from collections import defaultdict
|
||
from functools import wraps
|
||
import time
|
||
|
||
|
||
def safe_execute(func, *args, **kwargs):
|
||
try:
|
||
func(*args, **kwargs)
|
||
except Exception as e:
|
||
print(f"An exception occurred in {func.__name__}: {e}")
|
||
|
||
|
||
def get_snap_option_target_obj():
|
||
snap_obj = bpy.context.scene.easy_patch_properties.snap_obj
|
||
if (
|
||
snap_obj
|
||
and snap_obj.name in bpy.context.scene.objects
|
||
and snap_obj.type == "MESH"
|
||
):
|
||
return snap_obj
|
||
else:
|
||
return None
|
||
|
||
|
||
def get_obj_bvh_tree():
|
||
obj = g.obj
|
||
# 获取依赖图
|
||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||
|
||
evaluated_obj = obj.evaluated_get(depsgraph)
|
||
target_mesh = evaluated_obj.to_mesh()
|
||
|
||
temp_bm = bmesh.new()
|
||
temp_bm.from_mesh(target_mesh)
|
||
temp_bm.transform(obj.matrix_world) # 应用对象的变换
|
||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(temp_bm)
|
||
|
||
temp_bm.free()
|
||
evaluated_obj.to_mesh_clear()
|
||
|
||
return bvh_tree
|
||
|
||
|
||
# 获取bvh树,如果用户填了snap选项,则用snap,没填则用可见物体
|
||
def get_snap_bvh_trees(exclude_object=[]):
|
||
snap_obj = get_snap_option_target_obj()
|
||
# 获取依赖图
|
||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||
|
||
# 构建BVH树,只针对非当前对象的其他网格对象
|
||
bvh_trees = []
|
||
if snap_obj:
|
||
# 获取评估后的对象数据,包含变换和修改器
|
||
evaluated_obj = snap_obj.evaluated_get(depsgraph)
|
||
target_mesh = evaluated_obj.to_mesh()
|
||
|
||
temp_bm = bmesh.new()
|
||
temp_bm.from_mesh(target_mesh)
|
||
temp_bm.transform(snap_obj.matrix_world) # 应用对象的变换
|
||
|
||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(temp_bm)
|
||
bvh_trees.append(bvh_tree)
|
||
temp_bm.free()
|
||
else:
|
||
# 获取所有可见的网格对象(排除当前对象)
|
||
visible_objects = [
|
||
o
|
||
for o in bpy.context.visible_objects
|
||
if o.type == "MESH"
|
||
and o not in exclude_object
|
||
and o.visible_get()
|
||
and o != get_edit_obj()
|
||
]
|
||
|
||
# 构建场景中所有其他网格对象的BVH树,这里要排除不可见的物体、不在视图层的物体,否则会出现奇怪的捕捉,同时减轻构建BVH性能负担
|
||
for scene_obj in visible_objects:
|
||
# 获取评估后的对象数据,包含变换和修改器
|
||
evaluated_obj = scene_obj.evaluated_get(depsgraph)
|
||
target_mesh = evaluated_obj.to_mesh()
|
||
|
||
temp_bm = bmesh.new()
|
||
temp_bm.from_mesh(target_mesh)
|
||
temp_bm.transform(scene_obj.matrix_world) # 应用对象的变换
|
||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(temp_bm)
|
||
bvh_trees.append(bvh_tree)
|
||
|
||
temp_bm.free()
|
||
evaluated_obj.to_mesh_clear()
|
||
|
||
return bvh_trees
|
||
|
||
|
||
def get_bvh_tree_from_bm(bm: bmesh.types.BMesh):
|
||
# Prepare a list to store the bounding boxes (aabb) for each face
|
||
faces = []
|
||
coords = []
|
||
|
||
# Loop through each face in the BMesh
|
||
for face in bm.faces:
|
||
# Calculate the bounding box for the current face
|
||
min_bound = mathutils.Vector((float("inf"), float("inf"), float("inf")))
|
||
max_bound = mathutils.Vector((float("-inf"), float("-inf"), float("-inf")))
|
||
|
||
# Get the vertices of the face
|
||
for loop in face.loops:
|
||
vertex = loop.vert.co
|
||
min_bound = min(min_bound, vertex)
|
||
max_bound = max(max_bound, vertex)
|
||
|
||
# Add the bounding box corners (min and max points)
|
||
faces.append((min_bound, max_bound))
|
||
coords.append([v.co for v in face.verts])
|
||
|
||
# Create a BVHTree
|
||
bvh_tree = bpy.types.BVHTree()
|
||
|
||
# Build the BVH tree from the faces (using the coords)
|
||
bvh_tree.build(coords)
|
||
|
||
return bvh_tree
|
||
|
||
|
||
def log(input):
|
||
if bpy.app.debug:
|
||
print(f"Easy Patch: {input}")
|
||
|
||
|
||
def hsv_to_rgb(h, s, v, a=None):
|
||
if s == 0.0:
|
||
rgb = v, v, v
|
||
else:
|
||
i = int(h * 6.0)
|
||
f = h * 6.0 - i
|
||
p = v * (1.0 - s)
|
||
q = v * (1.0 - s * f)
|
||
t = v * (1.0 - s * (1.0 - f))
|
||
i %= 6
|
||
if i == 0:
|
||
rgb = v, t, p
|
||
if i == 1:
|
||
rgb = q, v, p
|
||
if i == 2:
|
||
rgb = p, v, t
|
||
if i == 3:
|
||
rgb = p, q, v
|
||
if i == 4:
|
||
rgb = t, p, v
|
||
if i == 5:
|
||
rgb = v, p, q
|
||
|
||
return rgb if a is None else (*rgb, a)
|
||
|
||
|
||
# 只会删除边
|
||
def remove_bm_edges(bm, bm_edges):
|
||
for bm_edge in bm_edges:
|
||
if bm_edge in bm.edges:
|
||
bm.edges.remove(bm_edge)
|
||
|
||
|
||
def delete_bm_verts(bm, bm_verts):
|
||
# # 先把记录的变量删除
|
||
# for b in g.boundaries:
|
||
# for v in bm_verts:
|
||
# if v in b.
|
||
|
||
temp_vertices = [v for v in bm_verts if v in bm.verts]
|
||
temp_vertices = list(set(temp_vertices))
|
||
bmesh.ops.delete(bm, geom=temp_vertices, context="VERTS")
|
||
|
||
|
||
def remove_bm_faces(bm, faces):
|
||
for face in faces:
|
||
if face in bm.faces:
|
||
bm.faces.remove(face)
|
||
|
||
|
||
def uniform_vertices_co_along_path(verts, mid_vertex_num):
|
||
if len(verts) < 2 or mid_vertex_num < 0:
|
||
return ([], [])
|
||
|
||
# total length
|
||
total_length = sum(
|
||
(mathutils.Vector(verts[i]) - mathutils.Vector(verts[i - 1])).length
|
||
for i in range(1, len(verts))
|
||
)
|
||
|
||
# inverval distance of new vertex
|
||
new_verts_interval = total_length / (mid_vertex_num + 1)
|
||
|
||
new_verts = []
|
||
new_verts_correspond_path_idx = []
|
||
path_verts_accumulated_length = 0.0
|
||
segment_start = mathutils.Vector(verts[0])
|
||
|
||
for i in range(1, len(verts)):
|
||
segment_end = mathutils.Vector(verts[i])
|
||
segment_length = (segment_end - segment_start).length
|
||
path_verts_accumulated_length += segment_length
|
||
|
||
new_verts_accumulated_length = new_verts_interval * len(new_verts)
|
||
|
||
if not segment_length > 0:
|
||
continue
|
||
|
||
while (
|
||
path_verts_accumulated_length > new_verts_accumulated_length
|
||
): # if >= will include last. when mid_vertex_num too large >= will stuck in a number when increase, > will stuck in a number when decrease,
|
||
ratio = (
|
||
path_verts_accumulated_length - new_verts_accumulated_length
|
||
) / segment_length
|
||
|
||
new_vert = segment_end.lerp(segment_start, ratio)
|
||
new_verts.append(new_vert)
|
||
new_verts_correspond_path_idx.append(i - 1)
|
||
new_verts_accumulated_length = new_verts_interval * len(new_verts)
|
||
|
||
segment_start = segment_end
|
||
|
||
# 未知原因导致多一个点,可能是最后一个点距离过近导致上述循环把最后一个点也包含进去了
|
||
if len(new_verts) >= mid_vertex_num + 2:
|
||
new_verts[-1] = verts[-1]
|
||
new_verts_correspond_path_idx[-1] = len(verts) - 1
|
||
else:
|
||
new_verts.append(verts[-1])
|
||
new_verts_correspond_path_idx.append(len(verts) - 1)
|
||
return new_verts, new_verts_correspond_path_idx
|
||
|
||
|
||
def get_midpoint_of_path(bm, edge_path):
|
||
bm.verts.ensure_lookup_table()
|
||
bm.edges.ensure_lookup_table()
|
||
|
||
if len(edge_path) == 0:
|
||
return
|
||
num_edges = len(edge_path)
|
||
|
||
if num_edges % 2 == 0:
|
||
middle_index = num_edges // 2
|
||
edge1 = edge_path[middle_index - 1]
|
||
edge2 = edge_path[middle_index]
|
||
|
||
verts_edge1 = set(edge1.verts)
|
||
verts_edge2 = set(edge2.verts)
|
||
common_verts = verts_edge1.intersection(verts_edge2)
|
||
common_vert = next(iter(common_verts), None)
|
||
if common_vert:
|
||
return common_vert.co
|
||
else:
|
||
return mathutils.Vector((0, 0, 0))
|
||
|
||
# 如果边的数量为奇数,中点是中间边的中点
|
||
else:
|
||
total_length = sum(edge.calc_length() for edge in edge_path)
|
||
half_length = total_length / 2
|
||
accum_length = 0
|
||
for edge in edge_path:
|
||
edge_length = edge.calc_length()
|
||
accum_length += edge_length
|
||
|
||
if accum_length >= half_length:
|
||
overshoot = accum_length - half_length
|
||
factor = (edge_length - overshoot) / edge_length
|
||
v1, v2 = edge.verts
|
||
midpoint = v1.co.lerp(v2.co, factor)
|
||
return midpoint
|
||
|
||
return mathutils.Vector((0, 0, 0)) # 如果路径为空,返回原点
|
||
|
||
|
||
def get_edit_obj():
|
||
return bpy.context.edit_object
|
||
|
||
|
||
def normal_local_to_world(obj, normal):
|
||
matrix = obj.matrix_world.to_3x3().inverted().transposed()
|
||
normal_world = matrix @ normal
|
||
if normal_world.length_squared == 0:
|
||
return mathutils.Vector((0.0, 0.0, 1.0))
|
||
return normal_world.normalized()
|
||
|
||
|
||
def dijkstra(
|
||
bm, start, end, exclude_edges=[], exclude_verts=[], is_along_boundary=False
|
||
):
|
||
# 该函数计算的是累计路径长度,而不是累计拓扑路径长度
|
||
|
||
distances = {
|
||
v: float("inf") for v in bm.verts
|
||
} # 初始化距离字典,所有顶点的初始距离设为无穷大
|
||
previous = {v: None for v in bm.verts} # 初始化前驱顶点字典,用于重建路径
|
||
edge_to = {v: None for v in bm.verts} # 初始化边字典,用于存储到达每个顶点的边
|
||
distances[start] = 0 # 起始顶点到自身的距离设为0
|
||
queue = []
|
||
heapq.heappush(queue, (0, 0, start)) # 元组包含距离、计数器和顶点
|
||
|
||
count = 1 # 计数器,用于解决堆中的比较冲突
|
||
|
||
while queue:
|
||
current_distance, _, current_vert = heapq.heappop(
|
||
queue
|
||
) # 从队列中弹出当前距离最短的顶点
|
||
if current_vert == end: # 如果到达终点,重建并返回路径
|
||
path_verts = []
|
||
path_edges = []
|
||
while previous[current_vert]:
|
||
path_verts.insert(0, current_vert)
|
||
path_edges.insert(0, edge_to[current_vert])
|
||
current_vert = previous[current_vert]
|
||
path_verts.insert(0, current_vert)
|
||
return path_verts, path_edges
|
||
|
||
# 跳过处理距离更长的路径
|
||
if current_distance > distances[current_vert]:
|
||
continue
|
||
|
||
# 遍历当前顶点的所有相连边
|
||
for edge in current_vert.link_edges:
|
||
if is_along_boundary and not edge.is_boundary:
|
||
continue
|
||
|
||
if edge in exclude_edges:
|
||
continue
|
||
|
||
neighbor = edge.other_vert(current_vert) # 获取邻接顶点
|
||
# if neighbor in restricted_verts:
|
||
# continue # 跳过受限顶点
|
||
if neighbor in exclude_verts and neighbor != end:
|
||
continue # 跳过受限顶点,除非它是终点
|
||
|
||
distance = current_distance + edge.calc_length() # 计算新的距离
|
||
|
||
# 更新邻接顶点的最短路径
|
||
if distance < distances[neighbor]:
|
||
distances[neighbor] = distance
|
||
previous[neighbor] = current_vert
|
||
edge_to[neighbor] = edge
|
||
heapq.heappush(queue, (distance, count, neighbor))
|
||
count += 1
|
||
|
||
return [], []
|
||
|
||
|
||
# 更新drawing_path的路径,处理solid和非solid
|
||
def update_solid_path(context, event):
|
||
# if not g.drawing_path_boundary:
|
||
# return
|
||
|
||
if not g.drawing_verts_snap_vertex or not event.shift:
|
||
g.drawing_path_boundary.solid_path = []
|
||
g.drawing_path_boundary.edges = []
|
||
g.drawing_path_boundary.verts = []
|
||
return
|
||
|
||
if (not event.type == "MOUSEMOVE") and (
|
||
not (event.type == "LEFT_SHIFT" and event.value == "PRESS")
|
||
):
|
||
return
|
||
|
||
# update solid path
|
||
verts_path = []
|
||
boundary = g.drawing_path_boundary
|
||
if boundary.start_vertex and boundary.start_vertex != g.drawing_verts_snap_vertex:
|
||
exclude_edges = g.temp_bm_edges
|
||
exclude_verts = g.temp_bm_verts
|
||
verts_path, edges_path = dijkstra(
|
||
g.obj_bm,
|
||
boundary.start_vertex,
|
||
g.drawing_verts_snap_vertex,
|
||
exclude_edges=exclude_edges,
|
||
exclude_verts=exclude_verts,
|
||
)
|
||
|
||
# 特殊情况:路径长度为1,且路径的结束点在temp中且不是patch结尾,
|
||
if len(verts_path) == 2 and verts_path[-1] in g.temp_bm_verts:
|
||
verts_path = []
|
||
|
||
if verts_path:
|
||
g.drawing_path_boundary.solid_path = [
|
||
g.obj.matrix_world @ v.co for v in verts_path
|
||
]
|
||
g.drawing_path_boundary.edges = edges_path
|
||
g.drawing_path_boundary.verts = verts_path
|
||
|
||
|
||
def update_snap_vertex(event, is_snap_opposite=False):
|
||
start_time = time.time()
|
||
if not event.ctrl:
|
||
g.drawing_verts_snap_vertex = None
|
||
return
|
||
|
||
# # 注意使用,鼠标移动的事件是离散的,这个时间限制是连续,设置时间戳要放在最后,不然事件全部被阻塞了
|
||
current_timestamp = time.time()
|
||
if current_timestamp - g.last_update_snap_timestamp < 1 / 10:
|
||
return
|
||
|
||
if event.type != "MOUSEMOVE" and not (
|
||
event.type == "LEFT_CTRL" and event.value == "PRESS"
|
||
):
|
||
return
|
||
|
||
if not bpy.context.tool_settings.mesh_select_mode[0]:
|
||
bpy.context.tool_settings.mesh_select_mode = (True, False, False)
|
||
|
||
bpy.ops.mesh.select_all(action="DESELECT")
|
||
moust_co_2d = event.mouse_region_x, event.mouse_region_y
|
||
ret = bpy.ops.view3d.select(extend=True, location=moust_co_2d)
|
||
if "FINISHED" not in ret:
|
||
# 判断是否选中,即使判断也有可能获得None
|
||
g.drawing_verts_snap_vertex = None
|
||
return
|
||
|
||
vertex = g.obj_bm.select_history[-1]
|
||
|
||
# 上一个snap残留和新的不一样的话,直接用新的,如果一样,则判断阈值
|
||
# 这里要加上None,不然的话一闪一闪
|
||
if (
|
||
vertex == g.drawing_verts_snap_vertex or g.drawing_verts_snap_vertex is None
|
||
) and vertex:
|
||
# 做2d阈值
|
||
context = bpy.context
|
||
region = context.region
|
||
rv3d = context.region_data
|
||
mouse_co_2d = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
|
||
vertex_co_2d = view3d_utils.location_3d_to_region_2d(
|
||
region, rv3d, g.obj.matrix_world @ vertex.co
|
||
)
|
||
if not vertex_co_2d:
|
||
vertex = None
|
||
if (mouse_co_2d - vertex_co_2d).length > const.snap_vertex_threshold:
|
||
vertex = None
|
||
|
||
# # 禁用2条边
|
||
# if len(self.patch.boundaries) == 1 and vertex == self.patch.start_vertex:
|
||
# vertex = None
|
||
|
||
# TODO:使用插件单独创建的时候,可能是反向的,导致吸附不上
|
||
|
||
# 处理法线方向
|
||
if not is_snap_opposite and vertex and vertex.link_faces:
|
||
vert_normal_global = normal_local_to_world(g.obj, vertex.normal)
|
||
view_vector = bpy.context.region_data.view_rotation @ mathutils.Vector(
|
||
(0.0, 0.0, -1.0)
|
||
)
|
||
# 法线和视图同向,不选择
|
||
if vert_normal_global.dot(view_vector) > 0:
|
||
vertex = None
|
||
|
||
g.drawing_verts_snap_vertex = vertex
|
||
|
||
bpy.ops.mesh.select_all(action="DESELECT")
|
||
|
||
g.last_update_snap_timestamp = current_timestamp # 这个要放到最后更新,不然中间有return了,就相当于没更新就设置了这个timestamp
|
||
|
||
return
|
||
|
||
|
||
def create_temp_vertex(coord):
|
||
vertex = g.obj_bm.verts.new(g.obj.matrix_world.inverted() @ coord)
|
||
g.temp_bm_verts.add(vertex)
|
||
return vertex
|
||
|
||
|
||
def create_temp_edge(vertice_1, vertice_2):
|
||
edge = g.obj_bm.edges.new((vertice_1, vertice_2))
|
||
g.temp_bm_edges.add(edge)
|
||
return edge
|
||
|
||
|
||
def get_nearest_boundary_from_mouse(event):
|
||
context = bpy.context
|
||
region = context.region
|
||
rv3d = context.region_data
|
||
mouse_co_2d = (event.mouse_region_x, event.mouse_region_y)
|
||
|
||
threshold = const.hover_threshold
|
||
for boundary in g.boundaries:
|
||
|
||
for edge in boundary.edges:
|
||
obj = g.obj
|
||
# vert.co获取的是局部坐标
|
||
edge_coords_3d = [obj.matrix_world @ vert.co for vert in edge.verts]
|
||
edge_coords_2d = [
|
||
view3d_utils.location_3d_to_region_2d(region, rv3d, coord)
|
||
for coord in edge_coords_3d
|
||
]
|
||
if None in edge_coords_2d:
|
||
continue
|
||
|
||
distance = distance_point_to_line(
|
||
mathutils.Vector(mouse_co_2d),
|
||
mathutils.Vector(edge_coords_2d[0]),
|
||
mathutils.Vector(edge_coords_2d[1]),
|
||
)
|
||
|
||
if distance < threshold:
|
||
return boundary
|
||
|
||
|
||
def distance_point_to_line(pt, v1, v2):
|
||
line_vec = v2 - v1
|
||
|
||
# in case 0 length
|
||
if line_vec.length == 0:
|
||
return (pt - v1).length
|
||
|
||
pt_vec = pt - v1
|
||
line_len = line_vec.length
|
||
line_unitvec = line_vec.normalized()
|
||
pt_vec_scaled = pt_vec * (1.0 / line_len)
|
||
t = line_unitvec.dot(pt_vec_scaled)
|
||
if t < 0.0:
|
||
t = 0.0
|
||
elif t > 1.0:
|
||
t = 1.0
|
||
nearest = v1 + line_vec * t
|
||
dist = (nearest - pt).length
|
||
return dist
|
||
|
||
|
||
def stable_random_from_obj(obj):
|
||
obj_id = id(obj)
|
||
random.seed(obj_id)
|
||
stable_random_value = random.random()
|
||
return stable_random_value
|
||
|
||
|
||
def get_loops_from_boundary(boundary):
|
||
loops = []
|
||
for loop in g.loops:
|
||
if boundary in loop.boundaries:
|
||
loops.append(loop)
|
||
return loops
|
||
|
||
|
||
def on_seperate_boundary_(boundary):
|
||
# 已在 done_from_temp写了
|
||
pass
|
||
|
||
|
||
def on_change_boundary_length(boundary):
|
||
# boundary长度改变,loop不变,pattern形状会变,
|
||
loops = get_loops_from_boundary(boundary)
|
||
for loop in loops:
|
||
loop.recreate_pattern()
|
||
|
||
g.is_redraw_boundary = True
|
||
g.is_redraw_pattern = True
|
||
|
||
|
||
def on_delete_boundary():
|
||
# boundary删除,loop可能会被删除,loop被删pattern也会被删
|
||
pass
|
||
|
||
|
||
def on_create_boundary():
|
||
# boundary创建,loop可能增加,pattern可能增加
|
||
pass
|
||
|
||
|
||
def on_change_patch_pattern():
|
||
pass
|
||
|
||
|
||
def rotate_list(lst, n):
|
||
if not lst:
|
||
return []
|
||
n = n % len(lst)
|
||
return lst[n:] + lst[:n]
|
||
|
||
|
||
def edge_loops(bm, edge):
|
||
# 获取循环边
|
||
def walk(edge):
|
||
yield edge
|
||
edge.tag = True
|
||
for l in edge.link_loops:
|
||
loop = l.link_loop_radial_next.link_loop_next.link_loop_next
|
||
if not (len(loop.face.verts) != 4 or loop.edge.tag):
|
||
yield from walk(loop.edge)
|
||
|
||
for e in bm.edges:
|
||
e.tag = False
|
||
return list(walk(edge))
|
||
|
||
|
||
def get_verts_and_edges_from_faces(bm, faces):
|
||
faces = [i for i in faces if i in bm.faces]
|
||
verts = set()
|
||
edges = set()
|
||
for face in faces:
|
||
for edge in face.edges:
|
||
edges.add(edge)
|
||
for vert in face.verts:
|
||
verts.add(vert)
|
||
|
||
return list(verts), list(edges)
|
||
|
||
|
||
def smooth_verts_by_normal(obj, bm, verts, smooth_type=0):
|
||
# https://github.com/fedackb/mesh-fairing
|
||
|
||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
#
|
||
# ##### END GPL LICENSE BLOCK #####
|
||
def setup_fairing(
|
||
v: bmesh.types.BMVert,
|
||
i,
|
||
A,
|
||
b,
|
||
multiplier,
|
||
depth,
|
||
vert_col_map,
|
||
vert_weights,
|
||
loop_weights,
|
||
):
|
||
if depth == 0:
|
||
if v in vert_col_map:
|
||
j = vert_col_map[v]
|
||
if (i, j) not in A:
|
||
A[i, j] = 0
|
||
A[i, j] -= multiplier
|
||
else:
|
||
b[i][0] += multiplier * v.co.x
|
||
b[i][1] += multiplier * v.co.y
|
||
b[i][2] += multiplier * v.co.z
|
||
else:
|
||
w_ij_sum = 0
|
||
w_i = vert_weights(v)
|
||
for l in v.link_loops:
|
||
other = l.link_loop_next.vert
|
||
w_ij = loop_weights(l)
|
||
w_ij_sum += w_ij
|
||
setup_fairing(
|
||
other,
|
||
i,
|
||
A,
|
||
b,
|
||
w_i * w_ij * multiplier,
|
||
depth - 1,
|
||
vert_col_map,
|
||
vert_weights,
|
||
loop_weights,
|
||
)
|
||
setup_fairing(
|
||
v,
|
||
i,
|
||
A,
|
||
b,
|
||
-1 * w_i * w_ij_sum * multiplier,
|
||
depth - 1,
|
||
vert_col_map,
|
||
vert_weights,
|
||
loop_weights,
|
||
)
|
||
|
||
def calc_circumcenter(a, b, c):
|
||
ab = b - a
|
||
ac = c - a
|
||
ab_cross_ac = ab.cross(ac)
|
||
if ab_cross_ac.length_squared > 0:
|
||
d = ac.length_squared * ab_cross_ac.cross(ab)
|
||
d += ab.length_squared * ac.cross(ab_cross_ac)
|
||
d /= 2 * ab_cross_ac.length_squared
|
||
return a + d
|
||
else:
|
||
return a
|
||
|
||
def calc_uniform_vertex_weight(v):
|
||
n = len(v.link_edges)
|
||
return 1 / n if n != 0 else float("inf")
|
||
|
||
def calc_uniform_loop_weight(l):
|
||
return 1
|
||
|
||
def calc_voronoi_vertex_weight(v):
|
||
area = 0
|
||
a = v.co
|
||
acute_threshold = math.pi / 2
|
||
for l in v.link_loops:
|
||
b = l.link_loop_next.vert.co
|
||
c = l.link_loop_prev.vert.co
|
||
if l.calc_angle() < acute_threshold:
|
||
d = calc_circumcenter(a, b, c)
|
||
else:
|
||
d = (b + c) / 2
|
||
area += mathutils.geometry.area_tri(a, (a + b) / 2, d)
|
||
area += mathutils.geometry.area_tri(a, d, (a + c) / 2)
|
||
return 1 / area if area != 0 else 1e12
|
||
|
||
def calc_cotangent_loop_weight(l):
|
||
weight = 0
|
||
co_a = l.vert.co
|
||
co_b = l.link_loop_next.vert.co
|
||
coords = [l.link_loop_prev.vert.co]
|
||
if not l.edge.is_boundary:
|
||
coords.append(l.link_loop_radial_next.link_loop_next.link_loop_next.vert.co)
|
||
for co_c in coords:
|
||
try:
|
||
angle = (co_a - co_c).angle(co_b - co_c)
|
||
weight += 1 / math.tan(angle)
|
||
except (ValueError, ZeroDivisionError):
|
||
weight += 1e-4
|
||
weight /= 2
|
||
return weight
|
||
|
||
def _do(verts, order, vert_weights, loop_weights):
|
||
interior_verts = (v for v in verts if not v.is_boundary and not v.is_wire)
|
||
vert_col_map = {v: col for col, v in enumerate(interior_verts)}
|
||
A = dict()
|
||
b = [[0 for i in range(3)] for j in range(len(vert_col_map))]
|
||
for v, col in vert_col_map.items():
|
||
setup_fairing(
|
||
v, col, A, b, 1, order, vert_col_map, vert_weights, loop_weights
|
||
)
|
||
|
||
x = None
|
||
n = len(b)
|
||
A_numpy = np.zeros((n, n), dtype="d")
|
||
b = np.asarray(b, dtype="d")
|
||
for key, val in A.items():
|
||
A_numpy[key] = val
|
||
x = np.linalg.solve(A_numpy, b)
|
||
|
||
for v, co in zip(verts, x):
|
||
v.co = co
|
||
|
||
verts = [v for v in verts if v in bm.verts]
|
||
if smooth_type == 0:
|
||
_do(verts, 1, calc_uniform_vertex_weight, calc_uniform_loop_weight)
|
||
_do(verts, 1, calc_voronoi_vertex_weight, calc_cotangent_loop_weight)
|
||
else:
|
||
_do(verts, 1, calc_uniform_vertex_weight, calc_uniform_loop_weight)
|
||
_do(verts, 2, calc_voronoi_vertex_weight, calc_cotangent_loop_weight)
|
||
|
||
bm.normal_update()
|
||
bmesh.update_edit_mesh(obj.data)
|
||
|
||
|
||
# TODO: 位于边缘的顶点法线方向不准,看看有没有现成api,没有就采样旁边的面
|
||
def snap_verts_to_surface_along_normal(
|
||
obj, bm, verts, bvh_trees, allow_opposite_direction=False
|
||
):
|
||
|
||
snap_count = 0
|
||
# 遍历所有选中的顶点
|
||
for v in verts:
|
||
# 获取顶点的全局坐标和法向
|
||
global_co = obj.matrix_world @ v.co
|
||
|
||
global_normal = normal_local_to_world(obj, v.normal)
|
||
|
||
# if v.is_boundary:
|
||
# # 如果是边界顶点,计算链接面的平均法向量
|
||
# face_normals = []
|
||
# for face in v.link_faces:
|
||
# face_normals.append(face.normal)
|
||
# average_normal = (
|
||
# sum(face_normals, mathutils.Vector()) / len(face_normals)
|
||
# if face_normals
|
||
# else mathutils.Vector((0, 0, 0))
|
||
# )
|
||
# global_normal = (obj.matrix_world.to_3x3() @ average_normal).normalized()
|
||
# else:
|
||
# # 否则,直接使用顶点的法向量
|
||
# global_normal = (obj.matrix_world.to_3x3() @ v.normal).normalized()
|
||
|
||
closest_location = None
|
||
closest_distance = float("inf")
|
||
closest_location_opposite = None
|
||
closest_distance_opposite = float("inf")
|
||
|
||
# 遍历每个 BVH 树,优先正方向投射,避免吸附到物体另一侧造成拉伸
|
||
for bvh_tree in bvh_trees:
|
||
# 正方向射线投射
|
||
result_pos = bvh_tree.ray_cast(global_co, global_normal)
|
||
if result_pos[0]:
|
||
location_pos, _, _, distance_pos = result_pos
|
||
if distance_pos < closest_distance:
|
||
closest_location = location_pos
|
||
closest_distance = distance_pos
|
||
|
||
if allow_opposite_direction:
|
||
result_neg = bvh_tree.ray_cast(global_co, -global_normal)
|
||
if result_neg[0]:
|
||
location_neg, _, _, distance_neg = result_neg
|
||
if distance_neg < closest_distance_opposite:
|
||
closest_location_opposite = location_neg
|
||
closest_distance_opposite = distance_neg
|
||
|
||
# 如果找到最近的投影点,则将顶点移动到该点
|
||
if not closest_location and closest_location_opposite:
|
||
closest_location = closest_location_opposite
|
||
|
||
if closest_location:
|
||
# 将全局坐标转换回局部坐标
|
||
new_local_co = obj.matrix_world.inverted() @ closest_location
|
||
v.co = new_local_co
|
||
snap_count += 1
|
||
|
||
# 将顶点更改应用到网格上,loop_triangles提高效率
|
||
bm.normal_update()
|
||
bmesh.update_edit_mesh(obj.data, loop_triangles=True)
|
||
|
||
|
||
# 666case will be failed
|
||
def get_all_reduced_inputs(loop_shape, is_remove_duplicated_rotation=True):
|
||
def reduce_and_explore(current_lst, results):
|
||
reduced = False
|
||
for k in range(len(current_lst)):
|
||
prev_index = (k - 1) % len(current_lst)
|
||
next_index = (k + 1) % len(current_lst)
|
||
if current_lst[prev_index] > 1 and current_lst[next_index] > 1:
|
||
new_lst = current_lst.copy()
|
||
reduction = min(new_lst[prev_index], new_lst[next_index]) - 1
|
||
new_lst[prev_index] -= reduction
|
||
new_lst[next_index] -= reduction
|
||
reduce_and_explore(new_lst, results)
|
||
reduced = True
|
||
|
||
if not reduced:
|
||
results.add(tuple(current_lst))
|
||
|
||
def is_rotate_dup(tup1, tup2):
|
||
if len(tup1) != len(tup2):
|
||
return False
|
||
return any(tup1 == tup2[i:] + tup2[:i] for i in range(len(tup2)))
|
||
|
||
def max_consecutive_ones(tup):
|
||
count = 0
|
||
max_count = 0
|
||
for num in tup:
|
||
if num == 1:
|
||
count += 1
|
||
max_count = max(max_count, count)
|
||
else:
|
||
count = 0
|
||
return max_count
|
||
|
||
# 两条边的时候特殊处理
|
||
if len(loop_shape) == 2:
|
||
if loop_shape[0] == loop_shape[1]:
|
||
return [(2, 2)]
|
||
else:
|
||
if loop_shape[0] > loop_shape[1]:
|
||
return [(3, 1)]
|
||
else:
|
||
return [(1, 3)]
|
||
|
||
all_results = set()
|
||
reduce_and_explore(list(loop_shape), all_results)
|
||
|
||
if not is_remove_duplicated_rotation:
|
||
return all_results
|
||
|
||
# 删除重复
|
||
unique_results = []
|
||
for tup in list(all_results):
|
||
if not any(is_rotate_dup(tup, existing) for existing in unique_results):
|
||
unique_results.append(tup)
|
||
|
||
# # 排序,1越少越往后,防止缩减错误和复杂度
|
||
# sorted_results = sorted(unique_results, key=lambda x: x.count(1), reverse=True)
|
||
|
||
sorted_results = sorted(
|
||
unique_results,
|
||
key=lambda x: (max_consecutive_ones(x), x.count(1)),
|
||
reverse=True,
|
||
)
|
||
|
||
return sorted_results
|
||
|
||
|
||
def flip_A(A_):
|
||
row_num = len(A_)
|
||
A = []
|
||
for row_idx, row in enumerate(A_):
|
||
flip_row_idx = row_num - row_idx - 1
|
||
row = row[:row_num] + A_[flip_row_idx][row_num:]
|
||
A.append(row)
|
||
return A
|
||
|
||
|
||
# def process_path_to_mirror_axis(path):
|
||
# # 首先,x坐标全部归零
|
||
# for co in path:
|
||
# co.x = 0
|
||
|
||
# # 然后,投射到表面(如果用户操作不是太离谱,这一步不用)
|
||
# # 首先投射方向是在yz平面上的(即垂直于x轴),然后方向垂直于连线,有两个方向,都raycast,然后投射到距离最近的那个
|
||
|
||
# for i, co in enumerate(path):
|
||
# # 获取相邻的路径点
|
||
# if i == len(path) - 1:
|
||
# neighbor_co = path[-2]
|
||
# else:
|
||
# neighbor_co = path[i + 1]
|
||
|
||
# line_vector = neighbor_co - co
|
||
# x_normal = mathutils.Vector((1, 0, 0))
|
||
|
||
# cross_product = line_vector.cross(
|
||
# x_normal
|
||
# ) # 两向量叉乘,得到垂直于两个向量的向量
|
||
# raycast_direction = cross_product.normalized()
|
||
|
||
# # 获取顶点的全局坐标
|
||
# global_co = co
|
||
|
||
# closest_location = None
|
||
# closest_distance = float("inf")
|
||
|
||
# # 遍历每个 BVH 树,进行双向射线投射
|
||
# for bvh_tree in g.snap_bvh_trees:
|
||
# # 正方向射线投射
|
||
# result_pos = bvh_tree.ray_cast(global_co, raycast_direction)
|
||
# if result_pos[0]:
|
||
# location_pos, _, _, distance_pos = result_pos
|
||
# if distance_pos < closest_distance:
|
||
# closest_location = location_pos
|
||
# closest_distance = distance_pos
|
||
|
||
# # 负方向射线投射
|
||
# result_neg = bvh_tree.ray_cast(global_co, -raycast_direction)
|
||
# if result_neg[0]:
|
||
# location_neg, _, _, distance_neg = result_neg
|
||
# if distance_neg < closest_distance:
|
||
# closest_location = location_neg
|
||
# closest_distance = distance_neg
|
||
|
||
# # 如果找到最近的投影点,则将坐标改为该投射点
|
||
# if closest_location:
|
||
# closest_location.x = 0
|
||
# path[i] = closest_location
|
||
|
||
|
||
def project_point_to_plane(point_coord, plane_normal, plane_coord):
|
||
# 确保法向量是单位向量
|
||
plane_normal.normalize()
|
||
|
||
# 计算点到平面的向量
|
||
vector_to_plane = point_coord - plane_coord
|
||
|
||
# 计算点到平面的垂直距离
|
||
distance = vector_to_plane.dot(plane_normal)
|
||
|
||
# 计算投影点
|
||
projected_point = point_coord - distance * plane_normal
|
||
|
||
return projected_point
|
||
|
||
|
||
def process_path_to_mirror_axis(path):
|
||
obj_x_vector = g.obj.matrix_world.col[
|
||
0
|
||
].xyz # 获取 X 正方向的向量(第1列),只取前三个分量 (X, Y, Z)
|
||
obj_coord = g.obj.location
|
||
|
||
# 先投影到轴上
|
||
for i in range(len(path)):
|
||
point_coord = path[i]
|
||
path[i] = project_point_to_plane(point_coord, obj_x_vector, obj_coord)
|
||
|
||
# 然后,投射到表面(如果用户操作不是太离谱,这一步不用)
|
||
# 首先投射方向是在yz平面上的(即垂直于x轴),然后方向垂直于连线,有两个方向,都raycast,然后投射到距离最近的那个
|
||
|
||
for i, co in enumerate(path):
|
||
# 获取相邻的路径点
|
||
if i == len(path) - 1:
|
||
neighbor_co = path[-2]
|
||
else:
|
||
neighbor_co = path[i + 1]
|
||
|
||
line_vector = neighbor_co - co
|
||
x_normal = obj_x_vector
|
||
|
||
cross_product = line_vector.cross(
|
||
x_normal
|
||
) # 两向量叉乘,得到垂直于两个向量的向量
|
||
raycast_direction = cross_product.normalized()
|
||
|
||
# 获取顶点的全局坐标,这个就是全局,别转了
|
||
global_co = co
|
||
|
||
closest_location = None
|
||
closest_distance = float("inf")
|
||
|
||
# 遍历每个 BVH 树,进行双向射线投射
|
||
for bvh_tree in g.snap_bvh_trees:
|
||
# 正方向射线投射
|
||
result_pos = bvh_tree.ray_cast(global_co, raycast_direction)
|
||
if result_pos[0]:
|
||
location_pos, _, _, distance_pos = result_pos
|
||
if distance_pos < closest_distance:
|
||
closest_location = location_pos
|
||
closest_distance = distance_pos
|
||
|
||
# 负方向射线投射
|
||
result_neg = bvh_tree.ray_cast(global_co, -raycast_direction)
|
||
if result_neg[0]:
|
||
location_neg, _, _, distance_neg = result_neg
|
||
if distance_neg < closest_distance:
|
||
closest_location = location_neg
|
||
closest_distance = distance_neg
|
||
|
||
# 如果找到最近的投影点,则将坐标改为该投射点,这里再投影一次,不过理论上来说没有必要
|
||
if closest_location:
|
||
path[i] = project_point_to_plane(closest_location, obj_x_vector, obj_coord)
|
||
|
||
|
||
def coords_3d_to_2d(region, rv3d, coords_3d):
|
||
coords_2d = []
|
||
for coord in coords_3d:
|
||
result = view3d_utils.location_3d_to_region_2d(region, rv3d, coord)
|
||
if result is not None:
|
||
coords_2d.append(result)
|
||
return coords_2d
|
||
|
||
|
||
def set_block_before_done(func):
|
||
@wraps(func)
|
||
def wrapper(*args, **kwargs):
|
||
# 函数开始前设置为
|
||
g.is_changing_mesh = True
|
||
try:
|
||
# 执行函数
|
||
return func(*args, **kwargs)
|
||
finally:
|
||
# 函数结束后设置为
|
||
g.is_changing_mesh = False
|
||
g.hovering_loop = None
|
||
g.hovering_boundary = None
|
||
|
||
return wrapper
|
||
|
||
|
||
def smooth_verts_by_avg(obj, bm, verts):
|
||
verts = [v for v in verts if v in bm.verts]
|
||
|
||
iterations = 5
|
||
for _ in range(iterations):
|
||
new_positions = []
|
||
max_movement = 0
|
||
|
||
for v in verts:
|
||
avg = mathutils.Vector((0, 0, 0))
|
||
for e in v.link_edges:
|
||
avg += e.other_vert(v).co
|
||
avg /= len(v.link_edges)
|
||
|
||
# 计算移动量
|
||
movement = (v.co - avg).length
|
||
max_movement = max(max_movement, movement)
|
||
new_positions.append((v, avg))
|
||
|
||
for v, new_co in new_positions:
|
||
v.co = new_co
|
||
|
||
bm.normal_update()
|
||
bmesh.update_edit_mesh(obj.data)
|
||
|
||
|
||
def flip_normal(obj, bm, faces):
|
||
faces = [i for i in faces if i in bm.faces]
|
||
for face in faces:
|
||
face.normal_flip()
|
||
bm.normal_update()
|
||
bmesh.update_edit_mesh(obj.data)
|
||
|
||
|
||
# 法向全部朝视图
|
||
def faces_normal_toward_viewport(obj, bm, faces):
|
||
# 确保faces只包含有效的面
|
||
faces = [i for i in faces if i in bm.faces]
|
||
|
||
region = bpy.context.region
|
||
view_direction = view3d_utils.region_2d_to_vector_3d(
|
||
region, region.data, (region.width / 2.0, region.height / 2.0)
|
||
)
|
||
# 变换物体空间的法向量到世界空间
|
||
for face in faces:
|
||
normal_world = normal_local_to_world(obj, face.normal)
|
||
if normal_world.dot(view_direction) > 0: # 和视图同向,反转
|
||
face.normal_flip()
|
||
bm.normal_update()
|
||
bmesh.update_edit_mesh(obj.data)
|
||
|
||
|
||
def get_3d_area():
|
||
try:
|
||
for a in bpy.data.window_managers[0].windows[0].screen.areas:
|
||
if a.type == "VIEW_3D":
|
||
return a
|
||
return None
|
||
except:
|
||
return None
|
||
|
||
|
||
def get_created_verts_num():
|
||
g.temp_bm_verts = [v for v in g.temp_bm_verts if v in g.obj_bm.verts]
|
||
return len(g.temp_bm_verts)
|
||
|
||
|
||
def get_package_name():
|
||
return __name__.split(".")[0]
|
||
|
||
|
||
def get_preferences():
|
||
return bpy.context.preferences.addons[get_package_name()].preferences
|
||
|
||
|
||
def is_mouse_move(event):
|
||
if not g.last_mouse_co_2d:
|
||
return True
|
||
|
||
if (
|
||
g.last_mouse_co_2d
|
||
- mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
|
||
).length < const.mouse_move_threshold:
|
||
return False
|
||
else:
|
||
return True
|