492 lines
19 KiB
Python
492 lines
19 KiB
Python
|
#!/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 .material_manager import material_manager
|
|||
|
from .memory_manager import memory_manager
|
|||
|
import time
|
|||
|
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 PartCreator:
|
|||
|
"""部件创建器 - 负责所有部件相关操作"""
|
|||
|
|
|||
|
def __init__(self, suw_impl=None):
|
|||
|
"""
|
|||
|
初始化部件创建器
|
|||
|
|
|||
|
Args:
|
|||
|
suw_impl: SUWImpl实例引用(可选)
|
|||
|
"""
|
|||
|
self.suw_impl = suw_impl
|
|||
|
|
|||
|
# 部件数据存储
|
|||
|
self.parts = {} # 按uid存储部件数据
|
|||
|
|
|||
|
# 创建统计
|
|||
|
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]:
|
|||
|
"""获取零件信息 - 保持原始方法名和参数"""
|
|||
|
uid = data.get("uid")
|
|||
|
if uid not in self.parts:
|
|||
|
self.parts[uid] = {}
|
|||
|
return self.parts[uid]
|
|||
|
|
|||
|
def c04(self, data: Dict[str, Any]):
|
|||
|
"""c04 - 添加部件 - 保持原始方法名"""
|
|||
|
return self.create_part(data)
|
|||
|
|
|||
|
def create_part(self, data: Dict[str, Any]):
|
|||
|
"""创建部件 - 保持原始方法名和参数"""
|
|||
|
try:
|
|||
|
if not BLENDER_AVAILABLE:
|
|||
|
logger.warning("Blender不可用,无法创建部件")
|
|||
|
return None
|
|||
|
|
|||
|
uid = data.get("uid")
|
|||
|
root = data.get("cp")
|
|||
|
|
|||
|
if not uid or root is None:
|
|||
|
logger.error("缺少必要的uid或cp参数")
|
|||
|
return None
|
|||
|
|
|||
|
logger.info(f"开始创建部件: uid={uid}, cp={root}")
|
|||
|
|
|||
|
# 【数据结构优先策略】先处理数据结构,后处理Blender对象
|
|||
|
parts = self.get_parts(data)
|
|||
|
|
|||
|
# 检查数据结构中是否已存在
|
|||
|
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}")
|
|||
|
# 【关键】数据结构优先清理,对应c09的删除顺序
|
|||
|
del parts[root]
|
|||
|
|
|||
|
# 创建部件容器
|
|||
|
part_name = f"Part_{root}"
|
|||
|
part = bpy.data.objects.new(part_name, None)
|
|||
|
bpy.context.scene.collection.objects.link(part)
|
|||
|
|
|||
|
logger.info(f"✅ 创建Part对象: {part_name}")
|
|||
|
|
|||
|
# 设置部件属性
|
|||
|
part["sw_uid"] = uid
|
|||
|
part["sw_cp"] = root
|
|||
|
part["sw_typ"] = "part"
|
|||
|
|
|||
|
# 【对称性】记录创建的对象类型,便于c09对称删除
|
|||
|
part["sw_created_objects"] = {
|
|||
|
"boards": [],
|
|||
|
"materials": [],
|
|||
|
"uv_layers": [],
|
|||
|
"creation_timestamp": time.time()
|
|||
|
}
|
|||
|
|
|||
|
# 存储部件到数据结构(数据结构优先)
|
|||
|
parts[root] = part
|
|||
|
memory_manager.register_object(part)
|
|||
|
|
|||
|
logger.info(f"✅ 部件存储到数据结构: uid={uid}, cp={root}")
|
|||
|
|
|||
|
# 处理finals数据
|
|||
|
finals = data.get("finals", [])
|
|||
|
logger.info(f"📦 处理 {len(finals)} 个板材数据")
|
|||
|
|
|||
|
created_boards = 0
|
|||
|
created_board_names = []
|
|||
|
for i, final_data in enumerate(finals):
|
|||
|
try:
|
|||
|
board = self.create_board_with_material_and_uv(
|
|||
|
part, final_data)
|
|||
|
if board:
|
|||
|
created_boards += 1
|
|||
|
# 【对称性】记录创建的板材,便于c09删除
|
|||
|
created_board_names.append(board.name)
|
|||
|
logger.info(
|
|||
|
f"✅ 板材 {i+1}/{len(finals)} 创建成功: {board.name}")
|
|||
|
else:
|
|||
|
logger.warning(f"⚠️ 板材 {i+1}/{len(finals)} 创建失败")
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"❌ 创建板材 {i+1}/{len(finals)} 失败: {e}")
|
|||
|
|
|||
|
# 【对称性】更新创建记录
|
|||
|
part["sw_created_objects"]["boards"] = created_board_names
|
|||
|
|
|||
|
logger.info(f"📊 板材创建统计: {created_boards}/{len(finals)} 成功")
|
|||
|
|
|||
|
# 更新统计信息
|
|||
|
self.creation_stats["parts_created"] += 1
|
|||
|
self.creation_stats["boards_created"] += created_boards
|
|||
|
|
|||
|
# 验证创建结果
|
|||
|
if part.name in bpy.data.objects:
|
|||
|
logger.info(f"🎉 部件创建完全成功: {part_name}")
|
|||
|
return part
|
|||
|
else:
|
|||
|
logger.error(f"❌ 部件创建验证失败: {part_name} 不在Blender中")
|
|||
|
self.creation_stats["creation_errors"] += 1
|
|||
|
return None
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"❌ 创建部件失败: {e}")
|
|||
|
self.creation_stats["creation_errors"] += 1
|
|||
|
import traceback
|
|||
|
logger.error(traceback.format_exc())
|
|||
|
return None
|
|||
|
|
|||
|
def create_board_with_material_and_uv(self, part, data):
|
|||
|
"""创建板材并关联材质和启用UV - 保持原始方法名和参数"""
|
|||
|
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})")
|
|||
|
|
|||
|
# 创建精确尺寸的立方体
|
|||
|
bpy.ops.mesh.primitive_cube_add(
|
|||
|
size=1,
|
|||
|
location=(center_x, center_y, center_z)
|
|||
|
)
|
|||
|
# 安全获取active_object
|
|||
|
if hasattr(bpy.context, 'active_object') and bpy.context.active_object:
|
|||
|
board = bpy.context.active_object
|
|||
|
else:
|
|||
|
# 回退方案:查找最近创建的网格对象
|
|||
|
board = None
|
|||
|
for obj in reversed(list(bpy.data.objects)):
|
|||
|
if obj.type == 'MESH':
|
|||
|
board = obj
|
|||
|
break
|
|||
|
if not board:
|
|||
|
logger.error("无法获取活动对象")
|
|||
|
raise Exception("无法获取活动对象")
|
|||
|
|
|||
|
# 缩放到精确尺寸
|
|||
|
board.scale = (size_x, size_y, size_z)
|
|||
|
|
|||
|
# 设置属性和父子关系
|
|||
|
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}")
|
|||
|
|
|||
|
# 关联材质 - 使用动态初始化方式
|
|||
|
color = data.get("ckey", "mat_default")
|
|||
|
if color:
|
|||
|
try:
|
|||
|
# 【修复】使用与CommandDispatcher相同的初始化方式
|
|||
|
from . import material_manager as mm_module
|
|||
|
if not mm_module.material_manager:
|
|||
|
mm_module.material_manager = mm_module.MaterialManager(
|
|||
|
self.suw_impl)
|
|||
|
material = mm_module.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 material_error:
|
|||
|
logger.error(f"❌ 材质处理失败: {material_error}")
|
|||
|
logger.warning(f"材质 {color} 未找到或板材数据无效")
|
|||
|
|
|||
|
# 启用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:
|
|||
|
# 创建默认立方体
|
|||
|
bpy.ops.mesh.primitive_cube_add(
|
|||
|
size=1,
|
|||
|
location=(0, 0, 0)
|
|||
|
)
|
|||
|
# 安全获取active_object
|
|||
|
if hasattr(bpy.context, 'active_object') and bpy.context.active_object:
|
|||
|
board = bpy.context.active_object
|
|||
|
else:
|
|||
|
# 回退方案:查找最近创建的网格对象
|
|||
|
board = None
|
|||
|
for obj in reversed(list(bpy.data.objects)):
|
|||
|
if obj.type == 'MESH':
|
|||
|
board = obj
|
|||
|
break
|
|||
|
if not board:
|
|||
|
logger.error("无法获取活动对象")
|
|||
|
raise Exception("无法获取活动对象")
|
|||
|
# 设置属性和父子关系
|
|||
|
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}")
|
|||
|
|
|||
|
# 关联默认材质 - 使用动态初始化方式
|
|||
|
color = data.get("ckey", "mat_default")
|
|||
|
try:
|
|||
|
from . import material_manager as mm_module
|
|||
|
if not mm_module.material_manager:
|
|||
|
mm_module.material_manager = mm_module.MaterialManager(
|
|||
|
self.suw_impl)
|
|||
|
material = mm_module.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}")
|
|||
|
except Exception as material_error:
|
|||
|
logger.error(f"❌ 默认材质处理失败: {material_error}")
|
|||
|
|
|||
|
# 启用UV
|
|||
|
self.enable_uv_for_board(board)
|
|||
|
|
|||
|
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:
|
|||
|
stats = {
|
|||
|
"manager_type": "PartCreator",
|
|||
|
"parts_by_uid": {uid: len(parts) for uid, parts in self.parts.items()},
|
|||
|
"total_parts": sum(len(parts) for parts in self.parts.values()),
|
|||
|
"creation_stats": self.creation_stats.copy(),
|
|||
|
"suw_impl_attached": self.suw_impl 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("创建统计信息已重置")
|
|||
|
|
|||
|
|
|||
|
# ==================== 模块实例 ====================
|
|||
|
|
|||
|
# 全局实例,将由SUWImpl初始化时设置
|
|||
|
part_creator = None
|
|||
|
|
|||
|
|
|||
|
def init_part_creator(suw_impl):
|
|||
|
"""初始化部件创建器"""
|
|||
|
global part_creator
|
|||
|
part_creator = PartCreator(suw_impl)
|
|||
|
return part_creator
|
|||
|
|
|||
|
|
|||
|
# 确保PartCreator全局实例正确初始化
|
|||
|
if part_creator is None:
|
|||
|
print("🔧 强制初始化PartCreator全局实例...")
|
|||
|
part_creator = PartCreator(None) # 【修复】传入None参数
|