Files
blender-easy-patch/utils/functions.py
小煜 ab91b120e6 feat: 初始化Easy Patch插件及依赖文件
- 添加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配置文件
2026-03-03 19:24:57 +08:00

1146 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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