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

785
draw.py Normal file
View File

@@ -0,0 +1,785 @@
from __future__ import annotations
import bpy
import blf
import gpu
import bmesh
from bpy_extras import view3d_utils
from . import g
from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_circle_2d
from .utils import functions
import random
from .utils.trans import t
import mathutils
import itertools
import traceback
import time
from .utils import functions
from . import const
if bpy.app.version[0] == 3 or bpy.app.version[0] == 4:
import bgl
def draw_desc(self, context):
prefs = functions.get_preferences()
if prefs.desc_position_y == 100 or prefs.desc_position_x == 100:
return
font_id = 0
region = context.region
width, height = region.width, region.height
blf.color(font_id, 1, 1, 1, 0.6)
font_size = self.get_font_size()
self.set_font_size(font_size)
# base_x, base_y = width / 15, height * 7 / 8
base_x = width * prefs.desc_position_x / 100
base_y = height * (100 - prefs.desc_position_y) / 100
def position_generator(base_x, base_y, line_height):
while True:
blf.position(font_id, base_x, base_y, 0)
yield
base_y -= line_height
pos_gen = position_generator(base_x, base_y, font_size)
if g.mode == g.Mode.NORMAL:
next(pos_gen)
blf.draw(font_id, f"{t('current_status')}: {t('NORMAL')}")
next(pos_gen)
next(pos_gen)
blf.draw(font_id, f"{t('click_drag_to_draw_line')}")
next(pos_gen)
blf.draw(font_id, f"{t('hold_ctrl')} - {t('snap_vert')}")
next(pos_gen)
blf.draw(
font_id, f"{t('hold_ctrl')}+{t('hold_shift')} - {t('snap_exist_path')}"
)
next(pos_gen)
blf.draw(
font_id,
f"M {t('key')} - {t('draw_on_axis_of_symmetry')}{'('+t('persist')+')' if g.is_drawing_along_mirror_persist else ''}: {t('enable') if g.is_drawing_along_mirror else t('disable')}",
)
next(pos_gen)
blf.draw(
font_id,
f"B {t('key')} - {t('skip_no_solution_pattern')}: {t('enable') if g.is_auto_search_pattern else t('disable')}",
)
next(pos_gen)
blf.draw(
font_id,
f"S {t('key')} - {t('snap_surface')}: {t('enable') if g.is_snap_surface else t('disable')}",
)
if g.mode == g.Mode.NORMAL:
next(pos_gen)
blf.draw(
font_id,
f"V {t('key')} - {t('snap_opposite')}: {t('enable') if g.is_snap_opposite else t('disable')}",
)
if g.hovering_boundary:
next(pos_gen)
blf.draw(font_id, f"X {t('key')} - {t('delete_highlight_edge')}")
if g.hovering_loop:
loop = g.hovering_loop
next(pos_gen)
next(pos_gen)
blf.draw(font_id, f"Ctrl+{t('scroll')} - {t('change_segment')}")
next(pos_gen)
blf.draw(font_id, f"Shift+{t('scroll')} - {t('change_padding')}")
next(pos_gen)
blf.draw(font_id, f"T {t('key')} - {t('switch_pattern')}")
next(pos_gen)
blf.draw(font_id, f"R {t('key')} - {t('rotate_pattern')}")
if g.hovering_loop.pattern:
next(pos_gen)
blf.draw(
font_id,
f"Shift + 1,2,3 - {t('smooth_mesh')}",
)
next(pos_gen)
blf.draw(
font_id,
f"i {t('key')} - {t('faces_normal_toward_viewport')}",
)
if g.hovering_loop.pattern:
pattern = loop.pattern
next(pos_gen)
next(pos_gen)
blf.draw(
font_id,
f"{t('solver_solution')}: {[int(i) for i in pattern.solution]}",
)
next(pos_gen)
blf.draw(font_id, f"{t('current_para')}:")
next(pos_gen)
blf.draw(font_id, f"{t('pattern')}: {pattern.name}")
next(pos_gen)
blf.draw(
font_id,
f"{t('rotation')}: {pattern.rotation}",
)
else:
next(pos_gen)
next(pos_gen)
blf.color(font_id, 1, 0.5, 0.5, 0.6)
blf.draw(
font_id,
f"{t('solver_solution')}: {loop.solver_msg}",
)
#
if (
any([i for i in loop.solver_constraint_list if i is not None])
or loop.solver_constraint_rotation is not None
or loop.solver_constraint_pattern
):
blf.color(font_id, 1, 0.5, 0.5, 0.6)
next(pos_gen)
next(pos_gen)
blf.draw(
font_id,
f"{t('solver_constraint')}:",
)
next(pos_gen)
blf.draw(
font_id,
f"{t('pattern')}: {loop.solver_constraint_pattern.name}",
)
next(pos_gen)
blf.draw(
font_id,
f"{t('rotation')}: {loop.solver_constraint_rotation}",
)
next(pos_gen)
blf.draw(
font_id,
f"{t('var_constraint')}: {[i if i is not None else '' for i in loop.solver_constraint_list]}",
)
next(pos_gen)
blf.draw(
font_id,
f"C {t('key')} {t('clear_constraint')}",
)
next(pos_gen)
blf.draw(
font_id,
f"Shift + C {t('only_clear_var')}",
)
next(pos_gen)
blf.draw(
font_id,
f"Ctrl + C {t('only_clear_rotation')}",
)
elif g.mode == g.Mode.DRAWING:
next(pos_gen)
blf.draw(font_id, f"{t('current_status')}: {t('DRAWING')}")
next(pos_gen)
next(pos_gen)
next(pos_gen)
blf.draw(font_id, f"{t('right_click')}: {t('cancel_this_draw')}")
next(pos_gen)
blf.draw(font_id, f"{t('left_click_release')}: {t('apply_this_draw')}")
next(pos_gen)
blf.draw(font_id, f"{t('scroll')} - {t('change_segment')}")
elif g.mode == g.Mode.MERGE_BOUNDARY:
next(pos_gen)
blf.draw(font_id, f"{t('current_status')}: {t('MERGE_BOUNDARY')}")
next(pos_gen)
next(pos_gen)
blf.color(font_id, 1, 1, 1, 0.6)
next(pos_gen)
next(pos_gen)
next(pos_gen)
blf.draw(font_id, f"{t('enter')} - {t('apply_mesh')}")
next(pos_gen)
blf.draw(font_id, f"ESC - {t('quit')}")
def draw_snap_vertex(self, context):
if (
not g.drawing_verts_snap_vertex
or g.drawing_verts_snap_vertex not in g.obj_bm.verts
):
return
location_2d = view3d_utils.location_3d_to_region_2d(
context.region,
context.region_data,
g.obj.matrix_world @ g.drawing_verts_snap_vertex.co,
)
if not location_2d:
return
if location_2d:
font_size = self.get_font_size() // 2
self.set_line_width(1)
draw_circle_2d(location_2d, (1, 1, 1, 1), font_size)
def draw_hovering_loop_3d(self):
if g.mode != g.Mode.NORMAL:
return
hovering_loop = g.hovering_loop
if not hovering_loop:
return
verts = hovering_loop.verts
mid_co = sum((g.obj.matrix_world @ v.co for v in verts), mathutils.Vector()) / len(
verts
)
coords = [mid_co] + [g.obj.matrix_world @ v.co for v in verts]
indices = []
for i in range(len(verts)):
if i == len(verts) - 1:
indices.append((0, i + 1, 1))
else:
indices.append((0, i + 1, i + 2))
shader = self.shader_3d_uniform_color
gpu.state.blend_set("ALPHA")
batch = batch_for_shader(shader, "TRIS", {"pos": coords}, indices=indices)
shader.bind()
shader.uniform_float("color", (0, 0.6, 0, 0.3))
batch.draw(shader)
def draw_hovering_boundary(self):
if g.mode != g.Mode.NORMAL:
return
hovering_boundary = g.hovering_boundary
if not hovering_boundary:
return
path = [g.obj.matrix_world @ v.co for v in hovering_boundary.verts]
line_co_pairs = []
for i in range(len(path) - 1):
line_co_pairs.append((path[i], path[i + 1]))
draw_lines_3d(self, line_co_pairs, (1, 1, 1, 1), 5)
def draw_loop(self):
for loop in g.loops:
for idx, (boundary, is_forward) in enumerate(loop.b_and_d_list):
region = bpy.context.region
rv3d = bpy.context.region_data
coord_2d = view3d_utils.location_3d_to_region_2d(
region, rv3d, boundary.mid_co
)
if coord_2d is not None:
text = str(idx)
if not is_forward:
text += " R"
# 绘制索引
blf.size(0, 50)
blf.position(0, coord_2d[0], coord_2d[1], 0)
blf.color(0, 0, 1, 0, 1)
blf.draw(0, text)
def draw_temp_patch_boundary(self):
boundary = g.drawing_path_boundary
if not boundary:
return
# draw path
vertex_co_along_path, path = boundary.get_actual_verts_co_and_path()
shader = self.shader_3d_uniform_color
gpu.state.blend_set("ALPHA")
self.set_line_width(5)
batch = batch_for_shader(shader, "LINE_STRIP", {"pos": path})
shader.bind()
shader.uniform_float("color", (0.0, 0.0, 1, 0.5))
batch.draw(shader)
# draw vertex
# vertex_co_along_path = patch_boundary.vertices_co_along_path()
for vertex_co in vertex_co_along_path:
# TODO: vulkun 用这个shader的时候gpu.state.point_size_set 失效了等待修复或别的方案。因为新shader POINT_UNIFORM_COLOR只能画2d顶点
batch = batch_for_shader(shader, "POINTS", {"pos": [vertex_co]})
shader.uniform_float("color", (1, 1, 1, 1))
batch.draw(shader)
def draw_boundaries_3d(self):
if not g.boundaries:
return
# edges
if g.is_redraw_boundary or not g.last_boundary_data:
draw_boundary_data_list = []
for boundary in g.boundaries:
line_color = functions.hsv_to_rgb(
functions.stable_random_from_obj(boundary), 1, 1, 1
)
boundaries_verts_co = boundary.vertices_co
boundaries_verts_co = [g.obj.matrix_world @ i for i in boundaries_verts_co]
line_co_pairs = []
vert_coords = boundaries_verts_co
for i in range(len(boundaries_verts_co) - 1):
line_co_pair = (boundaries_verts_co[i], boundaries_verts_co[i + 1])
line_co_pairs.append(line_co_pair)
draw_boundary_data_list.append((line_co_pairs, line_color, vert_coords))
draw_lines_3d(self, line_co_pairs, line_color, line_width=2.5)
draw_points_3d(self, vert_coords, (1, 1, 1, 1), point_size=5)
g.last_boundary_data = draw_boundary_data_list
g.is_redraw_boundary = False
else:
for boundary_data in g.last_boundary_data:
line_co_pairs, line_color, vert_coords = boundary_data
draw_lines_3d(self, line_co_pairs, line_color, line_width=2.5)
draw_points_3d(self, vert_coords, (1, 1, 1, 1), point_size=5)
def draw_boundaries_text_2d(self):
font_id = 0
font_size = self.get_font_size()
context = bpy.context
rv3d = context.region_data
region = context.region
# 绘制边数
for boundary in g.boundaries:
coord_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, boundary.mid_co)
if coord_2d is None:
return
x, y = coord_2d
blf.position(font_id, x, y, 0)
blf.color(font_id, 1, 1, 1, 1)
self.set_font_size(font_size)
blf.draw(font_id, f"{boundary.length}")
# 绘制建议文本
for loop in g.loops:
for i, boundary in enumerate(loop.boundaries):
boundary_mid_co_2d = view3d_utils.location_3d_to_region_2d(
region, rv3d, boundary.mid_co
)
loop_mid_co_2d = view3d_utils.location_3d_to_region_2d(
region, rv3d, loop.mid_co
)
offset = mathutils.Vector((0, 0))
if (
boundary_mid_co_2d.x <= loop_mid_co_2d.x
and boundary_mid_co_2d.y <= loop_mid_co_2d.y
):
result = mathutils.Vector((1, 1)) # a在b的左下角
elif (
boundary_mid_co_2d.x >= loop_mid_co_2d.x
and boundary_mid_co_2d.y <= loop_mid_co_2d.y
):
result = mathutils.Vector((-3, 1)) # a在b的右下角
elif (
boundary_mid_co_2d.x <= loop_mid_co_2d.x
and boundary_mid_co_2d.y >= loop_mid_co_2d.y
):
result = mathutils.Vector((1, -1)) # a在b的左上角
elif (
boundary_mid_co_2d.x >= loop_mid_co_2d.x
and boundary_mid_co_2d.y >= loop_mid_co_2d.y
):
result = mathutils.Vector((-3, -1)) # a在b的右上角
offset_value = 20
offset = result * offset_value
# 建议文本
suggest_text = None
if loop.suggest_solution:
pos_value = loop.suggest_solution[i]
neg_value = loop.suggest_solution[i + len(loop.boundaries)]
if pos_value > 0:
suggest_text = f"+{int(pos_value)}"
if neg_value > 0:
suggest_text = f"{-int(neg_value)}"
suggest_text_co_3d = boundary.mid_co
suggest_text_co_2d = view3d_utils.location_3d_to_region_2d(
region, rv3d, suggest_text_co_3d
)
if not suggest_text_co_2d:
continue
suggest_text_co_2d = suggest_text_co_2d + offset
constraint_text = None
constraint_value = loop.solver_constraint_list[i]
if loop.solver_constraint_list[i] is not None:
constraint_text = f"{t('padding')}:{int(constraint_value)}"
draw_patch_boundary_text(
self,
suggest_text_co_2d,
suggest_text=suggest_text,
constraint_text=constraint_text,
)
def draw_patch_pattern(self):
font_id = 0
if g.is_redraw_pattern:
for loop in g.loops:
pattern = loop.pattern
if not loop.pattern:
continue
color = (1, 1, 1, 0.5)
verts = pattern.interior_verts
edges = pattern.interior_edges
if verts:
pass
# 减轻压力
# draw_verts_2d(self, verts, color, 5)
line_co_pairs = []
for edge in edges:
line_co_pair = (
g.obj.matrix_world @ edge.verts[0].co,
g.obj.matrix_world @ edge.verts[1].co,
)
line_co_pairs.append(line_co_pair)
if line_co_pairs:
draw_lines_3d(self, line_co_pairs, color, line_width=2)
def draw_lines_3d(self, line_co_pairs, color, line_width=2):
if not line_co_pairs:
return
line_co_pair_flatten = list(itertools.chain(*line_co_pairs))
shader = self.shader_3d_uniform_color
gpu.state.blend_set("ALPHA")
self.set_line_width(line_width)
batch = batch_for_shader(shader, "LINES", {"pos": line_co_pair_flatten})
shader.bind()
shader.uniform_float("color", color)
batch.draw(shader)
def draw_points_3d(self, vert_coords, color, point_size=3):
if not vert_coords:
return
if bpy.app.version < (4, 5, 0):
shader = self.shader_3d_uniform_color
gpu.state.blend_set("ALPHA")
self.set_point_size(point_size)
batch = batch_for_shader(shader, "POINTS", {"pos": vert_coords})
shader.bind()
shader.uniform_float("color", color)
batch.draw(shader)
else:
shader = self.shader_point_uniform_color
batch = batch_for_shader(shader, "POINTS", {"pos": vert_coords})
shader.uniform_float("color", color)
gpu.state.point_size_set(point_size)
batch.draw(shader)
def draw_edges_2d(self, edges, color):
if not edges:
return
edges_co = [
tuple(g.obj.matrix_world @ vert.co for vert in edge.verts) for edge in edges
]
context = bpy.context
region = context.region
rv3d = context.region_data
coords_3d = edges_co
coords_2d = [
functions.coords_3d_to_2d(region, rv3d, sublist) for sublist in coords_3d
]
if None in coords_2d:
return
coords_2d = list(itertools.chain.from_iterable(coords_2d))
shader = self.shader_2d_uniform_color
batch = batch_for_shader(shader, "LINES", {"pos": coords_2d})
shader.bind()
shader.uniform_float("color", color)
batch.draw(shader)
def draw_path_2d(self, path, color, line_width=1):
if not path:
return
edges_co = [(path[i], path[i + 1]) for i in range(len(path) - 1)]
context = bpy.context
region = context.region
rv3d = context.region_data
coords_3d = edges_co
coords_2d = [
[
(view3d_utils.location_3d_to_region_2d(region, rv3d, item) or (None, None))
for item in sublist
if view3d_utils.location_3d_to_region_2d(region, rv3d, item) is not None
]
for sublist in coords_3d
]
if None in coords_2d:
return
coords_2d = list(itertools.chain.from_iterable(coords_2d))
self.set_line_width(line_width)
shader = self.shader_2d_uniform_color
batch = batch_for_shader(shader, "LINES", {"pos": coords_2d})
shader.bind()
shader.uniform_float("color", color)
batch.draw(shader)
def draw_patch_boundary_text(self, coord_2d, suggest_text, constraint_text):
font_id = 0
font_size = self.get_font_size()
# text
if coord_2d is None:
return
x, y = coord_2d
self.set_font_size(font_size * (2 / 3))
offset = font_size
if suggest_text is not None:
blf.position(font_id, x, y - offset, 0)
blf.color(font_id, 0, 1, 0, 1)
blf.draw(font_id, str(suggest_text))
offset += font_size
if constraint_text is not None:
blf.position(font_id, x, y, 0)
blf.color(font_id, 1, 0, 0, 1)
blf.draw(font_id, str(constraint_text))
self.set_font_size(font_size)
class Draw:
def __init__(self):
self.title_handler = None
self.temp_handler = None
self.snap_vertex_handler = None
self._draw_2d_handler = None
self._draw_3d_handler = None
def get_font_size(self):
region = bpy.context.region
width, height = region.width, region.height
font_size = max(10, min(width, height) // 50)
return font_size
def draw_2d(self, context):
if repr(self.op_instance).endswith("invalid>"):
self.clear()
return
if g.is_changing_mesh:
return
if not g.is_running:
self.clear()
return
try:
start_time = time.time()
# draw_hovering_boundary_2d(self)
draw_hovering_boundary_2d_time = time.time() - start_time
draw_desc(self, context)
start_time = time.time()
draw_snap_vertex(self, context)
draw_snap_vertex_time = time.time() - start_time
start_time = time.time()
# draw_boundaries(self)
draw_boundaries_time = time.time() - start_time
draw_boundaries_text_2d(self)
start_time = time.time()
# draw_patch_pattern(self)
draw_patch_pattern_time = time.time() - start_time
# last_echo_time = g.last_echo_time if g.last_echo_time else 0
# if time.time() - last_echo_time > 0.1:
# print(
# f"{draw_hovering_boundary_2d_time:.3f}",
# f"{draw_snap_vertex_time:.3f}",
# f"{draw_boundaries_time:.3f}",
# f"{draw_patch_pattern_time:.3f}",
# )
# g.last_echo_time = time.time()
except Exception as e:
self.clear()
self.process_exception(e)
# draw_loop(self) # debug
def draw_3d(self, context):
if repr(self.op_instance).endswith("invalid>"):
self.clear()
return
if g.is_changing_mesh:
return
if not g.is_running:
self.clear()
return
try:
draw_hovering_boundary(self)
draw_hovering_loop_3d(self)
draw_temp_patch_boundary(self)
draw_patch_pattern(self)
draw_boundaries_3d(self)
except Exception as e:
self.clear()
self.process_exception(e)
# raise e
def process_exception(self, e):
error_info = traceback.format_exc()
functions.log(error_info)
# tab退出时bm删除这里执行循环很快还来不及清空会抛出ReferenceError这个情况不向ui抛出错误
if isinstance(e, ReferenceError):
error_info = traceback.format_exc()
pass
else:
g.running_exception = e
# raise e
pass
def set_line_width(self, line_width):
if bpy.app.version[0] == 3:
# 3.x
bgl.glLineWidth(line_width)
else:
# 4.x 5.x
gpu.state.line_width_set(line_width)
def set_point_size(self, point_size):
if bpy.app.version[0] == 3:
# 3.x
bgl.glPointSize(point_size)
else:
# 4.x 5.x
gpu.state.point_size_set(point_size)
def set_font_size(self, font_size):
font_id = 0
if bpy.app.version[0] == 3:
# # 3.x got 3
blf.size(font_id, font_size, 72)
else:
# 4.0 got 2
blf.size(font_id, font_size)
def start(self, op_instance):
functions.log("start draw")
self.op_instance = op_instance
if bpy.app.version[0] == 3:
if bpy.app.version[0] >= 6:
# 3.6
self.shader_3d_uniform_color = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader_2d_uniform_color = gpu.shader.from_builtin(
"2D_UNIFORM_COLOR"
)
else:
# 3.3
self.shader_3d_uniform_color = gpu.shader.from_builtin(
"3D_UNIFORM_COLOR"
)
self.shader_2d_uniform_color = gpu.shader.from_builtin(
"2D_UNIFORM_COLOR"
)
else:
# 4.0 5.x
self.shader_3d_uniform_color = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader_2d_uniform_color = gpu.shader.from_builtin("UNIFORM_COLOR")
if bpy.app.version >= (4, 5, 0):
self.shader_point_uniform_color = gpu.shader.from_builtin("POINT_UNIFORM_COLOR")
if not self._draw_2d_handler:
self._draw_2d_handler = bpy.types.SpaceView3D.draw_handler_add(
self.draw_2d,
(bpy.context,),
"WINDOW",
"POST_PIXEL",
)
if not self._draw_3d_handler:
self._draw_3d_handler = bpy.types.SpaceView3D.draw_handler_add(
self.draw_3d,
(bpy.context,),
"WINDOW",
"POST_VIEW",
)
def clear(self):
functions.log("darw cleared")
if self._draw_2d_handler:
bpy.types.SpaceView3D.draw_handler_remove(self._draw_2d_handler, "WINDOW")
self._draw_2d_handler = None
if self._draw_3d_handler:
bpy.types.SpaceView3D.draw_handler_remove(self._draw_3d_handler, "WINDOW")
self._draw_3d_handler = None
draw = Draw()