#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ SUW Core - Selection Manager Module 拆分自: suw_impl.py (Line 3937-4300, 5914-5926) 用途: Blender选择管理、对象高亮、状态维护 版本: 1.0.0 作者: SUWood Team """ from .geometry_utils import MAT_TYPE_OBVERSE, MAT_TYPE_NORMAL, MAT_TYPE_NATURE from .memory_manager import memory_manager from .data_manager import data_manager, get_data_manager import logging from typing import Dict, Any, Optional, List # 设置日志 logger = logging.getLogger(__name__) # 检查Blender可用性 try: import bpy BLENDER_AVAILABLE = True except ImportError: BLENDER_AVAILABLE = False # ==================== 选择管理器类 ==================== class SelectionManager: """选择管理器 - 负责所有选择相关操作""" def __init__(self): """ 初始化选择管理器 - 完全独立,不依赖suw_impl """ # 使用全局数据管理器 - 【修复】使用get_data_manager()函数 self.data_manager = get_data_manager() # 使用函数获取实例,确保初始化 # 选择状态 self.selected_faces = [] self.selected_parts = [] self.selected_hws = [] # 状态缓存 self._face_color_cache = {} # 类级别选择状态 - 本地维护,不再依赖suw_impl self._selected_uid = None self._selected_obj = None self._selected_zone = None self._selected_part = None logger.info("✅ 选择管理器初始化完成") # ==================== 选择清除方法 ==================== def sel_clear(self): """清除选择 - 优化版本,避免阻塞界面""" try: if BLENDER_AVAILABLE: # 【修复】使用非阻塞的直接属性操作,而不是阻塞性操作符 try: for obj in bpy.data.objects: if hasattr(obj, 'select_set'): obj.select_set(False) # 直接设置选择状态,不刷新视口 except: # 如果直接操作失败,跳过而不是使用阻塞性操作符 pass # 清除类级别选择状态 - 使用本地属性 self._selected_uid = None self._selected_obj = None self._selected_zone = None self._selected_part = None # 清除选择的面、零件和硬件 for face in self.selected_faces: if face: self._textured_face(face, False) self.selected_faces.clear() for part in self.selected_parts: if part: self.textured_part(part, False) self.selected_parts.clear() for hw in self.selected_hws: if hw: self._textured_hw(hw, False) self.selected_hws.clear() logger.debug("选择状态已清除") except Exception as e: logger.error(f"清除选择失败: {e}") # ==================== 选择逻辑方法 ==================== def sel_local(self, obj): """本地选择对象""" try: if not obj: logger.warning("选择对象为空") return uid = obj.get("sw_uid") zid = obj.get("sw_zid") typ = obj.get("sw_typ") pid = obj.get("sw_pid", -1) cp = obj.get("sw_cp", -1) # 检查是否已选择 if typ == "zid": if (self._selected_uid == uid and self._selected_obj == zid): return elif typ == "cp": if (self._selected_uid == uid and (self._selected_obj == pid or self._selected_obj == cp)): return else: self.sel_clear() return # 准备选择参数 params = {} params["uid"] = uid params["zid"] = zid # 根据模式选择 if typ == "cp" and self.data_manager.get_part_mode(): params["pid"] = pid params["cp"] = cp self._sel_part_local(params) else: params["pid"] = -1 params["cp"] = -1 self._sel_zone_local(params) # 发送选择命令到客户端(如果需要) # self._set_cmd("r01", params) # select_client except Exception as e: logger.error(f"本地选择失败: {e}") def _sel_zone_local(self, data): """本地区域选择""" try: self.sel_clear() uid = data.get("uid") zid = data.get("zid") zones = self.data_manager.get_zones({"uid": uid}) parts = self.data_manager.get_parts({"uid": uid}) hardwares = self.data_manager.get_hardwares({"uid": uid}) children = self._get_child_zones(zones, zid, True) for child in children: child_id = child.get("zid") child_zone = zones.get(child_id) leaf = child.get("leaf") # 为区域的部件设置纹理 for v_root, part in parts.items(): if part and part.get("sw_zid") == child_id: self.textured_part(part, True) # 为区域的硬件设置纹理 for v_root, hw in hardwares.items(): if hw and hw.get("sw_zid") == child_id: self._textured_hw(hw, True) # 处理区域可见性 hide_none = self.data_manager.hide_none if not leaf or hide_none: if child_zone and hasattr(child_zone, 'hide_viewport'): child_zone.hide_viewport = True else: if child_zone and hasattr(child_zone, 'hide_viewport'): child_zone.hide_viewport = False # 为区域面设置纹理 self._texture_zone_faces(child_zone, True) if child_id == zid: self._selected_uid = uid self._selected_obj = zid self._selected_zone = child_zone except Exception as e: logger.error(f"区域选择失败: {e}") def _sel_part_local(self, data): """本地部件选择""" try: self.sel_clear() parts = self.data_manager.get_parts(data) hardwares = self.data_manager.get_hardwares(data) uid = data.get("uid") cp = data.get("cp") if cp in parts: part = parts[cp] if part: self.textured_part(part, True) self._selected_part = part elif cp in hardwares: hw = hardwares[cp] if hw: self._textured_hw(hw, True) self._selected_uid = uid self._selected_obj = cp except Exception as e: logger.error(f"部件选择失败: {e}") def _sel_part_parent(self, data): """选择部件父级""" try: # 这是一个从服务器来的命令,目前简化实现 uid = data.get("uid") pid = data.get("pid") parts = self.data_manager.get_parts({"uid": uid}) for v_root, part in parts.items(): if part and part.get("sw_pid") == pid: self.textured_part(part, True) self._selected_uid = uid self._selected_obj = pid except Exception as e: logger.error(f"选择部件父级失败: {e}") def _get_child_zones(self, zones, zip_id, myself=False): """获取子区域""" try: children = [] for zid, entity in zones.items(): if entity and entity.get("sw_zip") == zip_id: grandchildren = self._get_child_zones(zones, zid, False) child = { "zid": zid, "leaf": len(grandchildren) == 0 } children.append(child) children.extend(grandchildren) if myself: child = { "zid": zip_id, "leaf": len(children) == 0 } children.append(child) return children except Exception as e: logger.error(f"获取子区域失败: {e}") return [] def _is_selected_part(self, part): """检查部件是否被选中""" return part in self.selected_parts # ==================== 纹理方法 ==================== def _textured_face(self, face, selected): """为面设置纹理""" try: if selected: self.selected_faces.append(face) # 获取材质管理器 from .material_manager import material_manager if not material_manager: return color = "mat_select" if selected else "mat_normal" texture = material_manager.get_texture(color) if texture and hasattr(face, 'material'): face.material = texture # 设置背面材质 back_material = self.data_manager.get_back_material() if back_material or (texture and texture.get("alpha", 1.0) < 1.0): if hasattr(face, 'back_material'): face.back_material = texture except Exception as e: logger.error(f"设置面纹理失败: {e}") def textured_part(self, part, selected): """为部件设置纹理""" try: if not part: return if selected: self.selected_parts.append(part) # 获取材质管理器 from .material_manager import material_manager if not material_manager: return # 根据材质类型确定颜色 mat_type = self.data_manager.get_mat_type() if selected: color = "mat_select" elif mat_type == MAT_TYPE_NATURE: # 根据部件类型确定自然材质 mn = part.get("sw_mn", 1) if mn == 1: color = "mat_obverse" # 门板 elif mn == 2: color = "mat_reverse" # 柜体 elif mn == 3: color = "mat_thin" # 背板 else: color = "mat_normal" else: color = self._face_color(part, part) or "mat_normal" # 应用材质 texture = material_manager.get_texture(color) if texture: self._apply_part_material(part, texture, selected) # 处理子对象 if hasattr(part, 'children'): for child in part.children: if hasattr(child, 'type') and child.type == 'MESH': self._apply_part_material(child, texture, selected) except Exception as e: logger.error(f"设置部件纹理失败: {e}") def _textured_hw(self, hw, selected): """为硬件设置纹理""" try: if not hw: return if selected: self.selected_hws.append(hw) # 获取材质管理器 from .material_manager import material_manager if not material_manager: return color = "mat_select" if selected else hw.get( "sw_ckey", "mat_hardware") texture = material_manager.get_texture(color) if texture: self._apply_hw_material(hw, texture) except Exception as e: logger.error(f"设置硬件纹理失败: {e}") def _face_color(self, face, leaf): """获取面颜色""" try: # 检查是否有差异标记 if face and face.get("sw_differ", False): return "mat_default" # 根据材质类型确定颜色 mat_type = self.data_manager.get_mat_type() if mat_type == MAT_TYPE_OBVERSE: typ = face.get("sw_typ") if face else None if typ == "o" or typ == "e1": return "mat_obverse" elif typ == "e2": return "mat_thin" elif typ == "r" or typ == "e0": return "mat_reverse" # 从属性获取颜色 color = face.get("sw_ckey") if face else None if not color and leaf: color = leaf.get("sw_ckey") return color except Exception as e: logger.error(f"获取面颜色失败: {e}") return "mat_default" def _apply_part_material(self, obj, material, selected): """应用部件材质""" try: if not obj or not material: return if hasattr(obj, 'data') and obj.data and hasattr(obj.data, 'materials'): if not obj.data.materials: obj.data.materials.append(material) else: obj.data.materials[0] = material # 设置可见性 edge_visible = selected or self.data_manager.get_mat_type() == MAT_TYPE_NATURE if hasattr(obj, 'hide_viewport'): obj.hide_viewport = not edge_visible except Exception as e: logger.error(f"应用部件材质失败: {e}") def _apply_hw_material(self, obj, material): """应用硬件材质""" try: if not obj or not material: return if hasattr(obj, 'data') and obj.data and hasattr(obj.data, 'materials'): if not obj.data.materials: obj.data.materials.append(material) else: obj.data.materials[0] = material except Exception as e: logger.error(f"应用硬件材质失败: {e}") def _texture_zone_faces(self, zone, selected): """为区域面设置纹理""" try: if not zone or not hasattr(zone, 'data') or not zone.data: return # 遍历区域的所有面 if hasattr(zone.data, 'polygons'): for face in zone.data.polygons: self._textured_face(face, selected) except Exception as e: logger.error(f"设置区域面纹理失败: {e}") def view_front_and_zoom_extents(self): """切换到前视图并缩放到全部,刷新视图(适配无UI/后台环境)""" try: if not BLENDER_AVAILABLE: logger.warning("Blender 不可用,无法切换视图") return True # 不报错 found_view3d = False # for window in getattr(bpy.context, "window_manager", []).windows if hasattr(bpy.context, "window_manager") else []: # for area in window.screen.areas: # if area.type == 'VIEW_3D': # found_view3d = True # region = next( # (reg for reg in area.regions if reg.type == 'WINDOW'), None) # space = next( # (sp for sp in area.spaces if sp.type == 'VIEW_3D'), None) # if region and space: # with bpy.context.temp_override(window=window, area=area, region=region, space_data=space): # bpy.ops.view3d.view_axis(type='FRONT') # bpy.ops.view3d.view_all(center=False) # area.tag_redraw() # logger.info("✅ 已切换到前视图并缩放到全部") # return True if not found_view3d: logger.info("无3D视图环境,跳过视图操作(后台/无UI模式)") return True # 不报错,直接返回True logger.warning("未找到3D视图区域,无法切换视图") return True except Exception as e: logger.info("无3D视图环境,跳过视图操作(后台/无UI模式)") return True # 不报错,直接返回True def _is_leaf_zone(self, zip_id_to_check, all_zones_for_uid): """检查一个区域是否是叶子节点 (没有子区域)""" try: for zid, zone_obj in all_zones_for_uid.items(): if zone_obj and hasattr(zone_obj, 'get') and zone_obj.get("sw_zip") == zip_id_to_check: return False # Found a child, so it's not a leaf return True except Exception: return True # 发生错误时默认为叶子节点 # ==================== 命令处理方法 ==================== def c15(self, data: Dict[str, Any]): """sel_unit - 清除选择并根据层级设置区域可见性""" try: if not BLENDER_AVAILABLE: return False self.sel_clear() zones = self.data_manager.get_zones(data) hide_none = self.data_manager.hide_none for zid, zone in zones.items(): if zone and hasattr(zone, 'hide_viewport'): is_leaf = self._is_leaf_zone(zid, zones) if is_leaf: zone.hide_viewport = hide_none else: zone.hide_viewport = True logger.info("c15 (sel_unit) 执行完成") return True except Exception as e: logger.error(f"c15 (sel_unit) 执行失败: {e}") return False def c16(self, data: Dict[str, Any]): """sel_zone - 选择区域命令""" try: return self._sel_zone_local(data) except Exception as e: logger.error(f"c16命令执行失败: {e}") return None def c17(self, data: Dict[str, Any]): """sel_elem - 选择元素命令""" try: # 根据模式选择不同的处理方式 if self.data_manager.get_part_mode(): return self._sel_part_parent(data) else: return self._sel_zone_local(data) except Exception as e: logger.error(f"c17命令执行失败: {e}") return None def set_config(self, data: dict): """设置全局/单元/显示等配置,兼容Ruby set_config""" try: # 1. 服务器路径等全局参数 if "server_path" in data: setattr(self.data_manager, "server_path", data["server_path"]) if "order_id" in data: setattr(self.data_manager, "order_id", data["order_id"]) if "order_code" in data: setattr(self.data_manager, "order_code", data["order_code"]) if "back_material" in data: self.data_manager.back_material = data["back_material"] if "part_mode" in data: self.data_manager.part_mode = data["part_mode"] if "hide_none" in data: self.data_manager.hide_none = data["hide_none"] # 2. 单元/图纸相关 if "unit_drawing" in data: setattr(self.data_manager, "unit_drawing", data["unit_drawing"]) if "drawing_name" in data: setattr(self.data_manager, "drawing_name", data["drawing_name"]) # 3. 区域角点 if "zone_corner" in data: uid = data.get("uid") zid = data.get("zid") if uid and zid: zones = self.data_manager.get_zones({"uid": uid}) zone = zones.get(zid) if zone: zone["sw_cor"] = data["zone_corner"] logger.info("✅ set_config 配置完成") return True except Exception as e: logger.error(f"set_config 配置失败: {e}") return False # ==================== 类方法(保持兼容性)==================== @classmethod def selected_uid(cls): """获取选中的UID - 兼容性方法""" # 从全局实例获取 global selection_manager if selection_manager: return selection_manager._selected_uid return None @classmethod def selected_zone(cls): """获取选中的区域 - 兼容性方法""" global selection_manager if selection_manager: return selection_manager._selected_zone return None @classmethod def selected_part(cls): """获取选中的部件 - 兼容性方法""" global selection_manager if selection_manager: return selection_manager._selected_part return None @classmethod def selected_obj(cls): """获取选中的对象 - 兼容性方法""" global selection_manager if selection_manager: return selection_manager._selected_obj return None # ==================== 管理方法 ==================== def cleanup(self): """清理选择管理器""" try: self.sel_clear() self._face_color_cache.clear() logger.info("✅ 选择管理器清理完成") except Exception as e: logger.error(f"清理选择管理器失败: {e}") def get_selection_stats(self) -> Dict[str, Any]: """获取选择统计信息""" try: return { "selected_faces_count": len(self.selected_faces), "selected_parts_count": len(self.selected_parts), "selected_hws_count": len(self.selected_hws), "face_color_cache_size": len(self._face_color_cache), "selected_uid": self._selected_uid, "selected_obj": self._selected_obj, } except Exception as e: logger.error(f"获取选择统计失败: {e}") return {"error": str(e)} # ==================== 全局选择管理器实例 ==================== # 全局实例 selection_manager = None def init_selection_manager(): """初始化全局选择管理器实例 - 不再需要suw_impl参数""" global selection_manager selection_manager = SelectionManager() return selection_manager def get_selection_manager(): """获取全局选择管理器实例""" global selection_manager if selection_manager is None: selection_manager = init_selection_manager() return selection_manager