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 . # # ##### 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