#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ SUW Core - Explosion Manager Module 拆分自: suw_impl.py (Line 583-602) 用途: 炸开柜体功能、区域和零件移动、零件序列文本显示 版本: 1.0.0 作者: SUWood Team """ from .geometry_utils import Point3d, Vector3d from .memory_manager import memory_manager from .data_manager import data_manager, get_data_manager import math 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 ExplosionManager: """炸开管理器 - 负责炸开柜体相关操作""" def __init__(self): """ 初始化炸开管理器 - 完全独立,不依赖suw_impl """ # 使用全局数据管理器 self.data_manager = get_data_manager() # 标签对象 self.labels = None self.door_labels = None logger.info("ExplosionManager 初始化完成") # ==================== 核心命令方法 ==================== def c0e(self, data: Dict[str, Any]): """explode_zones - 炸开柜体 - 按照Ruby逻辑实现""" try: if not BLENDER_AVAILABLE: logger.warning("Blender 不可用,跳过炸开柜体操作") return 0 uid = data.get("uid") zones = data.get("zones", []) parts = data.get("parts", []) explode = data.get("explode", False) logger.info( f" 开始炸开柜体: uid={uid}, 区域数={len(zones)}, 零件数={len(parts)}, 显示序列={explode}") # 初始化标签对象 self._init_labels() # 处理区域移动 zones_moved = self._move_zones(uid, zones) # 处理零件移动 parts_moved = self._move_parts(uid, parts) # 处理零件序列文本显示 if explode: texts_created = self._create_part_sequence_texts(uid) else: # 【修复】当explode=False时,删除之前创建的文本标签 texts_deleted = self._delete_part_sequence_texts() texts_created = -texts_deleted # 负数表示删除的数量 logger.info( f"✅ 炸开柜体完成: 区域移动={zones_moved}, 零件移动={parts_moved}, 文本操作={texts_created}") return zones_moved + parts_moved + texts_created except Exception as e: logger.error(f"❌ 炸开柜体失败: {e}") return 0 def c0d(self, data: Dict[str, Any]): """parts_seqs - 设置零件序列信息 - 按照Ruby逻辑实现""" try: if not BLENDER_AVAILABLE: logger.warning("Blender 不可用,跳过零件序列设置") return 0 uid = data.get("uid") seqs = data.get("seqs", []) logger.info(f" 开始设置零件序列信息: uid={uid}, 序列数={len(seqs)}") parts_data = self.data_manager.get_parts({"uid": uid}) set_count = 0 # 【按照Ruby逻辑】处理每个序列项 for seq_data in seqs: try: root = seq_data.get("cp") # 部件id seq = seq_data.get("seq") # 顺序号 pos = seq_data.get("pos") # 位置 name = seq_data.get("name") # 板件名称(可选) size = seq_data.get("size") # 尺寸即长*宽*厚(可选) mat = seq_data.get("mat") # 材料(包括材质/颜色)(可选) if not root or seq is None or pos is None: logger.warning( f"跳过无效序列数据: root={root}, seq={seq}, pos={pos}") continue # 【按照Ruby逻辑】查找对应的零件 if root in parts_data: part = parts_data[root] if part and hasattr(part, 'get'): # 【修复】使用sw_前缀的属性,与c0e命令保持一致 part["sw_seq"] = seq part["sw_pos"] = pos # 设置可选属性 if name: part["sw_name"] = name if size: part["sw_size"] = size if mat: part["sw_mat"] = mat set_count += 1 logger.debug( f"设置零件序列: cp={root}, seq={seq}, pos={pos}, name={name}, size={size}, mat={mat}") else: logger.warning(f"零件对象无效: cp={root}") else: logger.warning(f"未找到零件: cp={root}") except Exception as e: logger.error(f"处理序列项失败: {e}") continue logger.info(f"✅ 零件序列信息设置完成: {set_count} 个") return set_count except Exception as e: logger.error(f"❌ 设置零件序列信息失败: {e}") return 0 # ==================== 私有方法 ==================== def _init_labels(self): """初始化标签对象""" try: if not self.labels: # 创建标签组 self.labels = bpy.data.objects.new("SUW_Labels", None) self.labels.empty_display_type = 'PLAIN_AXES' bpy.context.scene.collection.objects.link(self.labels) if not self.door_labels: # 创建门板标签组 self.door_labels = bpy.data.objects.new("SUW_DoorLabels", None) self.door_labels.empty_display_type = 'PLAIN_AXES' bpy.context.scene.collection.objects.link(self.door_labels) except Exception as e: logger.error(f"初始化标签对象失败: {e}") def _move_zones(self, uid: str, zones: List[Dict[str, Any]]) -> int: """移动区域""" try: moved_count = 0 zones_data = self.data_manager.get_zones({"uid": uid}) for zone_data in zones: zid = zone_data.get("zid") vec_str = zone_data.get("vec", "(0,0,0)") if zid in zones_data: zone = zones_data[zid] if zone and hasattr(zone, 'location'): # 解析偏移向量 offset = Vector3d.parse(vec_str) if offset: # 应用单位变换 if uid in self.data_manager.unit_trans: trans = self.data_manager.unit_trans[uid] offset = self._transform_vector(offset, trans) # 移动区域 zone.location.x += offset.x # Vector3d.parse已经转换过了 zone.location.y += offset.y zone.location.z += offset.z moved_count += 1 logger.debug(f"移动区域: zid={zid}, 偏移={vec_str}") return moved_count except Exception as e: logger.error(f"移动区域失败: {e}") return 0 def _move_parts(self, uid: str, parts: List[Dict[str, Any]]) -> int: """移动零件 - 按照Ruby逻辑匹配零件""" try: moved_count = 0 parts_data = self.data_manager.get_parts({"uid": uid}) hardwares_data = self.data_manager.get_hardwares({"uid": uid}) logger.debug( f"开始移动零件: 零件数据={len(parts_data)}, 五金数据={len(hardwares_data)}") # 【修复】将集合移到外层,避免重复移动 moved_parts = set() # 记录已移动的零件,避免重复移动 moved_hardwares = set() # 记录已移动的五金件,避免重复移动 for part_data in parts: pid = part_data.get("pid") vec_str = part_data.get("vec", "(0,0,0)") logger.debug(f"处理零件移动: pid={pid}, vec={vec_str}") # 解析偏移向量 offset = Vector3d.parse(vec_str) if not offset: logger.warning(f"无法解析偏移向量: {vec_str}") continue # 应用单位变换 if uid in self.data_manager.unit_trans: trans = self.data_manager.unit_trans[uid] offset = self._transform_vector(offset, trans) # 【新增】详细调试信息 matched_parts = [] matched_hardwares = [] # 【修复】按照Ruby逻辑匹配零件 - 通过pid属性匹配 for root, part in parts_data.items(): if not part: continue # 获取零件的pid属性 part_pid = self._get_part_attribute(part, "pid", -1) # 【新增】详细调试信息 if part_pid == pid: matched_parts.append(root) # logger.info( # f"比较: 目标pid={pid}, 零件pid={part_pid}, 零件键={root}") if part_pid == pid and root not in moved_parts: # 【修复】对于门板零件,需要特殊处理 # 检查是否是门板类型(通过layer属性或其他标识) part_layer = self._get_part_attribute(part, "layer", 0) part_name = self._get_part_attribute(part, "name", "") # 如果是门板层(layer=1)或者零件名称包含"门",则允许移动 is_door_part = ( part_layer == 1 or "门" in str(part_name)) if is_door_part: # 移动零件 - Vector3d.parse已经进行了单位转换 if hasattr(part, 'location'): # 【新增】记录移动前的位置 old_location = (part.location.x, part.location.y, part.location.z) # 【修复】确保位置计算正确,避免浮点数精度问题 new_x = part.location.x + offset.x new_y = part.location.y + offset.y new_z = part.location.z + offset.z # 应用新位置 part.location.x = new_x part.location.y = new_y part.location.z = new_z moved_count += 1 moved_parts.add(root) # 标记为已移动 # 【新增】详细的位置变化信息 new_location = (part.location.x, part.location.y, part.location.z) logger.info( f"✅ 移动门板零件成功: pid={pid}, root={root}, layer={part_layer}, name={part_name}, 偏移={vec_str}") else: logger.warning( f"零件对象没有location属性: pid={pid}, root={root}") else: # 对于非门板零件,检查是否已经移动过相同pid的零件 pid_already_moved = any( self._get_part_attribute(p, "pid", -1) == pid for p in [parts_data.get(r) for r in moved_parts if parts_data.get(r)] ) if pid_already_moved: logger.info( f"⚠️ 跳过重复pid的非门板零件: pid={pid}, root={root} (已移动过相同pid的零件)") continue # 移动非门板零件 if hasattr(part, 'location'): # 【新增】记录移动前的位置 old_location = (part.location.x, part.location.y, part.location.z) # 【修复】确保位置计算正确,避免浮点数精度问题 new_x = part.location.x + offset.x new_y = part.location.y + offset.y new_z = part.location.z + offset.z # 应用新位置 part.location.x = new_x part.location.y = new_y part.location.z = new_z moved_count += 1 moved_parts.add(root) # 标记为已移动 # 【新增】详细的位置变化信息 new_location = (part.location.x, part.location.y, part.location.z) logger.info( f"✅ 移动非门板零件成功: pid={pid}, root={root}, layer={part_layer}, name={part_name}, 偏移={vec_str}") else: logger.warning( f"零件对象没有location属性: pid={pid}, root={root}") # 【修复】按照Ruby逻辑匹配五金件 - 通过pid属性匹配 for root, hardware in hardwares_data.items(): if not hardware: continue # 获取五金件的pid属性 hw_pid = self._get_part_attribute(hardware, "pid", -1) # 【新增】详细调试信息 if hw_pid == pid: matched_hardwares.append(root) # logger.info( # f"比较: 目标pid={pid}, 五金pid={hw_pid}, 五金键={root}") if hw_pid == pid and root not in moved_hardwares: # 【修复】检查是否已经移动过相同pid的五金件 hw_pid_already_moved = any( self._get_part_attribute(hw, "pid", -1) == pid for hw in [hardwares_data.get(r) for r in moved_hardwares if hardwares_data.get(r)] ) if hw_pid_already_moved: logger.info( f"⚠️ 跳过重复pid的五金件: pid={pid}, root={root} (已移动过相同pid的五金件)") continue # 移动五金件 - Vector3d.parse已经进行了单位转换 if hasattr(hardware, 'location'): # 【新增】记录移动前的位置 old_location = ( hardware.location.x, hardware.location.y, hardware.location.z) # 【修复】Vector3d.parse已经转换过了,不需要再次转换 hardware.location.x += offset.x hardware.location.y += offset.y hardware.location.z += offset.z moved_count += 1 moved_hardwares.add(root) # 标记为已移动 # 【新增】详细的位置变化信息 new_location = ( hardware.location.x, hardware.location.y, hardware.location.z) logger.info( f"✅ 移动五金件成功: pid={pid}, root={root}, 偏移={vec_str}") else: logger.warning( f"五金件对象没有location属性: pid={pid}, root={root}") # 【新增】总结匹配结果 logger.info( f"📊 pid={pid}匹配结果: 零件={len(matched_parts)}, 五金件={len(matched_hardwares)}") # 【新增】强制更新视图 try: if BLENDER_AVAILABLE: bpy.context.view_layer.update() logger.debug("视图已更新") except Exception as e: logger.debug(f"视图更新失败: {e}") logger.info(f"零件移动完成: 移动了 {moved_count} 个对象") return moved_count except Exception as e: logger.error(f"移动零件失败: {e}") return 0 def _create_part_sequence_texts(self, uid: str) -> int: """创建零件序列文本 - 修复属性访问""" try: created_count = 0 parts_data = self.data_manager.get_parts({"uid": uid}) logger.debug(f"开始创建零件序列文本: 零件数={len(parts_data)}") for root, part in parts_data.items(): if not part: continue # 【修复】使用统一的属性获取方法 pos = self._get_part_attribute(part, "pos", 1) seq = self._get_part_attribute(part, "seq", 0) layer = self._get_part_attribute(part, "layer", 0) logger.debug( f"零件属性: root={root}, seq={seq}, pos={pos}, layer={layer}") if seq <= 0: continue # 获取零件位置 center = None if hasattr(part, 'location'): center = part.location else: logger.warning(f"零件没有位置信息: root={root}, seq={seq}") continue # 计算文本位置和方向 vector = self._get_position_vector(pos) if not vector: continue # 应用单位变换 if uid in self.data_manager.unit_trans: trans = self.data_manager.unit_trans[uid] vector = self._transform_vector(vector, trans) # 计算文本位置 text_location = ( center.x + vector.x * 0.1, # 100mm偏移 center.y + vector.y * 0.1, center.z + vector.z * 0.1 ) # 创建文本对象 text_obj = self._create_text_object(str(seq), text_location) if text_obj: # 设置材质为红色 self._add_red_material(text_obj) # 根据图层决定父对象 if layer == 1: # 门板层 text_obj.parent = self.door_labels else: text_obj.parent = self.labels created_count += 1 logger.debug( f"创建零件序列文本: seq={seq}, pos={pos}, root={root}") return created_count except Exception as e: logger.error(f"创建零件序列文本失败: {e}") return 0 def _delete_part_sequence_texts(self) -> int: """删除零件序列文本""" try: deleted_count = 0 # 【修复】直接通过名称删除固定的标签集合 # 删除 SUW_Labels 集合及其所有子对象 suw_labels = bpy.data.objects.get("SUW_Labels") if suw_labels: # 【修复】先收集所有子对象名称,再逐个删除 children_to_delete = [] for child in suw_labels.children: if child.type == 'FONT': children_to_delete.append(child.name) # 逐个删除子对象 for child_name in children_to_delete: child = bpy.data.objects.get(child_name) if child: try: bpy.data.objects.remove(child, do_unlink=True) deleted_count += 1 logger.debug(f"删除文本对象: {child_name}") except Exception as e: logger.warning(f"删除文本对象失败: {child_name}, {e}") # 删除父对象 try: bpy.data.objects.remove(suw_labels, do_unlink=True) logger.debug("删除SUW_Labels集合") except Exception as e: logger.warning(f"删除SUW_Labels集合失败: {e}") # 删除 SUW_DoorLabels 集合及其所有子对象 suw_door_labels = bpy.data.objects.get("SUW_DoorLabels") if suw_door_labels: # 【修复】先收集所有子对象名称,再逐个删除 children_to_delete = [] for child in suw_door_labels.children: if child.type == 'FONT': children_to_delete.append(child.name) # 逐个删除子对象 for child_name in children_to_delete: child = bpy.data.objects.get(child_name) if child: try: bpy.data.objects.remove(child, do_unlink=True) deleted_count += 1 logger.debug(f"删除门板文本对象: {child_name}") except Exception as e: logger.warning(f"删除门板文本对象失败: {child_name}, {e}") # 删除父对象 try: bpy.data.objects.remove(suw_door_labels, do_unlink=True) logger.debug("删除SUW_DoorLabels集合") except Exception as e: logger.warning(f"删除SUW_DoorLabels集合失败: {e}") # 【新增】清理场景中可能残留的文本对象 # 搜索并删除所有以"Text_"开头的对象 # 【修复】使用更安全的方式遍历和删除对象 text_objects_to_delete = [] for obj in bpy.data.objects: if obj.name.startswith("Text_") and obj.type == 'FONT': text_objects_to_delete.append(obj.name) for obj_name in text_objects_to_delete: obj = bpy.data.objects.get(obj_name) if obj: try: bpy.data.objects.remove(obj, do_unlink=True) deleted_count += 1 logger.debug(f"删除残留文本对象: {obj_name}") except Exception as e: logger.warning(f"删除残留文本对象失败: {obj_name}, {e}") # 【新增】强制更新视图 try: if BLENDER_AVAILABLE: bpy.context.view_layer.update() logger.debug("视图已更新") except Exception as e: logger.debug(f"视图更新失败: {e}") # 【修复】彻底清理内部引用和相关数据 # 重置内部引用 self.labels = None self.door_labels = None # 【新增】清理可能残留的引用 # 检查并清理场景中可能残留的引用 # 【修复】使用更安全的方式检查对象是否存在 for obj_name in ["SUW_Labels", "SUW_DoorLabels"]: obj = bpy.data.objects.get(obj_name) if obj: try: bpy.data.objects.remove(obj, do_unlink=True) logger.debug(f"清理残留引用: {obj_name}") except Exception as e: logger.debug(f"清理残留引用失败: {obj_name}, {e}") # 【新增】清理材质数据 # 删除可能残留的红色文本材质 # 【修复】使用更安全的方式清理材质 red_text_material = bpy.data.materials.get("Red_Text") if red_text_material: try: bpy.data.materials.remove(red_text_material) logger.debug("清理红色文本材质") except Exception as e: logger.debug(f"清理材质失败: {e}") # 【新增】清理曲线数据 # 删除可能残留的文本曲线 # 【修复】使用更安全的方式清理曲线 curves_to_delete = [] for curve in bpy.data.curves: if curve.name.startswith("Text_"): curves_to_delete.append(curve.name) for curve_name in curves_to_delete: curve = bpy.data.curves.get(curve_name) if curve: try: bpy.data.curves.remove(curve) logger.debug(f"清理文本曲线: {curve_name}") except Exception as e: logger.debug(f"清理曲线失败: {curve_name}, {e}") logger.info(f"✅ 删除零件序列文本: {deleted_count} 个") return deleted_count except Exception as e: logger.error(f"❌ 删除零件序列文本失败: {e}") return 0 def _get_position_vector(self, pos: int) -> Optional[Vector3d]: """根据位置获取方向向量""" try: if pos == 1: # F - 前面 return Vector3d(0, -1, 0) elif pos == 2: # K - 后面 return Vector3d(0, 1, 0) elif pos == 3: # L - 左面 return Vector3d(-1, 0, 0) elif pos == 4: # R - 右面 return Vector3d(1, 0, 0) elif pos == 5: # B - 底面 return Vector3d(0, 0, -1) elif pos == 6: # T - 顶面 return Vector3d(0, 0, 1) else: return Vector3d(0, 0, 1) # 默认向上 except Exception as e: logger.error(f"获取位置向量失败: {e}") return None def _create_text_object(self, text: str, location: tuple) -> Optional[Any]: """创建文本对象""" try: # 创建文本曲线 text_curve = bpy.data.curves.new(type="FONT", name=f"Text_{text}") text_curve.body = text text_curve.size = 0.05 # 5cm字体大小 # 创建文本对象 text_obj = bpy.data.objects.new(f"Text_{text}", text_curve) text_obj.location = location # 添加到场景 bpy.context.scene.collection.objects.link(text_obj) return text_obj except Exception as e: logger.error(f"创建文本对象失败: {e}") return None def _add_red_material(self, obj): """添加红色材质到对象""" try: # 创建红色材质 mat = bpy.data.materials.new(name="Red_Text") mat.use_nodes = True nodes = mat.node_tree.nodes nodes.clear() # 创建发射节点 emission = nodes.new(type='ShaderNodeEmission') emission.inputs[0].default_value = (1, 0, 0, 1) # 红色 emission.inputs[1].default_value = 1.0 # 强度 # 创建输出节点 output = nodes.new(type='ShaderNodeOutputMaterial') # 连接节点 mat.node_tree.links.new(emission.outputs[0], output.inputs[0]) # 应用材质到对象 if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) except Exception as e: logger.error(f"添加红色材质失败: {e}") def _transform_vector(self, vector: Vector3d, transform) -> Vector3d: """变换向量""" try: if not transform: return vector # 简化的变换实现 # 这里应该根据实际的变换矩阵进行计算 # 暂时返回原始向量 return vector except Exception as e: logger.error(f"变换向量失败: {e}") return vector def _get_part_attribute(self, obj, attr_name: str, default_value=None): """获取零件属性 - 支持多种对象类型""" try: # 【修复】优先检查sw_前缀的属性 if hasattr(obj, 'get'): # 如果是字典或类似对象 sw_attr_name = f"sw_{attr_name}" if sw_attr_name in obj: return obj[sw_attr_name] # 回退到sw字典 return obj.get("sw", {}).get(attr_name, default_value) elif hasattr(obj, 'sw'): # 如果有sw属性 sw_attr_name = f"sw_{attr_name}" if hasattr(obj, sw_attr_name): return getattr(obj, sw_attr_name) return obj.sw.get(attr_name, default_value) elif isinstance(obj, dict): # 如果是字典 sw_attr_name = f"sw_{attr_name}" if sw_attr_name in obj: return obj[sw_attr_name] return obj.get("sw", {}).get(attr_name, default_value) else: # 尝试从Blender对象的自定义属性获取 try: sw_attr_name = f"sw_{attr_name}" if hasattr(obj, sw_attr_name): return getattr(obj, sw_attr_name) elif hasattr(obj, 'sw'): return obj.sw.get(attr_name, default_value) elif hasattr(obj, 'get'): return obj.get("sw", {}).get(attr_name, default_value) except: pass return default_value except Exception as e: logger.debug(f"获取零件属性失败: {e}") return default_value # ==================== 管理器统计 ==================== def get_explosion_stats(self) -> Dict[str, Any]: """获取炸开管理器统计信息""" try: stats = { "manager_type": "ExplosionManager", "labels_created": self.labels is not None, "door_labels_created": self.door_labels is not None, "blender_available": BLENDER_AVAILABLE } return stats except Exception as e: logger.error(f"获取炸开管理器统计失败: {e}") return {"error": str(e)} # ==================== 模块实例 ==================== # 全局实例,将由SUWImpl初始化时设置 explosion_manager = None def init_explosion_manager(): """初始化炸开管理器 - 不再需要suw_impl参数""" global explosion_manager explosion_manager = ExplosionManager() return explosion_manager def get_explosion_manager(): """获取炸开管理器实例""" global explosion_manager if explosion_manager is None: explosion_manager = init_explosion_manager() return explosion_manager