#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ SUW Core - Part Creator Module 拆分自: suw_impl.py (Line 1302-1600) 用途: Blender部件创建、板材管理、UV处理 版本: 1.0.0 作者: SUWood Team """ from . import material_manager as mm_module from .material_manager import material_manager from .memory_manager import memory_manager, dependency_manager, safe_blender_operation from .data_manager import data_manager, get_data_manager import time import logging from typing import Dict, Any, Optional, List, Tuple # 设置日志 logger = logging.getLogger(__name__) # 检查Blender可用性 try: import bpy BLENDER_AVAILABLE = True except ImportError: BLENDER_AVAILABLE = False # ==================== 部件创建器类 ==================== class PartCreator: """部件创建器 - 负责所有部件相关操作""" def __init__(self): """ 初始化部件创建器 - 完全独立,不依赖suw_impl """ # 使用全局数据管理器 self.data_manager = get_data_manager() # 【修复】初始化时间戳,避免AttributeError self._last_board_creation_time = 0 # 创建统计 self.creation_stats = { "parts_created": 0, "boards_created": 0, "creation_errors": 0 } logger.info("PartCreator 初始化完成") def get_parts(self, data: Dict[str, Any]) -> Dict[str, Any]: """获取零件信息 - 保持原始方法名和参数""" return self.data_manager.get_parts(data) def c04(self, data: Dict[str, Any]): """c04 - 添加部件 - 修复版本,参考suw_impl.py的实现""" try: if not BLENDER_AVAILABLE: logger.warning("Blender 不可用,跳过零件创建") return uid = data.get("uid") root = data.get("cp") if not uid or not root: logger.error("缺少必要参数: uid或cp") return logger.info(f" 开始创建部件: uid={uid}, cp={root}") # 【修复1】获取parts数据结构 parts = self.get_parts(data) # 【修复2】检查是否已存在 if root in parts: existing_part = parts[root] if existing_part and self._is_object_valid(existing_part): logger.info(f"✅ 部件 {root} 已存在,跳过创建") return existing_part else: logger.warning(f"清理无效的部件引用: {root}") del parts[root] # 【修复3】创建部件容器 - 修改命名格式为Part_{uid}_{cp} part_name = f"Part_{uid}_{root}" part = bpy.data.objects.new(part_name, None) bpy.context.scene.collection.objects.link(part) logger.info(f"✅ 创建Part对象: {part_name}") # 【修复4】设置部件基本属性 part["sw_uid"] = uid part["sw_cp"] = root part["sw_typ"] = "part" part["sw_zid"] = data.get("zid") part["sw_pid"] = data.get("pid") # 【新增】设置layer属性 - 参考Ruby版本的逻辑 layer = data.get("layer", 0) part["sw_layer"] = layer if layer == 1: logger.info(f"✅ 部件 {part_name} 标记为门板图层 (layer=1)") elif layer == 2: logger.info(f"✅ 部件 {part_name} 标记为抽屉图层 (layer=2)") else: logger.info(f"✅ 部件 {part_name} 标记为普通图层 (layer=0)") # 【新增】设置门板属性 - 参考Ruby版本的逻辑 door_type = data.get("dor", 0) part["sw_door"] = door_type if door_type in [10, 15]: part["sw_door_width"] = data.get("dow", 0) part["sw_door_pos"] = data.get("dop", "F") logger.info( f"✅ 部件 {part_name} 设置门板属性: door_type={door_type}, width={data.get('dow', 0)}, pos={data.get('dop', 'F')}") # 【新增】设置抽屉属性 - 参考Ruby版本的逻辑 drawer_type = data.get("drw", 0) part["sw_drawer"] = drawer_type if drawer_type in [73, 74]: # DR_LP/DR_RP part["sw_dr_depth"] = data.get("drd", 0) logger.info( f"📦 部件 {part_name} 设置抽屉属性: drawer_type={drawer_type}, depth={data.get('drd', 0)}") elif drawer_type == 70: # DR_DP drv = data.get("drv") if drv: # 这里需要解析向量,暂时存储原始值 part["sw_drawer_dir"] = drv logger.info(f"📦 部件 {part_name} 设置抽屉方向: {drv}") # 【新增】设置Part对象的父对象为Zone对象 zone_name = f"Zone_{uid}" zone_obj = bpy.data.objects.get(zone_name) if zone_obj: part.parent = zone_obj logger.info(f"✅ 设置Part对象 {part_name} 的父对象为Zone: {zone_name}") else: logger.warning(f"⚠️ 未找到Zone对象: {zone_name},Part对象将没有父对象") # 【修复5】存储部件到数据结构 - 使用data_manager.add_part()方法 self.data_manager.add_part(uid, root, part) logger.info( f"✅ 使用data_manager.add_part()存储部件数据: uid={uid}, cp={root}") # 【修复6】处理finals数据 finals = data.get("finals", []) logger.info(f" 处理 {len(finals)} 个板材数据") created_boards = 0 for i, final_data in enumerate(finals): try: board = self.create_board_with_material_and_uv( part, final_data) if board: created_boards += 1 logger.info( f"✅ 板材 {i+1}/{len(finals)} 创建成功: {board.name}") # 【修复7】移除频繁的依赖图更新,避免评估过程中的错误 # if i % 5 == 0: # bpy.context.view_layer.update() else: logger.warning(f"⚠️ 板材 {i+1}/{len(finals)} 创建失败") except Exception as e: logger.error(f"❌ 创建板材 {i+1}/{len(finals)} 失败: {e}") # 【修复8】单个板材失败时的恢复 - 移除依赖图更新 try: import gc gc.collect() # bpy.context.view_layer.update() # 移除这行 except: pass logger.info(f"📊 板材创建统计: {created_boards}/{len(finals)} 成功") # 【修复9】最终清理 - 移除依赖图更新 try: # bpy.context.view_layer.update() # 移除这行 import gc gc.collect() except Exception as cleanup_error: logger.warning(f"最终清理失败: {cleanup_error}") # 【修复10】验证创建结果 if part.name in bpy.data.objects: logger.info(f"🎉 部件创建完全成功: {part_name}") return part else: logger.error(f"❌ 部件创建失败: {part_name} 不在bpy.data.objects中") return None except Exception as e: logger.error(f"❌ c04命令执行失败: {e}") import traceback logger.error(traceback.format_exc()) return None def create_board_with_material_and_uv(self, part, data): """创建板材并关联材质和启用UV - 完全修复版本,避免bpy.ops""" try: # 获取正反面数据 obv = data.get("obv") rev = data.get("rev") if not obv or not rev: logger.warning("缺少正反面数据,创建默认板材") return self.create_default_board_with_material(part, data) # 解析顶点计算精确尺寸 obv_vertices = self._parse_surface_vertices(obv) rev_vertices = self._parse_surface_vertices(rev) if len(obv_vertices) >= 3 and len(rev_vertices) >= 3: # 计算板材的精确边界 all_vertices = obv_vertices + rev_vertices min_x = min(v[0] for v in all_vertices) max_x = max(v[0] for v in all_vertices) min_y = min(v[1] for v in all_vertices) max_y = max(v[1] for v in all_vertices) min_z = min(v[2] for v in all_vertices) max_z = max(v[2] for v in all_vertices) # 计算中心点和精确尺寸 center_x = (min_x + max_x) / 2 center_y = (min_y + max_y) / 2 center_z = (min_z + max_z) / 2 size_x = max(max_x - min_x, 0.001) # 确保最小尺寸 size_y = max(max_y - min_y, 0.001) size_z = max(max_z - min_z, 0.001) logger.info( f" 计算板材尺寸: {size_x:.3f}x{size_y:.3f}x{size_z:.3f}m, 中心: ({center_x:.3f},{center_y:.3f},{center_z:.3f})") # 【修复1】完全避免bpy.ops,直接创建网格对象 board = None try: # 创建网格数据 mesh_data = bpy.data.meshes.new("Board_Mesh") # 创建立方体的顶点和面 vertices = [ (-0.5, -0.5, -0.5), # 0 (0.5, -0.5, -0.5), # 1 (0.5, 0.5, -0.5), # 2 (-0.5, 0.5, -0.5), # 3 (-0.5, -0.5, 0.5), # 4 (0.5, -0.5, 0.5), # 5 (0.5, 0.5, 0.5), # 6 (-0.5, 0.5, 0.5) # 7 ] faces = [ (0, 1, 2, 3), # 底面 (4, 7, 6, 5), # 顶面 (0, 4, 5, 1), # 前面 (2, 6, 7, 3), # 后面 (1, 5, 6, 2), # 右面 (0, 3, 7, 4) # 左面 ] # 创建网格 mesh_data.from_pydata(vertices, [], faces) mesh_data.update() # 创建对象 board = bpy.data.objects.new("Board", mesh_data) # 设置位置 board.location = (center_x, center_y, center_z) # 添加到场景 bpy.context.scene.collection.objects.link(board) logger.info("✅ 使用直接创建方式成功创建板材对象") except Exception as create_error: logger.error(f"直接创建板材对象失败: {create_error}") return None if not board: logger.error("无法创建板材对象") return None # 【修复2】缩放到精确尺寸 board.scale = (size_x, size_y, size_z) # 【调试】添加缩放验证日志 logger.info(f"🔧 板材缩放信息:") logger.info( f" 计算尺寸: {size_x:.6f} x {size_y:.6f} x {size_z:.6f} 米") logger.info(f" 应用缩放: {board.scale}") logger.info( f" 中心位置: ({center_x:.6f}, {center_y:.6f}, {center_z:.6f})") logger.info(f" 板材名称: {board.name}") # 【修复3】设置属性和父子关系 board.parent = part board.name = f"Board_{part.name}" board["sw_face_type"] = "board" board["sw_uid"] = part.get("sw_uid") board["sw_cp"] = part.get("sw_cp") board["sw_typ"] = "board" logger.info(f"✅ 板材属性设置完成: {board.name}, 父对象: {part.name}") # 【修复4】关联材质 - 使用修复后的材质管理器 color = data.get("ckey", "mat_default") if color: try: # 导入材质管理器 from suw_core.material_manager import MaterialManager material_manager = MaterialManager() material = material_manager.get_texture(color) if material and board.data: # 清空现有材质 board.data.materials.clear() # 添加新材质 board.data.materials.append(material) logger.info(f"✅ 材质 {color} 已关联到板材 {board.name}") else: logger.warning(f"材质 {color} 未找到或板材数据无效") except Exception as e: logger.error(f"关联材质失败: {e}") # 【修复5】启用UV - 移除依赖图更新 self.enable_uv_for_board(board) return board else: logger.warning("顶点数据不足,创建默认板材") return self.create_default_board_with_material(part, data) except Exception as e: logger.error(f"创建板材失败: {e}") return self.create_default_board_with_material(part, data) def enable_uv_for_board(self, board): """为板件启用UV - 保持原始方法名和参数""" try: if not board or not board.data: logger.warning("无效的板件对象,无法启用UV") return # 确保网格数据存在 mesh = board.data if not mesh: logger.warning("板件没有网格数据") return # 创建UV贴图层(如果不存在) if not mesh.uv_layers: uv_layer = mesh.uv_layers.new(name="UVMap") else: uv_layer = mesh.uv_layers[0] # 确保UV层是活动的 mesh.uv_layers.active = uv_layer # 更新网格数据 - 移除可能导致依赖图更新的操作 # mesh.calc_loop_triangles() # 移除这行 # 为立方体创建基本UV坐标 if len(mesh.polygons) == 6: # 标准立方体 # 为每个面分配UV坐标 for poly_idx, poly in enumerate(mesh.polygons): # 标准UV坐标 (0,0) (1,0) (1,1) (0,1) uv_coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] for loop_idx, loop_index in enumerate(poly.loop_indices): if loop_idx < len(uv_coords): uv_layer.data[loop_index].uv = uv_coords[loop_idx] else: # 为非标准网格设置简单UV for loop in mesh.loops: uv_layer.data[loop.index].uv = (0.5, 0.5) # 更新网格 - 移除可能导致依赖图更新的操作 # mesh.update() # 移除这行 except Exception as e: logger.error(f"启用UV失败: {e}") def create_default_board_with_material(self, part, data): """创建默认板材 - 修复版本,使用更安全的对象创建方式""" try: # 【修复1】使用更安全的对象创建方式,避免bpy.ops上下文问题 board = None # 方法1:尝试使用bpy.ops(如果上下文可用) try: if hasattr(bpy.context, 'active_object'): bpy.ops.mesh.primitive_cube_add( size=1, location=(0, 0, 0) ) board = bpy.context.active_object logger.info("✅ 使用bpy.ops成功创建立方体") else: raise Exception("bpy.context.active_object不可用") except Exception as ops_error: logger.warning(f"使用bpy.ops创建对象失败: {ops_error}") # 方法2:回退方案 - 直接创建网格对象 try: # 创建网格数据 mesh_data = bpy.data.meshes.new("Board_Mesh") # 创建立方体的顶点和面 vertices = [ (-0.5, -0.5, -0.5), # 0 (0.5, -0.5, -0.5), # 1 (0.5, 0.5, -0.5), # 2 (-0.5, 0.5, -0.5), # 3 (-0.5, -0.5, 0.5), # 4 (0.5, -0.5, 0.5), # 5 (0.5, 0.5, 0.5), # 6 (-0.5, 0.5, 0.5) # 7 ] faces = [ (0, 1, 2, 3), # 底面 (4, 7, 6, 5), # 顶面 (0, 4, 5, 1), # 前面 (2, 6, 7, 3), # 后面 (1, 5, 6, 2), # 右面 (0, 3, 7, 4) # 左面 ] # 创建网格 mesh_data.from_pydata(vertices, [], faces) mesh_data.update() # 创建对象 board = bpy.data.objects.new("Board_Default", mesh_data) # 添加到场景 bpy.context.collection.objects.link(board) logger.info("✅ 使用直接创建方式成功创建立方体") except Exception as direct_error: logger.error(f"直接创建对象也失败: {direct_error}") return None if not board: logger.error("无法创建板材对象") return None # 【修复2】设置属性和父子关系 try: board.parent = part board.name = f"Board_{part.name}_default" board["sw_face_type"] = "board" # 从part获取uid和cp信息 uid = part.get("sw_uid") cp = part.get("sw_cp") board["sw_uid"] = uid board["sw_cp"] = cp board["sw_typ"] = "board" logger.info(f"✅ 默认板材属性设置完成: {board.name}, 父对象: {part.name}") except Exception as attr_error: logger.error(f"设置板材属性失败: {attr_error}") # 【修复3】关联默认材质 - 使用更安全的材质处理 try: color = data.get("ckey", "mat_default") # 使用更安全的材质管理器初始化方式 if not mm_module.material_manager: mm_module.material_manager = mm_module.MaterialManager() # 额外安全检查 if mm_module.material_manager and hasattr(mm_module.material_manager, 'get_texture'): material = mm_module.material_manager.get_texture(color) else: logger.error("材质管理器未正确初始化") material = None if material and board.data: board.data.materials.clear() board.data.materials.append(material) logger.info(f"✅ 材质 {color} 已关联到板材 {board.name}") else: logger.warning(f"材质 {color} 未找到或板材数据无效") except Exception as material_error: logger.error(f"❌ 默认材质处理失败: {material_error}") # 【修复4】启用UV try: self.enable_uv_for_board(board) except Exception as uv_error: logger.error(f"启用UV失败: {uv_error}") return board except Exception as e: logger.error(f"创建默认板材失败: {e}") return None def parse_surface_vertices(self, surface): """解析表面顶点 - 保持原始方法名和参数""" try: vertices = [] if not surface: return vertices segs = surface.get("segs", []) for seg in segs: if len(seg) >= 2: coord_str = seg[0].strip('()') try: # 解析坐标字符串 coords = coord_str.split(',') if len(coords) >= 3: x = float(coords[0]) * 0.001 # 转换为米 y = float(coords[1]) * 0.001 z = float(coords[2]) * 0.001 vertices.append((x, y, z)) except ValueError as e: logger.warning(f"解析顶点坐标失败: {coord_str}, 错误: {e}") continue logger.debug(f"解析得到 {len(vertices)} 个顶点") return vertices except Exception as e: logger.error(f"解析表面顶点失败: {e}") return [] def _is_object_valid(self, obj) -> bool: """检查对象是否有效 - 保持原始方法名和参数""" try: if not obj: return False if not BLENDER_AVAILABLE: return True # 在非Blender环境中假设有效 # 检查对象是否仍在Blender数据中 return obj.name in bpy.data.objects except Exception: return False def clear_part_children(self, part): """清理部件子对象 - 保持原始方法名和参数""" try: if not part or not BLENDER_AVAILABLE: return # 清理所有子对象 children_to_remove = [] for child in part.children: children_to_remove.append(child) for child in children_to_remove: if child.name in bpy.data.objects: bpy.data.objects.remove(child, do_unlink=True) logger.info(f"清理部件 {part.name} 的 {len(children_to_remove)} 个子对象") except Exception as e: logger.error(f"清理部件子对象失败: {e}") def get_creation_stats(self) -> Dict[str, Any]: """获取创建统计信息""" return self.creation_stats.copy() def get_part_creator_stats(self) -> Dict[str, Any]: """获取部件创建器统计信息""" try: # 从data_manager获取parts数据 parts_data = {} if hasattr(self.data_manager, 'parts'): parts_data = self.data_manager.parts stats = { "manager_type": "PartCreator", "parts_by_uid": {uid: len(parts) for uid, parts in parts_data.items()}, "total_parts": sum(len(parts) for parts in parts_data.values()), "creation_stats": self.creation_stats.copy(), "data_manager_attached": self.data_manager is not None, "blender_available": BLENDER_AVAILABLE } return stats except Exception as e: logger.error(f"获取部件创建器统计失败: {e}") return {"error": str(e)} def reset_creation_stats(self): """重置创建统计信息""" self.creation_stats = { "parts_created": 0, "boards_created": 0, "creation_errors": 0 } logger.info("创建统计信息已重置") def _parse_surface_vertices(self, surface): """解析表面顶点坐标""" try: vertices = [] segs = surface.get("segs", []) for seg in segs: if len(seg) >= 2: coord_str = seg[0].strip('()') try: x, y, z = map(float, coord_str.split(',')) # 转换为米(Blender使用米作为单位) vertices.append((x * 0.001, y * 0.001, z * 0.001)) except ValueError: continue return vertices except Exception as e: logger.error(f"解析表面顶点失败: {e}") return [] def _calculate_board_dimensions(self, final_data: dict) -> Tuple[Optional['mathutils.Vector'], Optional['mathutils.Vector']]: """ [V2] 计算板材的精确尺寸和中心点。 """ try: obv_vertices = self._parse_surface_vertices(final_data.get("obv")) rev_vertices = self._parse_surface_vertices(final_data.get("rev")) if not obv_vertices or not rev_vertices: logger.warning("无法解析顶点数据,使用默认尺寸") return (None, None) all_vertices = obv_vertices + rev_vertices min_x = min(v[0] for v in all_vertices) max_x = max(v[0] for v in all_vertices) min_y = min(v[1] for v in all_vertices) max_y = max(v[1] for v in all_vertices) min_z = min(v[2] for v in all_vertices) max_z = max(v[2] for v in all_vertices) center_x = (min_x + max_x) / 2 center_y = (min_y + max_y) / 2 center_z = (min_z + max_z) / 2 size_x = max(max_x - min_x, 0.001) size_y = max(max_y - min_y, 0.001) size_z = max(max_z - min_z, 0.001) logger.info( f" 计算板材尺寸: {size_x:.3f}x{size_y:.3f}x{size_z:.3f}m, 中心: ({center_x:.3f},{center_y:.3f},{center_z:.3f})") return (mathutils.Vector((center_x, center_y, center_z)), mathutils.Vector((size_x, size_y, size_z))) except Exception as e: logger.error(f"❌ 计算板材尺寸失败: {e}") return None, None def _create_board_direct(self, parent_obj: 'bpy.types.Object', final_data: dict, center: 'mathutils.Vector', dimensions: 'mathutils.Vector') -> Optional['bpy.types.Object']: """ [V2] 通过直接操作bpy.data来安全地创建板材对象,避免bpy.ops的上下文错误。 """ try: # 1. 创建网格和对象数据 mesh_name = f"Board_Mesh_{parent_obj.name}" board_name = f"Board_{parent_obj.name}" mesh = bpy.data.meshes.new(mesh_name) board_obj = bpy.data.objects.new(board_name, mesh) # 2. 将新对象链接到与父对象相同的集合中 if parent_obj.users_collection: parent_obj.users_collection[0].objects.link(board_obj) else: # 如果父对象不在任何集合中,则回退到场景主集合 bpy.context.scene.collection.objects.link(board_obj) # 3. 直接根据最终尺寸创建顶点。这可以确保对象的缩放比例始终为(1,1,1) dx, dy, dz = dimensions.x / 2, dimensions.y / 2, dimensions.z / 2 verts = [ (dx, dy, dz), (-dx, dy, dz), (-dx, -dy, dz), (dx, -dy, dz), (dx, dy, -dz), (-dx, dy, -dz), (-dx, -dy, -dz), (dx, -dy, -dz), ] faces = [ (0, 1, 2, 3), (4, 7, 6, 5), (0, 4, 5, 1), (1, 5, 6, 2), (2, 6, 7, 3), (3, 7, 4, 0) ] mesh.from_pydata(verts, [], faces) mesh.update() # 4. 设置最终的位置和父子关系 board_obj.location = center board_obj.parent = parent_obj return board_obj except Exception as e: logger.error(f"❌ 使用直接数据创建板材时失败: {e}") # 清理创建失败时可能产生的孤立数据 if 'board_obj' in locals() and board_obj and board_obj.name in bpy.data.objects: bpy.data.objects.remove(board_obj, do_unlink=True) if 'mesh' in locals() and mesh and mesh.name in bpy.data.meshes: bpy.data.meshes.remove(mesh) return None def _add_board_part(self, part_obj: 'bpy.types.Object', final_data: dict) -> Optional['bpy.types.Object']: """ [V2] 将板材对象添加到部件对象的集合中。 """ try: # 1. 计算板材的精确尺寸和中心点 center, dimensions = self._calculate_board_dimensions(final_data) if not center or not dimensions: logger.warning(f"无法计算板材尺寸,跳过添加板材: {final_data.get('name')}") return None # 2. 使用直接数据创建板材对象 board_obj = self._create_board_direct( part_obj, final_data, center, dimensions) if not board_obj: logger.warning( f"使用直接数据创建板材失败,跳过添加板材: {final_data.get('name')}") return None # 3. 设置板材属性 board_obj["sw_face_type"] = "board" board_obj["sw_uid"] = part_obj.get("sw_uid") board_obj["sw_cp"] = part_obj.get("sw_cp") board_obj["sw_typ"] = "board" # 4. 关联材质 color = final_data.get("ckey", "mat_default") if color: try: # 导入材质管理器 from suw_core.material_manager import MaterialManager material_manager = MaterialManager() material = material_manager.get_texture(color) if material and board_obj.data: board_obj.data.materials.clear() board_obj.data.materials.append(material) logger.info(f"✅ 材质 {color} 已关联到板材 {board_obj.name}") else: logger.warning(f"材质 {color} 未找到或板材数据无效") except Exception as e: logger.error(f"关联材质失败: {e}") # 5. 启用UV self.enable_uv_for_board(board_obj) return board_obj except Exception as e: logger.error(f"❌ 添加板材失败: {e}") return None # ==================== 模块实例 ==================== # 全局实例 part_creator = None def init_part_creator(): """初始化部件创建器 - 不再需要suw_impl参数""" global part_creator part_creator = PartCreator() return part_creator def get_part_creator(): """获取全局部件创建器实例""" global part_creator if part_creator is None: part_creator = init_part_creator() return part_creator # 确保PartCreator全局实例正确初始化 if part_creator is None: part_creator = init_part_creator()