Files
blender-easy-patch/op.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

709 lines
25 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.

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)