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()