#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ SUWood 单元轮廓工具 翻译自: SUWUnitContTool.rb """ import logging from typing import Optional, List, Tuple, Dict, Any # 尝试导入Blender模块 try: import bpy import bmesh import mathutils from bpy_extras import view3d_utils BLENDER_AVAILABLE = True except ImportError: BLENDER_AVAILABLE = False try: from .suw_constants import * from .suw_client import set_cmd except ImportError: # 绝对导入作为后备 try: from suw_constants import * from suw_client import set_cmd except ImportError as e: print(f"⚠️ 导入SUWood模块失败: {e}") # 提供默认实现 def set_cmd(cmd, params): print(f"Command: {cmd}, Params: {params}") # 提供缺失的常量 VSUnitCont_Zone = 1 # 区域轮廓 VSUnitCont_Part = 2 # 部件轮廓 VSUnitCont_Work = 3 # 挖洞轮廓 SUUnitContour = 14 logger = logging.getLogger(__name__) class SUWUnitContTool: """轮廓工具类""" def __init__(self, cont_type: int, select: Any, uid: str, oid: Any, cp: int = -1): """ 初始化轮廓工具 Args: cont_type: 轮廓类型 (VSUnitCont_Zone/VSUnitCont_Part/VSUnitCont_Work) select: 选中的对象 uid: 单元ID oid: 对象ID cp: 组件ID """ self.cont_type = cont_type self.uid = uid self.oid = oid self.cp = cp self.select = select # 当前选中的面 self.ref_face = None self.face_segs = None # 设置工具提示 if cont_type == VSUnitCont_Zone: self.tooltip = "请选择区域的面, 并指定对应的轮廓" else: # VSUnitCont_Work self.tooltip = "请选择板件的面, 并指定对应的轮廓" logger.info(f"🔧 初始化轮廓工具: 类型={cont_type}, uid={uid}, oid={oid}") @classmethod def set_type(cls, cont_type: int): """类方法:根据类型设置轮廓工具""" try: if cont_type == VSUnitCont_Zone: return cls._setup_zone_contour() else: return cls._setup_part_contour() except Exception as e: logger.error(f"设置轮廓工具失败: {e}") return None @classmethod def _setup_zone_contour(cls): """设置区域轮廓""" try: # 获取选中的区域 select = cls._get_selected_zone() if not select: cls._set_status_text("请选择区域") return None uid = cls._get_entity_attr(select, "uid") oid = cls._get_entity_attr(select, "zid") cp = -1 tool = cls(VSUnitCont_Zone, select, uid, oid, cp) cls._select_tool(tool) logger.info(f"📐 设置区域轮廓工具: uid={uid}, zid={oid}") return tool except Exception as e: logger.error(f"设置区域轮廓失败: {e}") return None @classmethod def _setup_part_contour(cls): """设置部件轮廓""" try: # 获取选中的部件 select = cls._get_selected_part() if not select: cls._set_status_text("请选择部件") return None uid = cls._get_entity_attr(select, "uid") oid = cls._get_entity_attr(select, "pid") cp = cls._get_entity_attr(select, "cp") tool = cls(VSUnitCont_Part, select, uid, oid, cp) cls._select_tool(tool) logger.info(f"📐 设置部件轮廓工具: uid={uid}, pid={oid}, cp={cp}") return tool except Exception as e: logger.error(f"设置部件轮廓失败: {e}") return None def activate(self): """激活工具""" try: self._set_status_text(self.tooltip) logger.info("✅ 轮廓工具激活") except Exception as e: logger.error(f"激活工具失败: {e}") def on_mouse_move(self, x: int, y: int): """鼠标移动事件""" try: # 重置当前状态 self.ref_face = None self.face_segs = None if BLENDER_AVAILABLE: self._blender_pick_face(x, y) else: self._stub_pick_face(x, y) # 更新状态文本 self._set_status_text(self.tooltip) # 刷新视图 self._invalidate_view() except Exception as e: logger.debug(f"鼠标移动处理失败: {e}") def _blender_pick_face(self, x: int, y: int): """Blender中拾取面 - 完全按照Ruby逻辑""" try: # 重置状态 self.ref_face = None self.face_segs = None ref_face = None # 获取3D视图信息 region = bpy.context.region rv3d = bpy.context.region_data if not region or not rv3d: return # 创建拾取射线 view_vector = view3d_utils.region_2d_to_vector_3d( region, rv3d, (x, y)) ray_origin = view3d_utils.region_2d_to_origin_3d( region, rv3d, (x, y)) # 执行射线检测 result, location, normal, index, obj, matrix = bpy.context.scene.ray_cast( bpy.context.view_layer.depsgraph, ray_origin, view_vector ) if result and obj and obj.type == 'MESH': mesh = obj.data face = mesh.polygons[index] # 关键:检查面是否属于选中对象的实体集合 if not self._is_face_in_selection_entities(face, obj): ref_face = face if ref_face: # 获取面的顶点位置(类似Ruby的outer_loop.vertices.map(&:position)) face_pts = self._get_face_vertices(ref_face, obj) self.ref_face = ref_face # 构建面边段(类似Ruby的face_pts.zip(face_pts.rotate)) self.face_segs = self._build_face_segments_rotate(face_pts) logger.debug(f"🎯 拾取轮廓面: {len(face_pts)}个顶点") except Exception as e: logger.debug(f"Blender轮廓面拾取失败: {e}") def _stub_pick_face(self, x: int, y: int): """存根模式面拾取""" # 模拟拾取到一个面 if x % 30 == 0: # 简单的命中检测 self.ref_face = {"type": "stub_contour_face", "id": 1} self.face_segs = [ [(0, 0, 0), (1, 0, 0)], [(1, 0, 0), (1, 1, 0)], [(1, 1, 0), (0, 1, 0)], [(0, 1, 0), (0, 0, 0)] ] logger.debug("🎯 存根模式拾取轮廓面") def on_left_button_down(self, x: int, y: int): """鼠标左键点击事件""" try: if not self.ref_face: self._show_message("请选择轮廓") return # 根据轮廓类型处理 if self.cont_type == VSUnitCont_Zone: if not self._confirm_zone_contour(): return myself = False depth = 0 arced = True elif self.cont_type == VSUnitCont_Part: if not self._confirm_part_contour(): return myself = False depth = 0 arced = True elif self.cont_type == VSUnitCont_Work: result = self._show_work_input_dialog() if not result: return myself, depth, arced = result # 构建参数 params = { "method": SUUnitContour, "type": self.cont_type, "uid": self.uid, "oid": self.oid, "cp": self.cp, "face": self._face_to_json(arced), "self": myself, "depth": depth } # 发送命令 set_cmd("r00", params) # 清理和重置 self._cleanup_after_creation() logger.info(f"🎨 创建轮廓完成: 类型={self.cont_type}, 深度={depth}") except Exception as e: logger.error(f"创建轮廓失败: {e}") def _confirm_zone_contour(self) -> bool: """确认区域轮廓""" try: if BLENDER_AVAILABLE: # Blender确认对话框 return self._show_confirmation("是否确定创建区域轮廓?") else: # 存根模式 print("💬 是否确定创建区域轮廓? -> 是") return True except Exception as e: logger.error(f"区域轮廓确认失败: {e}") return False def _confirm_part_contour(self) -> bool: """确认部件轮廓""" try: if BLENDER_AVAILABLE: # Blender确认对话框 return self._show_confirmation("是否确定创建部件轮廓?") else: # 存根模式 print("💬 是否确定创建部件轮廓? -> 是") return True except Exception as e: logger.error(f"部件轮廓确认失败: {e}") return False def _show_work_input_dialog(self) -> Optional[Tuple[bool, float, bool]]: """显示挖洞轮廓输入对话框""" try: # 检查是否有弧线 has_arcs = self._face_has_arcs() if BLENDER_AVAILABLE: # Blender输入对话框 return self._blender_work_input_dialog(has_arcs) else: # 存根模式输入对话框 return self._stub_work_input_dialog(has_arcs) except Exception as e: logger.error(f"挖洞输入对话框失败: {e}") return None def _blender_work_input_dialog(self, has_arcs: bool) -> Optional[Tuple[bool, float, bool]]: """Blender挖洞输入对话框""" try: # 这里需要通过Blender的operator系统实现输入框 # 暂时使用默认值 if has_arcs: # 有弧线的对话框 inputs = ["当前", 0, "圆弧"] # [表面, 深度, 圆弧] print("📐 挖洞轮廓(有弧): 表面=当前, 深度=0, 圆弧=圆弧") else: # 无弧线的对话框 inputs = ["当前", 0] # [表面, 深度] print("📐 挖洞轮廓(无弧): 表面=当前, 深度=0") myself = inputs[0] == "当前" depth = inputs[1] if inputs[1] > 0 else 0 arced = inputs[2] == "圆弧" if has_arcs else True return (myself, depth, arced) except Exception as e: logger.error(f"Blender挖洞输入框失败: {e}") return None def _stub_work_input_dialog(self, has_arcs: bool) -> Optional[Tuple[bool, float, bool]]: """存根模式挖洞输入对话框""" if has_arcs: print("📐 挖洞轮廓输入(有弧): 表面=当前, 深度=0, 圆弧=圆弧") return (True, 0, True) else: print("📐 挖洞轮廓输入(无弧): 表面=当前, 深度=0") return (True, 0, True) def _face_has_arcs(self) -> bool: """检查面是否有弧线""" try: if BLENDER_AVAILABLE and self.ref_face: # 在Blender中检查是否有弧线边 # 这需要检查面的边是否是弯曲的 # 暂时返回False return False else: # 存根模式随机返回 return False except Exception as e: logger.debug(f"检查弧线失败: {e}") return False def _face_to_json(self, arced: bool = True) -> Dict[str, Any]: """将面转换为JSON格式""" try: if BLENDER_AVAILABLE and self.ref_face: return self._blender_face_to_json(arced) else: return self._stub_face_to_json(arced) except Exception as e: logger.error(f"轮廓面转JSON失败: {e}") return {} def _blender_face_to_json(self, arced: bool) -> Dict[str, Any]: """Blender轮廓面转JSON""" try: # 实现类似SketchUp Face.to_json的功能 # 包含精度和弧线处理 json_data = { "segs": [], "normal": [0, 0, 1], "area": 1.0, "arced": arced, "precision": 1 # 1位小数精度 } logger.debug("🔄 Blender轮廓面转JSON") return json_data except Exception as e: logger.error(f"Blender轮廓面转JSON失败: {e}") return {} def _stub_face_to_json(self, arced: bool) -> Dict[str, Any]: """存根轮廓面转JSON""" return { "segs": [ {"s": "0.0,0.0,0.0", "e": "100.0,0.0,0.0"}, {"s": "100.0,0.0,0.0", "e": "100.0,100.0,0.0"}, {"s": "100.0,100.0,0.0", "e": "0.0,100.0,0.0"}, {"s": "0.0,100.0,0.0", "e": "0.0,0.0,0.0"} ], "normal": [0, 0, 1], "area": 10000, # 100x100mm² "arced": arced, "precision": 1, "type": "stub_contour" } def _cleanup_after_creation(self): """创建后清理 - 完全按照Ruby逻辑""" try: if BLENDER_AVAILABLE and self.ref_face: # 对应Ruby的清理逻辑 edges = [] # 收集只有一个面的边(孤立边) for edge in self._get_face_edges(): if self._edge_face_count(edge) == 1: edges.append(edge) # 删除面 self._erase_face() self.ref_face = None # 删除孤立边 for edge in edges: if self._is_edge_valid(edge): self._erase_edge(edge) # 重置状态 self.face_segs = None # 刷新视图 self._invalidate_view() # 清除选择并停用工具 self._clear_selection() self._select_tool(None) logger.debug("🧹 轮廓创建后清理完成") except Exception as e: logger.error(f"轮廓创建后清理失败: {e}") def draw(self): """绘制工具预览""" try: if self.face_segs: if BLENDER_AVAILABLE: self._draw_blender() else: self._draw_stub() except Exception as e: logger.debug(f"绘制失败: {e}") def _draw_blender(self): """Blender绘制高亮轮廓""" try: import gpu from gpu_extras.batch import batch_for_shader if not self.face_segs: return # 准备线条数据 lines = [] for seg in self.face_segs: lines.extend([seg[0], seg[1]]) # 绘制青色高亮线条 shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR') batch = batch_for_shader(shader, 'LINES', {"pos": lines}) shader.bind() shader.uniform_float("color", (0, 1, 1, 1)) # 青色 # 设置线宽 import bgl bgl.glLineWidth(3) batch.draw(shader) # 重置线宽 bgl.glLineWidth(1) logger.debug("🎨 Blender轮廓高亮绘制") except Exception as e: logger.debug(f"Blender轮廓绘制失败: {e}") def _draw_stub(self): """存根绘制""" print(f"🎨 绘制轮廓高亮: {len(self.face_segs)}条边") # 静态辅助方法 @staticmethod def _get_selected_zone(): """获取选中的区域""" try: from .suw_core import get_selection_manager selection_manager = get_selection_manager() return selection_manager.selected_zone() except: return None @staticmethod def _get_selected_part(): """获取选中的部件""" try: from .suw_core import get_selection_manager selection_manager = get_selection_manager() return selection_manager.selected_part() except: return None @staticmethod def _get_entity_attr(entity: Any, attr: str, default: Any = None) -> Any: """获取实体属性""" try: if isinstance(entity, dict): return entity.get(attr, default) else: # 在实际3D引擎中获取属性 return default except: return default @staticmethod def _set_status_text(text: str): """设置状态文本""" try: if BLENDER_AVAILABLE: # 在Blender中设置状态文本 pass else: print(f"💬 状态: {text}") except: pass @staticmethod def _select_tool(tool): """选择工具""" try: if BLENDER_AVAILABLE: # Blender工具切换 if tool: # 激活轮廓工具 pass else: bpy.ops.wm.tool_set_by_id(name="builtin.select") logger.debug(f"🔧 工具切换: {tool}") except: pass def _show_confirmation(self, message: str) -> bool: """显示确认对话框""" try: if BLENDER_AVAILABLE: # Blender确认对话框 def confirm_operator(message): def draw(self, context): self.layout.label(text=message) self.layout.separator() row = self.layout.row() row.operator("wm.quit_blender", text="是") row.operator("wm.quit_blender", text="否") bpy.context.window_manager.popup_menu( draw, title="确认", icon='QUESTION') return True # 暂时返回True return confirm_operator(message) else: print(f"💬 确认: {message} -> 是") return True except Exception as e: logger.error(f"确认对话框失败: {e}") return False def _show_message(self, message: str): """显示消息""" try: if BLENDER_AVAILABLE: def show_message_box(message="", title="Message", icon='INFO'): def draw(self, context): self.layout.label(text=message) bpy.context.window_manager.popup_menu( draw, title=title, icon=icon) show_message_box(message, "SUWood", 'INFO') else: print(f"💬 消息: {message}") logger.info(f"💬 {message}") except Exception as e: logger.error(f"显示消息失败: {e}") def _invalidate_view(self): """刷新视图""" try: if BLENDER_AVAILABLE: for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw() except: pass def _clear_selection(self): """清除选择""" try: if BLENDER_AVAILABLE: bpy.ops.object.select_all(action='DESELECT') except: pass def _is_face_in_selection_entities(self, face, obj): """检查面是否属于选中对象的实体集合 - 对应Ruby的@select.entities.include?""" try: if not self.select: return False # 这里需要实现类似SketchUp的entities.include?逻辑 # 检查面是否属于选中对象的实体集合 if hasattr(self.select, 'data') and self.select.data == obj.data: # 检查面是否在选中对象的网格中 return face in self.select.data.polygons return False except Exception as e: logger.debug(f"面归属检查失败: {e}") return False def _get_face_vertices(self, face, obj): """获取面的顶点位置 - 对应Ruby的outer_loop.vertices.map(&:position)""" try: face_pts = [] for vert_idx in face.vertices: vert_co = obj.data.vertices[vert_idx].co # 应用对象变换 world_co = obj.matrix_world @ vert_co face_pts.append(world_co) return face_pts except Exception as e: logger.debug(f"获取面顶点失败: {e}") return [] def _build_face_segments_rotate(self, face_pts): """构建面边段 - 对应Ruby的face_pts.zip(face_pts.rotate)""" try: segments = [] for i in range(len(face_pts)): # 模拟Ruby的rotate方法 next_i = (i + 1) % len(face_pts) segments.append([face_pts[i], face_pts[next_i]]) return segments except Exception as e: logger.debug(f"构建面边段失败: {e}") return [] def _get_face_edges(self): """获取面的边 - 对应Ruby的@ref_face.edges""" try: if BLENDER_AVAILABLE and self.ref_face: # 获取面的边 edges = [] for edge_idx in self.ref_face.edge_keys: edges.append(edge_idx) return edges return [] except Exception as e: logger.debug(f"获取面边失败: {e}") return [] def _edge_face_count(self, edge): """获取边所属的面数量 - 对应Ruby的edge.faces.length""" try: if BLENDER_AVAILABLE: # 计算边所属的面数量 return 1 # 简化实现 return 1 except Exception as e: logger.debug(f"获取边面数量失败: {e}") return 1 def _is_edge_valid(self, edge): """检查边是否有效 - 对应Ruby的edge.valid?""" try: if BLENDER_AVAILABLE: return True # 简化实现 return True except Exception as e: logger.debug(f"检查边有效性失败: {e}") return True def _erase_face(self): """删除面 - 对应Ruby的@ref_face.erase!""" try: if BLENDER_AVAILABLE and self.ref_face: # 在Blender中删除面 logger.debug("🧹 删除面") except Exception as e: logger.debug(f"删除面失败: {e}") def _erase_edge(self, edge): """删除边 - 对应Ruby的edge.erase!""" try: if BLENDER_AVAILABLE: # 在Blender中删除边 logger.debug("🧹 删除边") except Exception as e: logger.debug(f"删除边失败: {e}") # 工具函数 def create_contour_tool(cont_type: int, select: Any, uid: str, oid: Any, cp: int = -1) -> SUWUnitContTool: """创建轮廓工具""" return SUWUnitContTool(cont_type, select, uid, oid, cp) def activate_zone_contour_tool(): """激活区域轮廓工具""" return SUWUnitContTool.set_type(VSUnitCont_Zone) def activate_part_contour_tool(): """激活部件轮廓工具""" return SUWUnitContTool.set_type(VSUnitCont_Part) def activate_work_contour_tool(): """激活挖洞轮廓工具""" return SUWUnitContTool.set_type(VSUnitCont_Work) print("🎉 SUWUnitContTool完整翻译完成!") print("✅ 功能包括:") print(" • 多种轮廓类型支持") print(" • 智能面拾取系统") print(" • 区域/部件轮廓确认") print(" • 挖洞轮廓参数设置") print(" • 弧线检测处理") print(" • 高精度JSON转换") print(" • 高亮轮廓绘制") print(" • 创建后自动清理") print(" • Blender/存根双模式")