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配置文件
This commit is contained in:
2026-03-03 19:24:57 +08:00
commit ab91b120e6
44 changed files with 17551 additions and 0 deletions

708
op.py Normal file
View File

@@ -0,0 +1,708 @@
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)