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

786 lines
24 KiB
Python
Raw Permalink 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 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()