from __future__ import annotations import bpy import bmesh from bpy_extras import view3d_utils import time import mathutils from . import const, g from .utils import functions from .draw import draw from . import boundary from . import generate_loop from .utils.trans import t class EasyPatchModalOperator(bpy.types.Operator): bl_idname = f"{const.addon_prefix}.start" bl_label = "Start Patching" bl_options = {"REGISTER", "UNDO"} # 不加UNDO撤回的话,会导致用户前一个步骤也撤回 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def modal(self, context, event): try: if context.area: context.area.tag_redraw() result = self.process_undo(context, event) if result is not None: return result # start_timestamp = time.time() # process drag result = self.process_drawing_verts(context, event) if result is not None: return result # drawing_verts_consume_time = time.time() - start_timestamp # process stop result = self.process_stop(context, event) if result is not None: return result # start_timestamp = time.time() result = self.process_running(context, event) if result is not None: return result # running_consume_time = time.time() - start_timestamp # start_timestamp = time.time() result = self.process_loop(context, event) if result is not None: return result # process_loop_consume_time = time.time() - start_timestamp # start_timestamp = time.time() update_hover(context, event) # update_hover_consume_time = time.time() - start_timestamp # last_echo_time = g.last_echo_time if g.last_echo_time else 0 # if time.time() - last_echo_time > 0.1: # print( # drawing_verts_consume_time, # running_consume_time, # process_loop_consume_time, # update_hover_consume_time, # ) # g.last_echo_time = time.time() # g.last_mouse_co_2d = mathutils.Vector( (event.mouse_region_x, event.mouse_region_y) ) return self.process_ui_block(context, event) except Exception as e: functions.safe_execute(self.clear, context) functions.log(f"An expection raise when modal, exit modal. Error: {str(e)}") self.report( {"WARNING"}, f"An expection raise when modal, exit modal. Error: {str(e)}", ) EasyPatchModalOperator.set_stop_flag() raise e return {"PASS_THROUGH"} # return {"RUNNING_MODAL"} def invoke(self, context, event): if not g.is_running: self.start(context) return {"RUNNING_MODAL"} else: EasyPatchModalOperator.stop() return {"FINISHED"} def start(self, context): functions.log("modal start") g.clear_all_variables() draw.start(self) g.is_running = True self._timer = context.window_manager.event_timer_add(0.1, window=context.window) context.window_manager.modal_handler_add(self) self.context = context self.region = context.region self.rv3d = context.region_data obj = functions.get_edit_obj() g.obj = obj g.obj_me = obj.data g.obj_bm = bmesh.from_edit_mesh(obj.data) g.mode = g.Mode.NORMAL g.drawing_verts_snap_vertex = None g.drawing_path_boundary = None g.snap_bvh_trees = functions.get_snap_bvh_trees() g.obj_bvh_tree = functions.get_obj_bvh_tree() def stop(): functions.log("modal stop") EasyPatchModalOperator.set_stop_flag() def clear(self, context, remove_mesh_flag=True): functions.log("modal clear") # 用户tab离开编辑模式,会导致bm被删除,所以会报很多错 if remove_mesh_flag: functions.safe_execute(self.remove_mesh) functions.safe_execute(context.window_manager.event_timer_remove, self._timer) functions.safe_execute(EasyPatchModalOperator.set_stop_flag) functions.safe_execute(draw.clear) if context.object.mode == "EDIT": bmesh.update_edit_mesh(g.obj_me) g.clear_all_variables() def set_stop_flag(): g.is_running = False # 删除插件生成的网格 def remove_mesh(self): if g.temp_bm_edges: functions.remove_bm_edges(g.obj_bm, g.temp_bm_edges) g.temp_bm_edges = set() if g.temp_bm_verts: functions.delete_bm_verts(g.obj_bm, g.temp_bm_verts) g.temp_bm_verts = set() for loop in g.loops: if loop.pattern: loop.pattern.remove_mesh() try: bmesh.update_edit_mesh(g.obj_me) except Exception as e: pass def process_stop(self, context, event): if ( not g.is_running or event.type == "ESC" or context.object.mode != "EDIT" or g.obj_bm is None ): self.clear(context) return {"FINISHED"} if event.type in ["RET", "NUMPAD_ENTER"]: count_vertex = functions.get_created_verts_num() self.clear(context, remove_mesh_flag=False) self.report({"INFO"}, f"{count_vertex} {t('verts')} {t('created')}") return {"FINISHED"} if g.running_exception: raise g.running_exception def process_drawing_verts(self, context, event): # TODO: update snap和hovering和append_from_2d加一个移动阈值才更新,记录上一个鼠标位置,超过阈值才进行,modal末尾更新,共用一个值 # TODO: loop加上一个屏蔽pattern生成的状态 functions.update_snap_vertex(event, is_snap_opposite=g.is_snap_opposite) if g.mode == g.Mode.DRAWING: # # update and clear snap vertex when ctrl in running functions.update_solid_path(context, event) # append路径 g.drawing_path_boundary.append_path_from_2d( context, (event.mouse_region_x, event.mouse_region_y), is_snap_surface=g.is_snap_surface, is_snap_opposite=g.is_snap_opposite, ) # drawing中改变点数 if event.type == "WHEELUPMOUSE": g.drawing_path_boundary.length += 1 return {"RUNNING_MODAL"} if event.type == "WHEELDOWNMOUSE": g.drawing_path_boundary.length -= 1 return {"RUNNING_MODAL"} # stop drag if event.type == "LEFTMOUSE" and event.value == "RELEASE": # # 第六条强制闭合 # if len(self.patch.boundaries) == 5: # g.drawing_verts_snap_vertex = self.patch.start_vertex snap_vert = ( g.drawing_verts_snap_vertex ) # 确保一致性,先获取,防止中间修改 functions.log("stop drag vertex:" + str(snap_vert)) done_boundary = g.drawing_path_boundary.done_from_temp(snap_vert) g.drawing_verts_snap_vertex = None g.mode = g.Mode.NORMAL # clear drag things g.drawing_path_boundary = None if not done_boundary: return if done_boundary.path[-1] != done_boundary.end_vertex.co: # 不知道为什么,end_vertex是吸附的时候需要手动补 done_boundary.path.append( g.obj.matrix_world @ done_boundary.end_vertex.co ) generate_loop.regenerate_loops() if not g.is_drawing_along_mirror_persist: g.is_drawing_along_mirror = False # 每次画完都重置这个变量 # 删除temp boundary if event.type == "RIGHTMOUSE" and event.value == "PRESS": g.drawing_path_boundary = None g.mode = g.Mode.NORMAL return {"RUNNING_MODAL"} # if event.value != "NOTHING": # print(event.type, event.value) # TODO: 以CLICK_DRAG,用户可能会以左键单击误触发,造成多余的线条,多一个ctrl+enter,以保留多余线条 # start drag if event.type == "LEFTMOUSE" and event.value == "CLICK_DRAG": functions.log("start drag vertex:" + str(g.drawing_verts_snap_vertex)) g.mode = g.Mode.DRAWING # create temp boundary if g.drawing_verts_snap_vertex: g.drawing_path_boundary = boundary.Boundary.create_temp( g.drawing_verts_snap_vertex ) else: g.drawing_path_boundary = boundary.Boundary.create_temp(None) return { "RUNNING_MODAL" } # block drag ui operation, if not running_modal, release will not trigger def process_undo(self, context, event): if event.type == "Z" and event.value == "PRESS" and event.ctrl: self.report({"WARNING"}, t("you_cant_undo_here")) return {"RUNNING_MODAL"} def process_ui_block(self, context, event): # I键内插,会导致未知问题 if event.type in ["X", "I"]: return {"RUNNING_MODAL"} return {"PASS_THROUGH"} def process_loop(self, context, event): if not g.hovering_loop: return loop = g.hovering_loop # R if event.type == "R" and event.value == "PRESS": loop.change_solver_constraint_rotation() return {"RUNNING_MODAL"} # T if event.type == "T" and event.value == "PRESS": loop.next_solver_constraint_pattern(is_auto_search=g.is_auto_search_pattern) return {"RUNNING_MODAL"} # invert normal if event.type == "I" and event.value == "PRESS" and loop.pattern: functions.faces_normal_toward_viewport(g.obj, g.obj_bm, loop.pattern.faces) return {"RUNNING_MODAL"} # 改变padding约束 if ( event.shift and (event.type == "WHEELUPMOUSE" or event.type == "WHEELDOWNMOUSE") and g.hovering_boundary ): boundary_index = loop.get_boundary_index(g.hovering_boundary) if event.type == "WHEELUPMOUSE": loop.change_solver_constraint_list(boundary_index, 1) if event.type == "WHEELDOWNMOUSE": loop.change_solver_constraint_list(boundary_index, -1) # X附加边流 if event.type == "X" and event.value == "PRESS" and event.shift: loop.change_solver_constraint_list("x", 1) return {"RUNNING_MODAL"} if event.type == "X" and event.value == "PRESS" and event.ctrl: loop.change_solver_constraint_list("x", -1) return {"RUNNING_MODAL"} # Y附加边流 if event.type == "Y" and event.value == "PRESS" and event.shift: loop.change_solver_constraint_list("y", 1) return {"RUNNING_MODAL"} if event.type == "Y" and event.value == "PRESS" and event.ctrl: loop.change_solver_constraint_list("y", -1) return {"RUNNING_MODAL"} # C 清除约束 if event.type == "C" and event.value == "PRESS": if event.shift: loop.init_solver_constraint( is_init_list=True, is_init_rotation=False, is_init_pattern=False ) elif event.ctrl: loop.init_solver_constraint( is_init_list=False, is_init_rotation=True, is_init_pattern=False ) else: loop.init_solver_constraint() return {"RUNNING_MODAL"} def process_running(self, context, event): if g.mode != g.Mode.NORMAL: return # 边缘建成后,改变长度 if event.ctrl and ( event.type == "WHEELUPMOUSE" or event.type == "WHEELDOWNMOUSE" ): # boundary = functions.get_nearest_boundary_from_mouse(event) boundary = g.hovering_boundary if boundary: if event.type == "WHEELUPMOUSE": boundary.length += 1 if event.type == "WHEELDOWNMOUSE": boundary.length -= 1 return {"RUNNING_MODAL"} # S if event.type == "S" and event.value == "PRESS": g.is_snap_surface = not g.is_snap_surface # 切换吸附状态 if g.hovering_loop and g.hovering_loop.pattern: if g.is_snap_surface: # 吸附曲面 functions.snap_verts_to_surface_along_normal( g.obj, g.obj_bm, g.hovering_loop.pattern.interior_verts, g.snap_bvh_trees, allow_opposite_direction=g.is_snap_opposite, ) else: functions.smooth_verts_by_normal( g.obj, g.obj_bm, g.hovering_loop.pattern.interior_verts ) return {"RUNNING_MODAL"} if event.type == "M" and event.value == "PRESS" and not event.ctrl: g.is_drawing_along_mirror = ( not g.is_drawing_along_mirror ) # 切换镜像绘制状态 if not g.is_drawing_along_mirror: g.is_drawing_along_mirror_persist = False return {"RUNNING_MODAL"} # 删除boundary if ( event.type == "X" and event.value == "PRESS" and not event.shift and not event.ctrl and g.hovering_boundary ): g.hovering_boundary.delete() generate_loop.regenerate_loops() return {"RUNNING_MODAL"} if event.type == "M" and event.value == "PRESS" and event.ctrl: g.is_drawing_along_mirror_persist = ( not g.is_drawing_along_mirror_persist ) # 切换镜像绘制状态 g.is_drawing_along_mirror = g.is_drawing_along_mirror_persist return {"RUNNING_MODAL"} # smooth if event.shift: tobe_smooth_loops = [] if event.ctrl: tobe_smooth_loops = g.loops else: if g.hovering_loop: tobe_smooth_loops = [g.hovering_loop] for tobe_smooth_loop in tobe_smooth_loops: if ( not tobe_smooth_loop.pattern or not tobe_smooth_loop.pattern.interior_verts ): continue to_be_smooth_verts = tobe_smooth_loop.pattern.interior_verts if event.type in ["ONE", "NUMPAD_1"]: functions.smooth_verts_by_avg(g.obj, g.obj_bm, to_be_smooth_verts) return {"RUNNING_MODAL"} elif event.type in ["TWO", "NUMPAD_2"]: functions.smooth_verts_by_normal( g.obj, g.obj_bm, to_be_smooth_verts, smooth_type=0, ) return {"RUNNING_MODAL"} elif event.type in ["THREE", "NUMPAD_3"]: functions.smooth_verts_by_normal( g.obj, g.obj_bm, to_be_smooth_verts, smooth_type=1, ) return {"RUNNING_MODAL"} # self.is_snap_surface = False # v snap oppose,目前只针对path绘制的时候应用 if event.type == "V" and event.value == "PRESS" and g.is_running: if g.is_snap_opposite: g.is_snap_opposite = False else: g.is_snap_opposite = True return {"RUNNING_MODAL"} if event.type == "B" and event.value == "PRESS": g.is_auto_search_pattern = not g.is_auto_search_pattern return {"RUNNING_MODAL"} class EasyPatchSnapSelectedToSurfaceOperator(bpy.types.Operator): bl_idname = f"{const.addon_prefix}.snap_selected_to_surface" bl_label = "Snap Selected Vertices to Surface" # bl_description = t("toggle_driver") bl_options = {"REGISTER", "UNDO"} # 不加UNDO撤回的话,会导致用户前一个步骤也撤回 def execute(self, context): obj = context.object # 检查是否在编辑模式 if obj.mode != "EDIT": self.report({"WARNING"}, "Please run this in edit mode") return {"FINISHED"} # 获取依赖图 depsgraph = context.evaluated_depsgraph_get() # 获取网格数据 bm = bmesh.from_edit_mesh(obj.data) bm.verts.ensure_lookup_table() # 获取所有选中的顶点 selected_vertices = [v for v in bm.verts if v.select] if not selected_vertices: self.report({"WARNING"}, "No Vertices selected") return {"FINISHED"} # 获取所有可见的网格对象(排除当前对象) visible_objects = [ o for o in context.visible_objects if o.type == "MESH" and o != obj and o.visible_get() ] if not visible_objects: self.report({"WARNING"}, "no_visible_target_mesh_to_snap") return {"FINISHED"} # 构建所有可见目标对象的 BVH 树(一次性操作) bvh_trees = functions.get_snap_bvh_trees() functions.snap_verts_to_surface_along_normal( obj, bm, selected_vertices, bvh_trees, allow_opposite_direction=False, ) # 将顶点更改应用到网格上,loop_triangles提高效率 bmesh.update_edit_mesh(obj.data, loop_triangles=True) # self.report({"INFO"}, f"Successfully snap {snap_count} vertices") return {"FINISHED"} def get_nearest_boundary_and_loop_by_co_2d(context, co_2d): nearest_boundary = None nearest_loop = None min_distance = float("inf") # 获取当前视图区域和区域的3D视图 region = context.region rv3d = context.space_data.region_3d # # mid_co方案 # # 遍历boundary列表 # for boundary in g.boundaries: # # 获取boundary的mid_co三维坐标 # mid_co = boundary.mid_co # # 将三维坐标投影到2D屏幕坐标 # screen_coord = view3d_utils.location_3d_to_region_2d(region, rv3d, mid_co) # # 如果投影成功 # if screen_coord: # # 计算鼠标位置与投影点的距离 # dist = (screen_coord[0] - co_2d[0]) ** 2 + (screen_coord[1] - co_2d[1]) ** 2 # if dist < min_distance: # min_distance = dist # nearest_boundary = boundary # TODO: 只有一个edge的用mid_co,不然重叠了捕获不到 # 遍历boundary列表 for boundary in g.boundaries: coords = [v.co for v in boundary.verts] + [boundary.mid_co] # 遍历boundary的每一个顶点 for vert_co in coords: # 将三维坐标投影到2D屏幕坐标 screen_coord = view3d_utils.location_3d_to_region_2d(region, rv3d, vert_co) # 如果投影成功 if screen_coord: # 计算鼠标位置与投影点的距离 dist = (screen_coord[0] - co_2d[0]) ** 2 + ( screen_coord[1] - co_2d[1] ) ** 2 if dist < min_distance: min_distance = dist nearest_boundary = boundary if nearest_boundary: # 获取与nearest_boundary相关的loop connected_loops = functions.get_loops_from_boundary(nearest_boundary) # print(nearest_boundary, connected_loops) if len(connected_loops) == 0: nearest_loop = None elif len(connected_loops) == 1: nearest_loop = connected_loops[0] else: # 找到距离鼠标位置最近的loop min_loop_distance = float("inf") for loop in connected_loops: # 获取loop的mid_co三维坐标 loop_mid_co = loop.mid_co # 将loop的三维坐标投影到2D屏幕坐标 loop_screen_coord = view3d_utils.location_3d_to_region_2d( region, rv3d, loop_mid_co ) # 如果loop投影成功 if loop_screen_coord: # 计算鼠标位置与loop的投影点的距离 loop_dist = (loop_screen_coord[0] - co_2d[0]) ** 2 + ( loop_screen_coord[1] - co_2d[1] ) ** 2 if loop_dist < min_loop_distance: min_loop_distance = loop_dist nearest_loop = loop return nearest_boundary, nearest_loop def get_nearest_boundary_and_loop_by_co_3d(co_3d): nearest_boundary = None nearest_loop = None min_distance = float("inf") # # mid_co方案 # # 遍历boundary列表 # for boundary in g.boundaries: # mid_co = boundary.mid_co # # 计算给定的3D坐标与boundary的mid_co之间的欧几里得距离 # distance = (mid_co - co_3d).length # # 判断是否是最近的boundary # if distance < min_distance: # min_distance = distance # nearest_boundary = boundary # verts方案 for boundary in g.boundaries: coords = [v.co for v in boundary.verts] + [boundary.mid_co] # 遍历boundary的每一个顶点 for vert_co in coords: # 计算给定的3D坐标与顶点坐标之间的欧几里得距离 distance = (vert_co - co_3d).length # 判断是否是最近的boundary if distance < min_distance: min_distance = distance nearest_boundary = boundary if nearest_boundary: connected_loops = functions.get_loops_from_boundary(nearest_boundary) if len(connected_loops) == 0: nearest_loop = None elif len(connected_loops) == 1: nearest_loop = connected_loops[0] else: # 多个loop的情况,计算每个loop的mid_co和目标co_3d之间的3D距离 min_loop_distance = float("inf") for loop in connected_loops: loop_mid_co = loop.mid_co # 计算给定的3D坐标与loop的mid_co之间的欧几里得距离 loop_distance = (loop_mid_co - co_3d).length # 判断是否是最近的loop if loop_distance < min_loop_distance: min_loop_distance = loop_distance nearest_loop = loop return nearest_boundary, nearest_loop def update_hover(context, event): if g.is_changing_mesh: g.hovering_boundary = None g.hovering_loop = None g.is_redraw_hovering = True return current_timestamp = time.time() if current_timestamp - g.last_update_hovering_timestamp < (1 / 10): return # 两个方案,1.boundary的mid_co投影到2d,获取最近的,然后loop挑出b相邻的,再根据mid_co取2d最近的 2.ray_cast,没结果则用方案一,有结果,取距离该点最近的boundary的mid_co,然后再相邻取loop region = context.region rv3d = context.region_data mouse_co_2d = (event.mouse_region_x, event.mouse_region_y) ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, mouse_co_2d) ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, mouse_co_2d) closest_co = None distance_min = float("inf") for bvh_tree in g.snap_bvh_trees: ray_cast_co, _, _, distance = bvh_tree.ray_cast(ray_origin, ray_direction) if distance and distance < distance_min: distance_min = distance closest_co = ray_cast_co hovering_boundary = None hovering_loop = None if closest_co: hovering_boundary, hovering_loop = get_nearest_boundary_and_loop_by_co_3d( closest_co ) else: # boundary hovering_boundary, hovering_loop = get_nearest_boundary_and_loop_by_co_2d( context, mouse_co_2d ) g.hovering_boundary = hovering_boundary g.hovering_loop = hovering_loop g.last_update_hovering_timestamp = current_timestamp g.is_redraw_hovering = True classes = (EasyPatchModalOperator, EasyPatchSnapSelectedToSurfaceOperator)