diff --git a/Start_Run.py b/Start_Run.py index fa515efa..8e6a1214 100644 --- a/Start_Run.py +++ b/Start_Run.py @@ -21,8 +21,14 @@ sys.path.insert(0, icons_path) if __name__ == "__main__": args = sys.argv[1:] # args = "/home/tiger/桌面/Test1" + # args = "C:/Users/29381/Desktop/1" + print(f'Path is {args}') + # 将整个列表转换为字符串(包括方括号) + args_str = ''.join(args) + from main import run if args: - run(args[0]) + run(args_str) + # run(args) else: run() \ No newline at end of file diff --git a/core/collision_manager.py b/core/collision_manager.py index fb247f55..dec87ece 100644 --- a/core/collision_manager.py +++ b/core/collision_manager.py @@ -325,18 +325,29 @@ class CollisionManager: return CollisionSphere(center, kwargs.get('radius', radius)) elif shape_type == 'box': - # 创建包围盒 - min_point = bounds.getMin() - max_point = bounds.getMax() + # 创建自定义尺寸的包围盒 + width = kwargs.get('width', bounds.getMax().x - bounds.getMin().x) + length = kwargs.get('length', bounds.getMax().y - bounds.getMin().y) + height = kwargs.get('height', bounds.getMax().z - bounds.getMin().z) + + # 计算盒子的最小和最大点 + half_width = width / 2 + half_length = length / 2 + half_height = height / 2 + + min_point = Point3(center.x - half_width, center.y - half_length, center.z - half_height) + max_point = Point3(center.x + half_width, center.y + half_length, center.z + half_height) return CollisionBox(min_point, max_point) elif shape_type == 'capsule': - # 创建胶囊体(适合角色) - height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z)) - radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, height * 0.3)) - point_a = Point3(center.x, center.y, bounds.getMin().z + radius) - point_b = Point3(center.x, center.y, bounds.getMax().z - radius) - return CollisionCapsule(point_a, point_b, radius) + # 创建自定义参数的胶囊体 + custom_height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z)) + custom_radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, custom_height * 0.3)) + + # 计算胶囊体的两个端点 + point_a = Point3(center.x, center.y, center.z - custom_height/2 + custom_radius) + point_b = Point3(center.x, center.y, center.z + custom_height/2 - custom_radius) + return CollisionCapsule(point_a, point_b, custom_radius) elif shape_type == 'plane': # 创建平面(适合地面、墙面) diff --git a/core/world.py b/core/world.py index 4ed52631..0f4ec28a 100644 --- a/core/world.py +++ b/core/world.py @@ -288,7 +288,8 @@ class CoreWorld(Panda3DWorld): self.cam.setPos(0, -50, 20) self.cam.lookAt(0, 0, 0) self.camLens.setFov(80) - # self.cam.setTag("is_scene_element", "1") + self.cam.setTag("is_scene_element", "1") + self.cam.setTag("tree_item_type", "CAMERA_NODE") print("✓ 相机设置完成") def _setupLighting(self): @@ -323,6 +324,7 @@ class CoreWorld(Panda3DWorld): self.ground.setZ(-1.0) self.ground.setColor(0.8, 0.8, 0.8, 1) # self.ground.setTag("is_scene_element", "1") + # self.ground.setTag("tree_item_type", "SCENE_NODE") # 创建支持贴图的材质 mat = Material() diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 83737e6f..bbd0beff 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -723,6 +723,7 @@ class GUIManager: textNodePath.setTag("is_scene_element", "1") textNodePath.setTag("tree_item_type", "GUI_3DTEXT") textNodePath.setTag("created_by_user", "1") + textNodePath.setTag("name", text_name) # 添加到GUI元素列表 self.gui_elements.append(textNodePath) diff --git a/icons/logo.png b/icons/logo.png new file mode 100644 index 00000000..361b35c7 Binary files /dev/null and b/icons/logo.png differ diff --git a/main.py b/main.py index 4d150534..c52d28c4 100644 --- a/main.py +++ b/main.py @@ -935,7 +935,7 @@ def run(args = None): # 使用新的UI模块创建主窗口 from ui.main_window import setup_main_window - + print(f'Path is {args}') app, main_window = setup_main_window(world, args) # 启动应用程序 diff --git a/project/project_manager.py b/project/project_manager.py index c3370792..31b2df70 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -217,7 +217,7 @@ class ProjectManager: config_file = os.path.join(project_path, "project.json") if not os.path.exists(config_file): if parent_window: - QMessageBox.warning(parent_window, "警告", "选择的不是有效的项目文件夹!") + QMessageBox.warning(parent_window, "警告", f"选择的不是有效的项目文件夹!{project_path}") else: print("警告: 选择的不是有效的项目文件夹!") return False diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 41175d72..c5c0b810 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -236,178 +236,34 @@ class SceneManager: print(f"导入模型失败: {str(e)}") return None - def _validateAndFixAllTransforms(self, node_path, depth=0): - """验证并修复所有节点的变换矩阵""" - indent = " " * depth + def _applyModelScale(self, model, scale_factor): + """应用模型特定缩放 + + Args: + model: 要缩放的模型 + scale_factor: 缩放因子 + """ try: - if node_path.isEmpty(): - return + print(f"应用模型缩放因子: {scale_factor}") - # 获取节点名称 - node_name = node_path.getName() - print(f"{indent}检查节点变换: {node_name}") + # 获取当前边界用于后续位置调整 + original_bounds = model.getBounds() - # 修复变换矩阵问题 - self._fixNodeTransform(node_path) + # 应用缩放 + model.setScale(scale_factor) - # 递归检查子节点 - for i in range(node_path.getNumChildren()): - try: - child = node_path.getChild(i) - self._validateAndFixAllTransforms(child, depth + 1) - except Exception as e: - print(f"{indent}⚠️ 处理子节点 {i} 时出错: {e}") - continue + # 重新调整位置(因为缩放会影响边界) + if original_bounds and not original_bounds.isEmpty(): + new_bounds = model.getBounds() + min_point = new_bounds.getMin() + ground_offset = -min_point.getZ() + model.setZ(ground_offset) + print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") + + print(f"模型缩放完成,缩放因子: {scale_factor}") except Exception as e: - print(f"{indent}❌ 检查节点变换时出错: {node_name} - {e}") - - def _fixNodeTransform(self, node_path): - """修复单个节点的变换问题""" - try: - if node_path.isEmpty(): - return - - node_name = node_path.getName() - - # 方法1: 尝试获取和验证当前变换 - try: - transform = node_path.getTransform() - if not transform.hasMat(): - print(f"⚠️ 节点 {node_name} 没有有效变换矩阵") - node_path.clearTransform() - return - except Exception as e: - print(f"⚠️ 获取节点 {node_name} 变换时出错: {e}") - node_path.clearTransform() - return - - # 方法2: 简单的矩阵有效性检查(移除 isSingular 调用) - try: - matrix = node_path.getMat() - # 检查矩阵元素是否有效 - is_valid = True - for i in range(4): - for j in range(4): - element = matrix.getCell(i, j) - # 检查是否为 NaN 或无穷大 - if str(element) in ['nan', 'inf', '-inf'] or abs(element) > 1e10: - is_valid = False - break - if not is_valid: - break - - if not is_valid: - print(f"⚠️ 节点 {node_name} 有无效矩阵") - node_path.clearTransform() - return - except Exception as e: - print(f"⚠️ 检查节点 {node_name} 矩阵时出错: {e}") - node_path.clearTransform() - return - - # 方法3: 检查缩放值 - try: - scale = node_path.getScale() - # 检查是否包含无效值 - if (scale.getX() != scale.getX() or # NaN检查 - scale.getY() != scale.getY() or - scale.getZ() != scale.getZ() or - abs(scale.getX()) > 1e10 or # 无穷大检查 - abs(scale.getY()) > 1e10 or - abs(scale.getZ()) > 1e10): - print(f"⚠️ 节点 {node_name} 有无效缩放值: {scale}") - node_path.setScale(1.0) - return - - # 检查零缩放 - if (abs(scale.getX()) < 1e-10 or - abs(scale.getY()) < 1e-10 or - abs(scale.getZ()) < 1e-10): - print(f"⚠️ 节点 {node_name} 有零或近零缩放: {scale}") - node_path.setScale(1.0) - return - except Exception as e: - print(f"⚠️ 检查节点 {node_name} 缩放时出错: {e}") - try: - node_path.setScale(1.0) - except: - node_path.clearTransform() - return - - # 方法4: 检查位置值 - try: - pos = node_path.getPos() - # 检查是否包含无效值 - if (pos.getX() != pos.getX() or # NaN检查 - pos.getY() != pos.getY() or - pos.getZ() != pos.getZ() or - abs(pos.getX()) > 1e10 or # 无穷大检查 - abs(pos.getY()) > 1e10 or - abs(pos.getZ()) > 1e10): - print(f"⚠️ 节点 {node_name} 有无效位置值: {pos}") - node_path.setPos(0, 0, 0) - except Exception as e: - print(f"⚠️ 检查节点 {node_name} 位置时出错: {e}") - try: - node_path.setPos(0, 0, 0) - except: - pass - - # 方法5: 检查旋转值 - try: - hpr = node_path.getHpr() - # 检查是否包含无效值 - if (hpr.getX() != hpr.getX() or # NaN检查 - hpr.getY() != hpr.getY() or - hpr.getZ() != hpr.getZ()): - print(f"⚠️ 节点 {node_name} 有无效旋转值: {hpr}") - node_path.setHpr(0, 0, 0) - except Exception as e: - print(f"⚠️ 检查节点 {node_name} 旋转时出错: {e}") - try: - node_path.setHpr(0, 0, 0) - except: - pass - - except Exception as e: - print(f"❌ 修复节点 {node_path.getName()} 变换时出错: {e}") - try: - node_path.clearTransform() - except: - pass - - def _safeReparentTo(self, child_node, parent_node): - """安全地重新设置父节点,避免矩阵问题""" - try: - if child_node.isEmpty() or parent_node.isEmpty(): - print("⚠️ 空节点无法挂载") - return - - # 在重新设置父节点前验证和修复变换 - self._fixNodeTransform(child_node) - - # 使用 wrtReparentTo 避免变换问题 - child_node.wrtReparentTo(parent_node) - print(f"✅ 节点 {child_node.getName()} 已安全挂载到 {parent_node.getName()}") - - except Exception as e: - print(f"❌ 安全挂载节点失败: {e}") - # 备用方案:先清除变换再挂载 - try: - child_node.clearTransform() - child_node.reparentTo(parent_node) - print(f"✅ 使用备用方案挂载节点: {child_node.getName()}") - except Exception as e2: - print(f"❌ 备用方案也失败: {e2}") - # 最后的备用方案:创建新节点并复制内容 - try: - new_node = parent_node.attachNewNode(child_node.getName()) - child_node.copyTo(new_node) - child_node.removeNode() - print(f"✅ 使用最终备用方案挂载节点: {new_node.getName()}") - except Exception as e3: - print(f"❌ 所有方案都失败: {e3}") + print(f"应用模型缩放失败: {str(e)}") def _applyMaterialsToModel(self, model): """递归应用材质到模型的所有GeomNode""" @@ -536,37 +392,6 @@ class SceneManager: print(f"应用材质时出错: {e}") print("=== 材质设置完成 ===\n") - - def _applyModelScale(self, model, scale_factor): - """应用模型特定缩放 - - Args: - model: 要缩放的模型 - scale_factor: 缩放因子 - """ - try: - print(f"应用模型缩放因子: {scale_factor}") - - # 获取当前边界用于后续位置调整 - original_bounds = model.getBounds() - - # 应用缩放 - model.setScale(scale_factor) - - # 重新调整位置(因为缩放会影响边界) - if original_bounds and not original_bounds.isEmpty(): - new_bounds = model.getBounds() - min_point = new_bounds.getMin() - ground_offset = -min_point.getZ() - model.setZ(ground_offset) - print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") - - print(f"模型缩放完成,缩放因子: {scale_factor}") - - except Exception as e: - print(f"应用模型缩放失败: {str(e)}") - - def _adjustModelToGround(self, model): """智能调整模型到地面,但保持原有缩放结构""" try: @@ -1691,12 +1516,11 @@ class SceneManager: if not gui_manager: print("GUI管理器未找到,无法重建GUI元素") return - print(f"开始重建 {len(gui_data)} 个GUI元素...") - # 用于跟踪已处理的元素名称,防止重复创建 processed_names = set() + pos = (0, 0, 0) for i, gui_info in enumerate(gui_data): try: gui_type = gui_info.get("type", "unknown") diff --git a/ui/icon_manager.py b/ui/icon_manager.py new file mode 100644 index 00000000..944c3aa6 --- /dev/null +++ b/ui/icon_manager.py @@ -0,0 +1,322 @@ +""" +图标管理工具 + +负责统一管理应用程序中的所有图标: +- 图标路径解析 +- 图标缓存 +- 图标预加载 +- 图标错误处理 +""" +import os +import sys +from typing import Dict, Optional +from PyQt5.QtGui import QIcon, QPixmap +from PyQt5.QtCore import QSize + + +class IconManager: + """图标管理器类""" + + def __init__(self): + """初始化图标管理器""" + self.icon_cache: Dict[str, QIcon] = {} + self.icon_directory = self._get_icon_directory() + self.default_icon = None + + # 预定义的图标映射 + self.icon_map = { + # 主窗口图标 + 'app_logo': 'logo.png', + + # 工具栏图标 + 'select_tool': 'select_tool.png', + 'move_tool': 'move_tool.png', + 'rotate_tool': 'rotate_tool.png', + 'scale_tool': 'scale_tool.png', + + # 菜单图标(如果有的话) + 'new_file': 'new_file.png', + 'open_file': 'open_file.png', + 'save_file': 'save_file.png', + 'exit': 'exit.png', + + # 对象类型图标 + 'object_3d': 'object_3d.png', + 'light': 'light.png', + 'camera': 'camera.png', + 'terrain': 'terrain.png', + 'script': 'script.png', + + # 状态图标 + 'success': 'success.png', + 'warning': 'warning.png', + 'error': 'error.png', + 'info': 'info.png', + } + + # 初始化默认图标 + self._create_default_icon() + + # 预加载常用图标 + self._preload_icons() + + def _get_icon_directory(self) -> str: + """获取图标目录的绝对路径""" + # 获取当前文件的目录(ui目录) + current_dir = os.path.dirname(os.path.abspath(__file__)) + # 获取项目根目录(ui的父目录) + project_root = os.path.dirname(current_dir) + # 拼接icons目录路径 + icon_dir = os.path.join(project_root, "icons") + + print(f"🔍 图标目录路径: {icon_dir}") + + # 检查目录是否存在 + if not os.path.exists(icon_dir): + print(f"⚠️ 图标目录不存在: {icon_dir}") + # 尝试创建目录 + try: + os.makedirs(icon_dir, exist_ok=True) + print(f"✅ 已创建图标目录: {icon_dir}") + except Exception as e: + print(f"❌ 创建图标目录失败: {e}") + + return icon_dir + + def _create_default_icon(self): + """创建默认图标""" + # 创建一个简单的默认图标 + pixmap = QPixmap(16, 16) + pixmap.fill() # 填充为白色 + self.default_icon = QIcon(pixmap) + + def _preload_icons(self): + """预加载常用图标""" + print("🔄 开始预加载图标...") + + for icon_name, file_name in self.icon_map.items(): + icon_path = os.path.join(self.icon_directory, file_name) + if os.path.exists(icon_path): + try: + icon = QIcon(icon_path) + self.icon_cache[icon_name] = icon + print(f"✅ 已加载图标: {icon_name} -> {file_name}") + except Exception as e: + print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}") + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + + print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标") + + def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon: + """ + 获取图标 + + Args: + icon_name: 图标名称(可以是预定义名称或文件名) + size: 图标尺寸 + + Returns: + QIcon对象 + """ + # 首先检查缓存 + if icon_name in self.icon_cache: + icon = self.icon_cache[icon_name] + if size: + # 如果指定了尺寸,返回指定尺寸的图标 + pixmap = icon.pixmap(size) + return QIcon(pixmap) + return icon + + # 如果不在缓存中,尝试从映射中获取 + if icon_name in self.icon_map: + file_name = self.icon_map[icon_name] + icon_path = os.path.join(self.icon_directory, file_name) + else: + # 直接使用文件名 + icon_path = os.path.join(self.icon_directory, icon_name) + if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + icon_path += '.png' # 默认添加.png扩展名 + + # 尝试加载图标 + if os.path.exists(icon_path): + try: + icon = QIcon(icon_path) + # 缓存图标 + self.icon_cache[icon_name] = icon + print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}") + + if size: + pixmap = icon.pixmap(size) + return QIcon(pixmap) + return icon + except Exception as e: + print(f"❌ 加载图标失败: {icon_path}, 错误: {e}") + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + + # 返回默认图标 + return self.default_icon + + def get_icon_path(self, icon_name: str) -> str: + """ + 获取图标文件的完整路径 + + Args: + icon_name: 图标名称 + + Returns: + 图标文件的完整路径 + """ + if icon_name in self.icon_map: + file_name = self.icon_map[icon_name] + else: + file_name = icon_name + if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + file_name += '.png' + + icon_path = os.path.join(self.icon_directory, file_name) + + if os.path.exists(icon_path): + return icon_path + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + return "" + + def has_icon(self, icon_name: str) -> bool: + """ + 检查图标是否存在 + + Args: + icon_name: 图标名称 + + Returns: + 是否存在 + """ + return bool(self.get_icon_path(icon_name)) + + def add_icon(self, icon_name: str, icon_path: str) -> bool: + """ + 添加新图标到缓存 + + Args: + icon_name: 图标名称 + icon_path: 图标文件路径 + + Returns: + 是否添加成功 + """ + try: + if os.path.exists(icon_path): + icon = QIcon(icon_path) + self.icon_cache[icon_name] = icon + print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}") + return True + else: + print(f"❌ 图标文件不存在: {icon_path}") + return False + except Exception as e: + print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}") + return False + + def refresh_cache(self): + """刷新图标缓存""" + print("🔄 刷新图标缓存...") + self.icon_cache.clear() + self._preload_icons() + + def get_available_icons(self) -> list: + """获取所有可用的图标列表""" + available_icons = [] + + # 添加预定义的图标 + available_icons.extend(self.icon_map.keys()) + + # 扫描图标目录中的所有图标文件 + if os.path.exists(self.icon_directory): + for file_name in os.listdir(self.icon_directory): + if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + icon_name = os.path.splitext(file_name)[0] + if icon_name not in available_icons: + available_icons.append(icon_name) + + return sorted(available_icons) + + def get_cache_info(self) -> dict: + """获取缓存信息""" + return { + 'cached_icons': len(self.icon_cache), + 'icon_directory': self.icon_directory, + 'available_icons': len(self.get_available_icons()), + 'cache_keys': list(self.icon_cache.keys()) + } + + def debug_info(self): + """打印调试信息""" + print("=" * 50) + print("📋 图标管理器调试信息") + print("=" * 50) + + info = self.get_cache_info() + print(f"图标目录: {info['icon_directory']}") + print(f"目录存在: {os.path.exists(info['icon_directory'])}") + print(f"缓存图标数: {info['cached_icons']}") + print(f"可用图标数: {info['available_icons']}") + + if info['cache_keys']: + print("\n已缓存的图标:") + for key in info['cache_keys']: + print(f" - {key}") + + print("\n图标目录内容:") + if os.path.exists(self.icon_directory): + for file_name in os.listdir(self.icon_directory): + file_path = os.path.join(self.icon_directory, file_name) + size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0 + print(f" - {file_name} ({size} bytes)") + else: + print(" 目录不存在") + + print("=" * 50) + + +# 全局图标管理器实例 +_icon_manager = None + + +def get_icon_manager() -> IconManager: + """获取全局图标管理器实例""" + global _icon_manager + if _icon_manager is None: + _icon_manager = IconManager() + return _icon_manager + + +def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon: + """便捷函数:获取图标""" + return get_icon_manager().get_icon(icon_name, size) + + +def get_icon_path(icon_name: str) -> str: + """便捷函数:获取图标路径""" + return get_icon_manager().get_icon_path(icon_name) + + +def has_icon(icon_name: str) -> bool: + """便捷函数:检查图标是否存在""" + return get_icon_manager().has_icon(icon_name) + + +if __name__ == "__main__": + # 测试代码 + print("🧪 测试图标管理器...") + + manager = IconManager() + manager.debug_info() + + # 测试获取图标 + logo_icon = manager.get_icon('app_logo') + print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}") + + move_tool_icon = manager.get_icon('move_tool') + print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}") \ No newline at end of file diff --git a/ui/icon_manager_gui.py b/ui/icon_manager_gui.py new file mode 100644 index 00000000..bda6524a --- /dev/null +++ b/ui/icon_manager_gui.py @@ -0,0 +1,405 @@ +""" +图标管理器GUI工具 + +提供图形界面来管理和查看图标: +- 显示所有可用图标 +- 图标预览 +- 图标信息 +- 图标刷新 +""" +import os +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QListWidget, QListWidgetItem, QLabel, QGroupBox, + QTextEdit, QSplitter, QDialog, QDialogButtonBox, + QFileDialog, QMessageBox, QScrollArea, QGridLayout) +from PyQt5.QtGui import QIcon, QPixmap, QFont +from PyQt5.QtCore import Qt, QSize + +from ui.icon_manager import get_icon_manager + + +class IconPreviewWidget(QWidget): + """图标预览控件""" + + def __init__(self): + super().__init__() + self.setupUI() + + def setupUI(self): + """设置UI""" + layout = QVBoxLayout(self) + + # 图标显示区域 + self.icon_label = QLabel() + self.icon_label.setAlignment(Qt.AlignCenter) + self.icon_label.setStyleSheet(""" + QLabel { + border: 2px dashed #8b5cf6; + border-radius: 8px; + background-color: #2d2d44; + color: #e0e0ff; + min-height: 100px; + margin: 10px; + } + """) + self.icon_label.setText("选择图标查看预览") + layout.addWidget(self.icon_label) + + # 图标信息 + self.info_label = QLabel() + self.info_label.setStyleSheet(""" + QLabel { + background-color: #252538; + color: #e0e0ff; + padding: 10px; + border-radius: 4px; + font-family: monospace; + } + """) + self.info_label.setText("图标信息将在此显示") + layout.addWidget(self.info_label) + + def showIcon(self, icon_name: str, icon: QIcon): + """显示图标""" + if not icon.isNull(): + # 显示不同尺寸的图标 + sizes = [16, 24, 32, 48, 64] + pixmaps = [] + + # 创建组合图标显示 + for size in sizes: + pixmap = icon.pixmap(QSize(size, size)) + if not pixmap.isNull(): + pixmaps.append((size, pixmap)) + + if pixmaps: + # 创建合成图片显示多个尺寸 + total_width = sum(size for size, _ in pixmaps) + 20 * (len(pixmaps) - 1) + max_height = max(size for size, _ in pixmaps) + + combined_pixmap = QPixmap(total_width, max_height + 40) + combined_pixmap.fill(Qt.transparent) + + from PyQt5.QtGui import QPainter, QPen + painter = QPainter(combined_pixmap) + + x = 0 + for size, pixmap in pixmaps: + # 绘制图标 + y = (max_height - size) // 2 + painter.drawPixmap(x, y, pixmap) + + # 绘制尺寸标签 + painter.setPen(QPen(Qt.white)) + painter.drawText(x, max_height + 15, f"{size}x{size}") + + x += size + 20 + + painter.end() + + self.icon_label.setPixmap(combined_pixmap) + else: + self.icon_label.setText("无法加载图标") + else: + self.icon_label.setText("图标无效") + + # 更新信息 + info_text = f"图标名称: {icon_name}\n" + info_text += f"图标有效: {'是' if not icon.isNull() else '否'}\n" + + # 获取图标管理器信息 + icon_manager = get_icon_manager() + icon_path = icon_manager.get_icon_path(icon_name) + if icon_path: + info_text += f"文件路径: {icon_path}\n" + if os.path.exists(icon_path): + size = os.path.getsize(icon_path) + info_text += f"文件大小: {size} bytes\n" + + # 获取可用尺寸 + if not icon.isNull(): + available_sizes = icon.availableSizes() + if available_sizes: + sizes_text = ", ".join(f"{s.width()}x{s.height()}" for s in available_sizes) + info_text += f"可用尺寸: {sizes_text}\n" + + self.info_label.setText(info_text) + + +class IconManagerDialog(QDialog): + """图标管理器对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.icon_manager = get_icon_manager() + self.setupUI() + self.loadIcons() + + def setupUI(self): + """设置UI""" + self.setWindowTitle("图标管理器") + self.setModal(False) + self.resize(800, 600) + + # 设置样式 + self.setStyleSheet(""" + QDialog { + background-color: #1e1e2e; + color: #e0e0ff; + } + QListWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + alternate-background-color: #2d2d44; + } + QListWidget::item { + padding: 8px; + border-bottom: 1px solid #3a3a4a; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QGroupBox { + background-color: #252538; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; + } + QGroupBox::title { + subline-offset: -2px; + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + QTextEdit { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + font-family: monospace; + } + """) + + layout = QVBoxLayout(self) + + # 顶部按钮栏 + button_layout = QHBoxLayout() + + self.refresh_btn = QPushButton("刷新图标") + self.refresh_btn.clicked.connect(self.refreshIcons) + button_layout.addWidget(self.refresh_btn) + + self.add_icon_btn = QPushButton("添加图标") + self.add_icon_btn.clicked.connect(self.addIcon) + button_layout.addWidget(self.add_icon_btn) + + self.debug_btn = QPushButton("调试信息") + self.debug_btn.clicked.connect(self.showDebugInfo) + button_layout.addWidget(self.debug_btn) + + button_layout.addStretch() + layout.addLayout(button_layout) + + # 主分割器 + splitter = QSplitter(Qt.Horizontal) + + # 左侧:图标列表 + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + list_group = QGroupBox("可用图标") + list_layout = QVBoxLayout(list_group) + + self.icon_list = QListWidget() + self.icon_list.itemSelectionChanged.connect(self.onIconSelected) + list_layout.addWidget(self.icon_list) + + left_layout.addWidget(list_group) + splitter.addWidget(left_widget) + + # 右侧:图标预览 + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + preview_group = QGroupBox("图标预览") + preview_layout = QVBoxLayout(preview_group) + + self.preview_widget = IconPreviewWidget() + preview_layout.addWidget(self.preview_widget) + + right_layout.addWidget(preview_group) + splitter.addWidget(right_widget) + + # 设置分割器比例 + splitter.setSizes([300, 500]) + layout.addWidget(splitter) + + # 底部按钮 + button_box = QDialogButtonBox(QDialogButtonBox.Close) + button_box.rejected.connect(self.close) + layout.addWidget(button_box) + + def loadIcons(self): + """加载图标列表""" + self.icon_list.clear() + + available_icons = self.icon_manager.get_available_icons() + + for icon_name in available_icons: + item = QListWidgetItem() + + # 获取图标 + icon = self.icon_manager.get_icon(icon_name, QSize(24, 24)) + + # 设置图标和文本 + if not icon.isNull(): + item.setIcon(icon) + item.setText(f"🎨 {icon_name}") + else: + item.setText(f"❌ {icon_name}") + + item.setData(Qt.UserRole, icon_name) + self.icon_list.addItem(item) + + print(f"📊 加载了 {len(available_icons)} 个图标") + + def onIconSelected(self): + """当选择图标时""" + current_item = self.icon_list.currentItem() + if current_item: + icon_name = current_item.data(Qt.UserRole) + icon = self.icon_manager.get_icon(icon_name) + self.preview_widget.showIcon(icon_name, icon) + + def refreshIcons(self): + """刷新图标""" + print("🔄 刷新图标缓存...") + self.icon_manager.refresh_cache() + self.loadIcons() + QMessageBox.information(self, "完成", "图标缓存已刷新") + + def addIcon(self): + """添加新图标""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择图标文件", + "", + "图像文件 (*.png *.jpg *.jpeg *.svg *.ico);;所有文件 (*)" + ) + + if file_path: + # 获取文件名作为图标名称 + file_name = os.path.basename(file_path) + icon_name = os.path.splitext(file_name)[0] + + # 复制文件到图标目录 + import shutil + target_path = os.path.join(self.icon_manager.icon_directory, file_name) + + try: + shutil.copy2(file_path, target_path) + + # 添加到缓存 + success = self.icon_manager.add_icon(icon_name, target_path) + + if success: + QMessageBox.information(self, "成功", f"图标 '{icon_name}' 已添加") + self.loadIcons() + else: + QMessageBox.warning(self, "失败", "添加图标失败") + + except Exception as e: + QMessageBox.critical(self, "错误", f"复制文件失败:\n{str(e)}") + + def showDebugInfo(self): + """显示调试信息""" + debug_dialog = QDialog(self) + debug_dialog.setWindowTitle("图标管理器调试信息") + debug_dialog.resize(600, 400) + + layout = QVBoxLayout(debug_dialog) + + text_edit = QTextEdit() + text_edit.setFont(QFont("Consolas", 10)) + + # 获取调试信息 + info = self.icon_manager.get_cache_info() + debug_text = "图标管理器调试信息\n" + debug_text += "=" * 50 + "\n\n" + debug_text += f"图标目录: {info['icon_directory']}\n" + debug_text += f"目录存在: {os.path.exists(info['icon_directory'])}\n" + debug_text += f"缓存图标数: {info['cached_icons']}\n" + debug_text += f"可用图标数: {info['available_icons']}\n\n" + + debug_text += "已缓存的图标:\n" + for key in info['cache_keys']: + debug_text += f" - {key}\n" + + debug_text += "\n图标目录内容:\n" + if os.path.exists(info['icon_directory']): + for file_name in os.listdir(info['icon_directory']): + file_path = os.path.join(info['icon_directory'], file_name) + if os.path.isfile(file_path): + size = os.path.getsize(file_path) + debug_text += f" - {file_name} ({size} bytes)\n" + else: + debug_text += " 目录不存在\n" + + text_edit.setPlainText(debug_text) + layout.addWidget(text_edit) + + button_box = QDialogButtonBox(QDialogButtonBox.Close) + button_box.rejected.connect(debug_dialog.close) + layout.addWidget(button_box) + + debug_dialog.exec_() + + +def show_icon_manager(parent=None): + """显示图标管理器对话框""" + dialog = IconManagerDialog(parent) + dialog.show() + return dialog + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + import sys + + app = QApplication(sys.argv) + + # 设置全局样式 + app.setStyleSheet(""" + QApplication { + background-color: #1e1e2e; + color: #e0e0ff; + } + """) + + dialog = IconManagerDialog() + dialog.show() + + sys.exit(app.exec_()) \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index df48d9fa..9d8e7bc9 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -21,6 +21,7 @@ from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect from direct.showbase.ShowBaseGlobal import aspect2d from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget +from ui.icon_manager import get_icon_manager, get_icon try: from PyQt5.QtWebEngineWidgets import QWebEngineView WEB_ENGINE_AVAILABLE = True @@ -36,6 +37,11 @@ class MainWindow(QMainWindow): self.world = world self.world.main_window = self # 关键:让world对象能访问主窗口 + # 初始化图标管理器并打印调试信息 + self.icon_manager = get_icon_manager() + print("🔧 图标管理器初始化完成") + self.icon_manager.debug_info() + self.setStyleSheet(""" QMainWindow { background-color: #1e1e2e; @@ -332,7 +338,13 @@ class MainWindow(QMainWindow): def setupCenterWidget(self): """设置窗口基本属性""" self.setWindowTitle("引擎编辑器") - + # 使用图标管理器设置窗口图标 + app_icon = get_icon('app_logo') + if not app_icon.isNull(): + self.setWindowIcon(app_icon) + print("✅ 应用图标设置成功") + else: + print("⚠️ 应用图标设置失败,使用默认图标") # 使用自定义的 Panda3D 部件作为中央部件 self.pandaWidget = CustomPanda3DWidget(self.world) self.setCentralWidget(self.pandaWidget) @@ -432,9 +444,9 @@ class MainWindow(QMainWindow): # 选择工具 self.selectTool = QToolButton() - icon_path = self.get_icon_path("select_tool.png") - if icon_path and os.path.exists(icon_path): - self.selectTool.setIcon(QIcon(icon_path)) + select_icon = get_icon('select_tool', QSize(16, 16)) + if not select_icon.isNull(): + self.selectTool.setIcon(select_icon) else: self.selectTool.setText('选择') # 如果没有图标则显示文字 self.selectTool.setIconSize(QSize(16, 16)) @@ -446,12 +458,10 @@ class MainWindow(QMainWindow): # 移动工具 self.moveTool = QToolButton() - self.moveTool.setText("移动") icon_path = self.get_icon_path("move_tool.png") - # if icon_path and os.path.exists(icon_path): - # self.moveTool.setIcon(QIcon(icon_path)) - # else: - # self.moveTool.setText('移动') + if icon_path and os.path.exists(icon_path): + self.moveTool.setIcon(QIcon(icon_path)) + self.moveTool.setText("移动") self.moveTool.setIconSize(QSize(16, 16)) self.moveTool.setCheckable(True) self.moveTool.setToolTip("移动工具 (W)") @@ -461,12 +471,10 @@ class MainWindow(QMainWindow): # 旋转工具 self.rotateTool = QToolButton() + rotate_icon = get_icon('rotate_tool', QSize(16, 16)) + if not rotate_icon.isNull(): + self.rotateTool.setIcon(rotate_icon) self.rotateTool.setText("旋转") - icon_path = self.get_icon_path("rotate_tool.png") - # if icon_path and os.path.exists(icon_path): - # self.rotateTool.setIcon(QIcon(icon_path)) - # else: - # self.rotateTool.setText('旋转') self.rotateTool.setIconSize(QSize(16, 16)) self.rotateTool.setCheckable(True) self.rotateTool.setToolTip("旋转工具 (E)") @@ -476,12 +484,10 @@ class MainWindow(QMainWindow): # 缩放工具 self.scaleTool = QToolButton() + scale_icon = get_icon('scale_tool', QSize(16, 16)) + if not scale_icon.isNull(): + self.scaleTool.setIcon(scale_icon) self.scaleTool.setText("缩放") - icon_path = self.get_icon_path("scale_tool.png") - # if icon_path and os.path.exists(icon_path): - # self.scaleTool.setIcon(QIcon(icon_path)) - # else: - # self.scaleTool.setText('缩放') self.scaleTool.setIconSize(QSize(16, 16)) self.scaleTool.setCheckable(True) self.scaleTool.setToolTip("缩放工具 (R)") @@ -647,8 +653,12 @@ class MainWindow(QMainWindow): self.moveAction = self.toolsMenu.addAction('移动工具') self.rotateAction = self.toolsMenu.addAction('旋转工具') self.scaleAction = self.toolsMenu.addAction('缩放工具') + self.toolsMenu.addSeparator() self.sunsetAction = self.toolsMenu.addAction('光照编辑') self.pluginAction = self.toolsMenu.addAction('图形编辑') + # self.toolsMenu.addSeparator() + # self.iconManagerAction = self.toolsMenu.addAction('图标管理器') + # self.iconManagerAction.triggered.connect(self.onOpenIconManager) # 统一创建菜单 - 关键修改 self.createMenu = menubar.addMenu('创建') @@ -856,11 +866,21 @@ class MainWindow(QMainWindow): padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ border-bottom: 0px solid #3a3a4a; } - QDockWidget::close-button, QDockWidget::float-button { + QDockWidget::close-button { background-color: #8b5cf6; border: none; icon-size: 8px; /* 调整图标大小 */ border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #7c3aed; /* 悬停时显示较亮的背景 */ @@ -900,11 +920,21 @@ class MainWindow(QMainWindow): padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ border-bottom: 0px solid #3a3a4a; } - QDockWidget::close-button, QDockWidget::float-button { + QDockWidget::close-button { background-color: #8b5cf6; border: none; icon-size: 8px; /* 调整图标大小 */ border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #7c3aed; /* 悬停时显示较亮的背景 */ @@ -996,11 +1026,21 @@ class MainWindow(QMainWindow): padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ border-bottom: 0px solid #3a3a4a; } - QDockWidget::close-button, QDockWidget::float-button { + QDockWidget::close-button { background-color: #8b5cf6; border: none; icon-size: 8px; /* 调整图标大小 */ border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #7c3aed; /* 悬停时显示较亮的背景 */ @@ -1097,11 +1137,21 @@ class MainWindow(QMainWindow): padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ border-bottom: 0px solid #3a3a4a; } - QDockWidget::close-button, QDockWidget::float-button { + QDockWidget::close-button { background-color: #8b5cf6; border: none; icon-size: 8px; /* 调整图标大小 */ border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #7c3aed; /* 悬停时显示较亮的背景 */ @@ -1150,11 +1200,21 @@ class MainWindow(QMainWindow): padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ border-bottom: 0px solid #3a3a4a; } - QDockWidget::close-button, QDockWidget::float-button { + QDockWidget::close-button { background-color: #8b5cf6; border: none; icon-size: 8px; /* 调整图标大小 */ border-radius: 4px; /* 增加圆角 */ + right: 5px; + top: 2px; + } + QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + right: 25px; + top: 2px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: #7c3aed; /* 悬停时显示较亮的背景 */ @@ -2592,6 +2652,16 @@ class MainWindow(QMainWindow): except Exception as e: QMessageBox.critical(self, "错误", f"卸载脚本时出错: {str(e)}") + def onOpenIconManager(self): + """打开图标管理器""" + try: + from ui.icon_manager_gui import show_icon_manager + self.icon_manager_dialog = show_icon_manager(self) + print("🎨 图标管理器已打开") + except Exception as e: + print(f"❌ 打开图标管理器失败: {e}") + QMessageBox.warning(self, "错误", f"打开图标管理器失败:\n{str(e)}") + def closeEvent(self, event): """处理窗口关闭事件""" try: diff --git a/ui/property_panel.py b/ui/property_panel.py index 6b63abac..5b73d4ba 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -1213,6 +1213,9 @@ class PropertyPanelManager: self.transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(self.transform_group) + # 碰撞检测面板 + self._addCollisionPanel(model) + # 动画和太阳方位角面板 self._addAnimationPanel(model) self._addSunAzimuthPanel() @@ -8683,4 +8686,1085 @@ except Exception as e: if actor: actor.stop() actor.cleanup() - actor.removeNode() \ No newline at end of file + actor.removeNode() + + def _addCollisionPanel(self, model): + """添加碰撞检测面板""" + try: + # 创建碰撞检测组 + collision_group = QGroupBox("碰撞检测") + collision_layout = QGridLayout() + + # 检查模型是否已有碰撞 + has_collision = self._hasCollision(model) + + # 碰撞状态标签 + status_label = QLabel("状态:") + collision_layout.addWidget(status_label, 0, 0) + + # 状态文本(需要保存引用以便更新) + self.collision_status_text = QLabel("已启用" if has_collision else "未启用") + self.collision_status_text.setStyleSheet("color: green;" if has_collision else "color: red;") + collision_layout.addWidget(self.collision_status_text, 0, 1) + + # 形状选择标签(始终显示) + self.collision_shape_label = QLabel("碰撞形状:") + collision_layout.addWidget(self.collision_shape_label, 1, 0) + + # 形状选择下拉框(始终显示) + self.collision_shape_combo = QComboBox() + self.collision_shape_combo.addItems([ + "球形 (Sphere)", + "盒型 (Box)", + "胶囊体 (Capsule)", + "平面 (Plane)", + "自动选择 (Auto)" + ]) + collision_layout.addWidget(self.collision_shape_combo, 1, 1) + + # 保存布局引用,用于动态添加/移除控件 + self.collision_layout = collision_layout + self.collision_group = collision_group + + current_row = 2 # 下一行的索引 + + # 显示/隐藏切换按钮(只有有碰撞时才显示) + if has_collision: + # 检查碰撞的当前可见性 + is_collision_visible = self._isCollisionVisible(model) + + # 显示当前碰撞类型并设置为只读 + current_shape = self._getCurrentCollisionShape(model) + self._setComboToShape(current_shape) + self.collision_shape_combo.setEnabled(False) + + # 添加碰撞参数调整控件 + current_row = self._addCollisionParameterControls(model, collision_layout, current_row, current_shape) + + # 显示/隐藏切换按钮 + self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") + self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) + collision_layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 2) + current_row += 1 + + # 移除碰撞按钮 + self.collision_button = QPushButton("移除碰撞") + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + collision_layout.addWidget(self.collision_button, current_row, 0, 1, 2) + else: + # 如果没有碰撞,设置默认选择并允许编辑 + self.collision_shape_combo.setCurrentText("球形 (Sphere)") + self.collision_shape_combo.setEnabled(True) + + # 清理之前的参数控件 + self._clearCollisionParameterControls() + + # 隐藏显示/隐藏按钮 + if hasattr(self, 'collision_visibility_button'): + self.collision_visibility_button.setVisible(False) + + # 添加碰撞按钮 + self.collision_button = QPushButton("添加碰撞") + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + collision_layout.addWidget(self.collision_button, current_row, 0, 1, 2) + collision_group.setLayout(collision_layout) + self._propertyLayout.addWidget(collision_group) + + except Exception as e: + print(f"创建碰撞面板失败: {e}") + def _addCollisionParameterControls(self, model, layout, start_row, shape_type): + """添加碰撞参数调整控件""" + try: + current_row = start_row + + # 位置调整控件(所有类型都有) + pos_label = QLabel("位置偏移:") + layout.addWidget(pos_label, current_row, 0) + current_row += 1 + + # X, Y, Z 位置调整 + self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) + + layout.addWidget(QLabel("X:"), current_row, 0) + layout.addWidget(self.collision_pos_x, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Y:"), current_row, 0) + layout.addWidget(self.collision_pos_y, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Z:"), current_row, 0) + layout.addWidget(self.collision_pos_z, current_row, 1) + current_row += 1 + + # 连接位置变化信号 + self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) + self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) + self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) + + # 根据形状类型添加特定参数 + if shape_type == 'sphere': + current_row = self._addSphereParameters(model, layout, current_row) + elif shape_type == 'box': + current_row = self._addBoxParameters(model, layout, current_row) + elif shape_type == 'capsule': + current_row = self._addCapsuleParameters(model, layout, current_row) + elif shape_type == 'plane': + current_row = self._addPlaneParameters(model, layout, current_row) + + # 获取并设置当前参数值 + self._loadCurrentCollisionParameters(model, shape_type) + + return current_row + + except Exception as e: + print(f"添加碰撞参数控件失败: {e}") + return start_row + + def _createCollisionSpinBox(self, min_val, max_val, decimals=2): + """创建碰撞参数调整用的SpinBox""" + spinbox = QDoubleSpinBox() + spinbox.setRange(min_val, max_val) + spinbox.setDecimals(decimals) + spinbox.setSingleStep(0.1) + return spinbox + + def _addSphereParameters(self, model, layout, start_row): + """添加球形碰撞参数""" + current_row = start_row + + # 半径调整 + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + + self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) + layout.addWidget(self.collision_radius, current_row, 1) + current_row += 1 + + return current_row + + def _addBoxParameters(self, model, layout, start_row): + """添加盒型碰撞参数""" + current_row = start_row + + size_label = QLabel("尺寸:") + layout.addWidget(size_label, current_row, 0) + current_row += 1 + + # 宽度、长度、高度 + self.collision_width = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_length = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_height = self._createCollisionSpinBox(0.1, 100, 2) + + layout.addWidget(QLabel("宽度:"), current_row, 0) + layout.addWidget(self.collision_width, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("长度:"), current_row, 0) + layout.addWidget(self.collision_length, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("高度:"), current_row, 0) + layout.addWidget(self.collision_height, current_row, 1) + current_row += 1 + + # 连接信号 + self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) + self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) + self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) + + return current_row + + def _addCapsuleParameters(self, model, layout, start_row): + """添加胶囊体碰撞参数""" + current_row = start_row + + # 半径和高度 + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + + self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) + layout.addWidget(self.collision_capsule_radius, current_row, 1) + current_row += 1 + + height_label = QLabel("高度:") + layout.addWidget(height_label, current_row, 0) + + self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) + layout.addWidget(self.collision_capsule_height, current_row, 1) + current_row += 1 + + return current_row + + def _addPlaneParameters(self, model, layout, start_row): + """添加平面碰撞参数""" + current_row = start_row + + # 法向量 + normal_label = QLabel("法向量:") + layout.addWidget(normal_label, current_row, 0) + current_row += 1 + + self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) + + layout.addWidget(QLabel("Nx:"), current_row, 0) + layout.addWidget(self.collision_normal_x, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Ny:"), current_row, 0) + layout.addWidget(self.collision_normal_y, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Nz:"), current_row, 0) + layout.addWidget(self.collision_normal_z, current_row, 1) + current_row += 1 + + # 连接信号 + self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) + self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) + self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) + + return current_row + def _hasCollision(self, model): + """检查模型是否已有碰撞体""" + try: + from panda3d.core import CollisionNode + + # 检查模型及其子节点是否有碰撞节点 + collision_nodes = model.findAllMatches("**/+CollisionNode") + has_collision = collision_nodes.getNumPaths() > 0 + + print(f"碰撞检查:模型 {model.getName()} - {'有' if has_collision else '无'}碰撞 (找到{collision_nodes.getNumPaths()}个碰撞节点)") + + return has_collision + except Exception as e: + print(f"检查碰撞失败: {e}") + return False + + def _isCollisionVisible(self, model): + """检查碰撞是否可见""" + try: + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + # 检查碰撞节点是否隐藏 + return not collision_np.isHidden() + return False + except Exception as e: + print(f"检查碰撞可见性失败: {e}") + return False + + def _toggleCollisionVisibility(self, model): + """切换碰撞可见性""" + try: + collision_nodes = model.findAllMatches("**/+CollisionNode") + is_visible = self._isCollisionVisible(model) + + for collision_np in collision_nodes: + if is_visible: + collision_np.hide() + print(f"隐藏碰撞:{model.getName()}") + else: + collision_np.show() + print(f"显示碰撞:{model.getName()}") + + # 立即更新按钮状态 + self._updateCollisionVisibilityButton(model) + + except Exception as e: + print(f"切换碰撞可见性失败: {e}") + + def _updateCollisionVisibilityButton(self, model): + """更新碰撞可见性按钮状态""" + try: + if hasattr(self, 'collision_visibility_button'): + is_visible = self._isCollisionVisible(model) + self.collision_visibility_button.setText("隐藏碰撞" if is_visible else "显示碰撞") + print(f"更新可见性按钮:{model.getName()} - {'可见' if is_visible else '隐藏'}") + except Exception as e: + print(f"更新碰撞可见性按钮失败: {e}") + + def _getCurrentCollisionShape(self, model): + """获取当前模型的碰撞形状类型""" + try: + from panda3d.core import CollisionNode, CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane + + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_node = collision_np.node() + if collision_node.getNumSolids() > 0: + solid = collision_node.getSolid(0) + solid_type = type(solid).__name__ + + if solid_type == "CollisionSphere": + return "sphere" + elif solid_type == "CollisionBox": + return "box" + elif solid_type == "CollisionCapsule": + return "capsule" + elif solid_type == "CollisionPlane": + return "plane" + + return "sphere" # 默认返回球形 + except Exception as e: + print(f"获取碰撞形状失败: {e}") + return "sphere" + + def _setComboToShape(self, shape_type): + """根据形状类型设置下拉框选择""" + shape_map = { + "sphere": "球形 (Sphere)", + "box": "盒型 (Box)", + "capsule": "胶囊体 (Capsule)", + "plane": "平面 (Plane)", + "auto": "自动选择 (Auto)" + } + + if shape_type in shape_map: + self.collision_shape_combo.setCurrentText(shape_map[shape_type]) + else: + self.collision_shape_combo.setCurrentText("球形 (Sphere)") + + def _getSelectedCollisionShape(self): + """获取选中的碰撞形状类型""" + if hasattr(self, 'collision_shape_combo'): + shape_text = self.collision_shape_combo.currentText() + # 从显示文本中提取形状类型 + if "球形" in shape_text: + return 'sphere' + elif "盒型" in shape_text: + return 'box' + elif "胶囊体" in shape_text: + return 'capsule' + elif "平面" in shape_text: + return 'plane' + elif "自动选择" in shape_text: + return 'auto' + return 'sphere' # 默认返回球形 + + def _addCollisionAndUpdate(self, model): + """添加指定形状的碰撞体并更新界面""" + try: + # 防止重复调用 + if getattr(self, '_adding_collision', False): + print("正在添加碰撞,跳过重复调用") + return + + self._adding_collision = True + + if hasattr(self.world, 'scene_manager'): + # 获取选中的碰撞形状 + shape_type = self._getSelectedCollisionShape() + + # 参考scene_manager的setupCollision方法 + from panda3d.core import CollisionNode, BitMask32 + + # 创建碰撞节点 + cNode = CollisionNode(f'modelCollision_{model.getName()}') + + # 设置碰撞掩码 + cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 + + # 如果启用了模型间碰撞检测,添加额外的掩码 + if (hasattr(self.world, 'collision_manager') and + hasattr(self.world.collision_manager, 'model_collision_enabled') and + self.world.collision_manager.model_collision_enabled): + current_mask = cNode.getIntoCollideMask() + model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION + cNode.setIntoCollideMask(current_mask | model_collision_mask) + + # 创建指定形状的碰撞体 + if hasattr(self.world, 'collision_manager'): + collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type) + else: + # 回退方案:创建简单球体 + from panda3d.core import CollisionSphere, Point3 + bounds = model.getBounds() + if bounds.isEmpty(): + collision_shape = CollisionSphere(Point3(0, 0, 0), 1.0) + else: + center = bounds.getCenter() + radius = bounds.getRadius() + if radius <= 0: + radius = 1.0 + collision_shape = CollisionSphere(center, radius) + + cNode.addSolid(collision_shape) + + # 将碰撞节点附加到模型上 + cNodePath = model.attachNewNode(cNode) + + # 根据调试设置决定是否显示碰撞体 + if hasattr(self.world, 'debug_collision') and self.world.debug_collision: + cNodePath.show() + else: + cNodePath.hide() + + # 为模型添加碰撞相关标签 + model.setTag("has_collision", "true") + model.setTag("collision_shape", shape_type) + if 'radius' in locals(): + model.setTag("collision_radius", str(radius)) + + print(f"✅ 为模型 {model.getName()} 添加了 {shape_type} 碰撞体") + + # 简单更新按钮状态,不调用完整的状态更新 + if hasattr(self, 'collision_button'): + self.collision_button.setText("移除碰撞") + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + + if hasattr(self, 'collision_status_text'): + self.collision_status_text.setText("已启用") + self.collision_status_text.setStyleSheet("color: green;") + + if hasattr(self, 'collision_shape_combo'): + self.collision_shape_combo.setEnabled(False) + + else: + print("场景管理器未初始化") + + except Exception as e: + print(f"添加碰撞失败: {e}") + import traceback + traceback.print_exc() + finally: + # 确保标志被重置 + self._adding_collision = False + + def _removeCollisionAndUpdate(self, model): + """移除模型的碰撞体并更新界面""" + try: + # 查找并移除碰撞节点 + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_np.removeNode() + + print(f"移除了模型 {model.getName()} 的碰撞体") + + # 重置状态并更新界面 + self._previous_collision_state = True # 强制刷新 + self._updateCollisionPanelState(model) + + except Exception as e: + print(f"移除碰撞失败: {e}") + + def _updateCollisionPanelState(self, model): + """更新碰撞面板状态""" + try: + # 防止重复调用 + if getattr(self, '_updating_collision_panel', False): + return + + self._updating_collision_panel = True + + if hasattr(self, 'collision_button') and hasattr(self, 'collision_status_text') and hasattr(self, 'collision_shape_combo'): + has_collision = self._hasCollision(model) + + # 更新状态文本和颜色 + self.collision_status_text.setText("已启用" if has_collision else "未启用") + self.collision_status_text.setStyleSheet("color: green;" if has_collision else "color: red;") + + if has_collision: + # 有碰撞:显示移除按钮,下拉框变为只读并显示当前类型 + self.collision_button.setText("移除碰撞") + + # 先断开所有连接,再重新连接 + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + + # 获取并显示当前碰撞类型,设置为只读 + current_shape = self._getCurrentCollisionShape(model) + self._setComboToShape(current_shape) + self.collision_shape_combo.setEnabled(False) + + # 确保参数控件存在 - 只在没有参数控件时才添加 + if not hasattr(self, 'collision_pos_x'): + print("添加碰撞参数控件") + self._addParameterControlsToExistingPanel(model, current_shape) + + # 显示/隐藏按钮状态更新 + if not hasattr(self, 'collision_visibility_button'): + # 创建可见性按钮 + self._addVisibilityButtonToExistingPanel(model) + else: + self.collision_visibility_button.setVisible(True) + self._updateCollisionVisibilityButton(model) + + print(f"碰撞面板状态更新:有碰撞 - {current_shape}") + + else: + # 无碰撞:显示添加按钮,下拉框变为可编辑 + self.collision_button.setText("添加碰撞") + + # 先断开所有连接,再重新连接 + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + + # 恢复为可编辑状态 + self.collision_shape_combo.setEnabled(True) + + # 隐藏并清理参数控件 - 只在有参数控件时才清理 + if hasattr(self, 'collision_pos_x'): + print("清理碰撞参数控件") + self._hideCollisionParameterControls() + + # 隐藏显示/隐藏按钮 + if hasattr(self, 'collision_visibility_button'): + self.collision_visibility_button.setVisible(False) + + print(f"碰撞面板状态更新:无碰撞 - 可编辑") + + except Exception as e: + print(f"更新碰撞面板状态失败: {e}") + import traceback + traceback.print_exc() + finally: + # 确保标志被重置 + self._updating_collision_panel = False + + def _loadCurrentCollisionParameters(self, model, shape_type): + """加载当前碰撞参数到界面""" + try: + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_node = collision_np.node() + if collision_node.getNumSolids() > 0: + solid = collision_node.getSolid(0) + + # 获取碰撞节点的位置 + pos = collision_np.getPos() + self.collision_pos_x.setValue(pos.x) + self.collision_pos_y.setValue(pos.y) + self.collision_pos_z.setValue(pos.z) + + if shape_type == 'sphere': + self._loadSphereParameters(solid) + elif shape_type == 'box': + self._loadBoxParameters(solid) + elif shape_type == 'capsule': + self._loadCapsuleParameters(solid) + elif shape_type == 'plane': + self._loadPlaneParameters(solid) + break + + except Exception as e: + print(f"加载碰撞参数失败: {e}") + + def _loadSphereParameters(self, solid): + """加载球形参数""" + try: + from panda3d.core import CollisionSphere + if isinstance(solid, CollisionSphere): + radius = solid.getRadius() + self.collision_radius.setValue(radius) + except Exception as e: + print(f"加载球形参数失败: {e}") + + def _loadBoxParameters(self, solid): + """加载盒型参数""" + try: + from panda3d.core import CollisionBox + if isinstance(solid, CollisionBox): + min_point = solid.getMin() + max_point = solid.getMax() + width = max_point.x - min_point.x + length = max_point.y - min_point.y + height = max_point.z - min_point.z + + self.collision_width.setValue(width) + self.collision_length.setValue(length) + self.collision_height.setValue(height) + except Exception as e: + print(f"加载盒型参数失败: {e}") + + def _loadCapsuleParameters(self, solid): + """加载胶囊体参数""" + try: + from panda3d.core import CollisionCapsule + if isinstance(solid, CollisionCapsule): + radius = solid.getRadius() + point_a = solid.getPointA() + point_b = solid.getPointB() + height = (point_b - point_a).length() + + self.collision_capsule_radius.setValue(radius) + self.collision_capsule_height.setValue(height) + except Exception as e: + print(f"加载胶囊体参数失败: {e}") + + def _loadPlaneParameters(self, solid): + """加载平面参数""" + try: + from panda3d.core import CollisionPlane + if isinstance(solid, CollisionPlane): + plane = solid.getPlane() + normal = plane.getNormal() + + self.collision_normal_x.setValue(normal.x) + self.collision_normal_y.setValue(normal.y) + self.collision_normal_z.setValue(normal.z) + except Exception as e: + print(f"加载平面参数失败: {e}") + + def _updateCollisionPosition(self, model, axis, value): + """更新碰撞位置""" + try: + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + current_pos = collision_np.getPos() + if axis == 'x': + collision_np.setPos(value, current_pos.y, current_pos.z) + elif axis == 'y': + collision_np.setPos(current_pos.x, value, current_pos.z) + elif axis == 'z': + collision_np.setPos(current_pos.x, current_pos.y, value) + print(f"更新碰撞位置 {axis}: {value}") + break + except Exception as e: + print(f"更新碰撞位置失败: {e}") + + def _updateSphereRadius(self, model, radius): + """更新球形半径""" + try: + self._recreateCollisionShape(model, 'sphere', radius=radius) + print(f"更新球形半径: {radius}") + except Exception as e: + print(f"更新球形半径失败: {e}") + + def _updateBoxSize(self, model, dimension, value): + """更新盒型尺寸""" + try: + # 获取当前所有尺寸 + width = self.collision_width.value() if hasattr(self, 'collision_width') else value + length = self.collision_length.value() if hasattr(self, 'collision_length') else value + height = self.collision_height.value() if hasattr(self, 'collision_height') else value + + self._recreateCollisionShape(model, 'box', width=width, length=length, height=height) + print(f"更新盒型{dimension}: {value}") + except Exception as e: + print(f"更新盒型尺寸失败: {e}") + + def _updateCapsuleRadius(self, model, radius): + """更新胶囊体半径""" + try: + height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 + self._recreateCollisionShape(model, 'capsule', radius=radius, height=height) + print(f"更新胶囊体半径: {radius}") + except Exception as e: + print(f"更新胶囊体半径失败: {e}") + + def _updateCapsuleHeight(self, model, height): + """更新胶囊体高度""" + try: + radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 + self._recreateCollisionShape(model, 'capsule', radius=radius, height=height) + print(f"更新胶囊体高度: {height}") + except Exception as e: + print(f"更新胶囊体高度失败: {e}") + + def _updatePlaneNormal(self, model, axis, value): + """更新平面法向量""" + try: + # 获取当前法向量 + normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 + normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 + normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 + + self._recreateCollisionShape(model, 'plane', normal=(normal_x, normal_y, normal_z)) + print(f"更新平面法向量 {axis}: {value}") + except Exception as e: + print(f"更新平面法向量失败: {e}") + + def _recreateCollisionShape(self, model, shape_type, **kwargs): + """重新创建碰撞形状(保持位置和可见性)""" + try: + # 保存当前状态 + collision_nodes = model.findAllMatches("**/+CollisionNode") + if not collision_nodes: + return + + collision_np = collision_nodes[0] + current_pos = collision_np.getPos() + is_visible = not collision_np.isHidden() + + # 移除旧的碰撞体 + collision_np.removeNode() + + # 创建新的碰撞体 + from panda3d.core import CollisionNode, BitMask32 + cNode = CollisionNode(f'modelCollision_{model.getName()}') + cNode.setIntoCollideMask(BitMask32.bit(2)) + + # 创建新形状 + if hasattr(self.world, 'collision_manager'): + collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type, **kwargs) + else: + # 回退方案 + from panda3d.core import CollisionSphere, Point3 + collision_shape = CollisionSphere(Point3(0, 0, 0), kwargs.get('radius', 1.0)) + + cNode.addSolid(collision_shape) + + # 重新附加并恢复状态 + new_collision_np = model.attachNewNode(cNode) + new_collision_np.setPos(current_pos) + + if is_visible: + new_collision_np.show() + else: + new_collision_np.hide() + + except Exception as e: + print(f"重新创建碰撞形状失败: {e}") + import traceback + traceback.print_exc() + + def _clearCollisionParameterControls(self): + """清理碰撞参数控件""" + try: + # 位置控件 + for attr in ['collision_pos_x', 'collision_pos_y', 'collision_pos_z']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 球形参数控件 + if hasattr(self, 'collision_radius'): + if self.collision_radius and self.collision_radius.parent(): + self.collision_radius.setParent(None) + self.collision_radius.deleteLater() + delattr(self, 'collision_radius') + + # 盒型参数控件 + for attr in ['collision_width', 'collision_length', 'collision_height']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 胶囊体参数控件 + for attr in ['collision_capsule_radius', 'collision_capsule_height']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 平面参数控件 + for attr in ['collision_normal_x', 'collision_normal_y', 'collision_normal_z']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + print("清理碰撞参数控件完成") + + except Exception as e: + print(f"清理碰撞参数控件失败: {e}") + + def _refreshCollisionPanel(self, model): + """刷新整个碰撞面板(简化版)""" + try: + print("使用简化的面板刷新") + # 直接调用状态更新,不删除面板 + self._updateCollisionPanelState(model) + + except Exception as e: + print(f"刷新碰撞面板失败: {e}") + import traceback + traceback.print_exc() + + def _addParameterControlsToExistingPanel(self, model, shape_type): + """向现有面板添加参数控件""" + try: + if not hasattr(self, 'collision_layout'): + return + + layout = self.collision_layout + + # 首先清理可能存在的旧控件 + self._hideCollisionParameterControls() + + # 找到插入位置(在按钮之前) + current_row = 2 + + # 位置调整控件 + pos_label = QLabel("位置偏移:") + pos_label.setVisible(True) # 确保可见 + layout.addWidget(pos_label, current_row, 0) + current_row += 1 + + # X, Y, Z 位置调整 + self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) + + x_label = QLabel("X:") + x_label.setVisible(True) + layout.addWidget(x_label, current_row, 0) + self.collision_pos_x.setVisible(True) + layout.addWidget(self.collision_pos_x, current_row, 1) + current_row += 1 + + y_label = QLabel("Y:") + y_label.setVisible(True) + layout.addWidget(y_label, current_row, 0) + self.collision_pos_y.setVisible(True) + layout.addWidget(self.collision_pos_y, current_row, 1) + current_row += 1 + + z_label = QLabel("Z:") + z_label.setVisible(True) + layout.addWidget(z_label, current_row, 0) + self.collision_pos_z.setVisible(True) + layout.addWidget(self.collision_pos_z, current_row, 1) + current_row += 1 + + # 连接位置变化信号 + self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) + self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) + self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) + + # 根据形状类型添加特定参数 + if shape_type == 'sphere': + current_row = self._addSphereParametersToExisting(model, layout, current_row) + elif shape_type == 'box': + current_row = self._addBoxParametersToExisting(model, layout, current_row) + elif shape_type == 'capsule': + current_row = self._addCapsuleParametersToExisting(model, layout, current_row) + elif shape_type == 'plane': + current_row = self._addPlaneParametersToExisting(model, layout, current_row) + + # 重新定位按钮 + self._repositionButtons(current_row) + + # 加载参数值 + self._loadCurrentCollisionParameters(model, shape_type) + + except Exception as e: + print(f"添加参数控件到现有面板失败: {e}") + import traceback + traceback.print_exc() + + def _addSphereParametersToExisting(self, model, layout, start_row): + """向现有面板添加球形参数""" + current_row = start_row + + radius_label = QLabel("半径:") + radius_label.setVisible(True) + layout.addWidget(radius_label, current_row, 0) + + self.collision_radius = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_radius.setVisible(True) + self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) + layout.addWidget(self.collision_radius, current_row, 1) + current_row += 1 + + return current_row + + def _addBoxParametersToExisting(self, model, layout, start_row): + """向现有面板添加盒型参数""" + current_row = start_row + + size_label = QLabel("尺寸:") + size_label.setVisible(True) + layout.addWidget(size_label, current_row, 0) + current_row += 1 + + self.collision_width = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_length = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_height = self._createCollisionSpinBox(0.1, 100, 2) + + width_label = QLabel("宽度:") + width_label.setVisible(True) + layout.addWidget(width_label, current_row, 0) + self.collision_width.setVisible(True) + layout.addWidget(self.collision_width, current_row, 1) + current_row += 1 + + length_label = QLabel("长度:") + length_label.setVisible(True) + layout.addWidget(length_label, current_row, 0) + self.collision_length.setVisible(True) + layout.addWidget(self.collision_length, current_row, 1) + current_row += 1 + + height_label = QLabel("高度:") + height_label.setVisible(True) + layout.addWidget(height_label, current_row, 0) + self.collision_height.setVisible(True) + layout.addWidget(self.collision_height, current_row, 1) + current_row += 1 + + # 连接信号 + self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) + self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) + self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) + + return current_row + + def _addCapsuleParametersToExisting(self, model, layout, start_row): + """向现有面板添加胶囊体参数""" + current_row = start_row + + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + + self.collision_capsule_radius = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) + layout.addWidget(self.collision_capsule_radius, current_row, 1) + current_row += 1 + + height_label = QLabel("高度:") + layout.addWidget(height_label, current_row, 0) + + self.collision_capsule_height = self._createCollisionSpinBox(0.1, 100, 2) + self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) + layout.addWidget(self.collision_capsule_height, current_row, 1) + current_row += 1 + + return current_row + + def _addPlaneParametersToExisting(self, model, layout, start_row): + """向现有面板添加平面参数""" + current_row = start_row + + normal_label = QLabel("法向量:") + layout.addWidget(normal_label, current_row, 0) + current_row += 1 + + self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) + + layout.addWidget(QLabel("Nx:"), current_row, 0) + layout.addWidget(self.collision_normal_x, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Ny:"), current_row, 0) + layout.addWidget(self.collision_normal_y, current_row, 1) + current_row += 1 + + layout.addWidget(QLabel("Nz:"), current_row, 0) + layout.addWidget(self.collision_normal_z, current_row, 1) + current_row += 1 + + # 连接信号 + self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) + self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) + self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) + + return current_row + + def _addVisibilityButtonToExistingPanel(self, model): + """向现有面板添加可见性按钮""" + try: + if hasattr(self, 'collision_layout'): + layout = self.collision_layout + is_collision_visible = self._isCollisionVisible(model) + + # 找到合适的行位置 + current_row = layout.rowCount() + + self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") + self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) + layout.addWidget(self.collision_visibility_button, current_row - 1, 0, 1, 2) + + except Exception as e: + print(f"添加可见性按钮失败: {e}") + + def _repositionButtons(self, new_row): + """重新定位按钮位置""" + try: + if hasattr(self, 'collision_layout'): + layout = self.collision_layout + + # 移动可见性按钮 + if hasattr(self, 'collision_visibility_button'): + layout.addWidget(self.collision_visibility_button, new_row, 0, 1, 2) + new_row += 1 + + # 移动主按钮 + if hasattr(self, 'collision_button'): + layout.addWidget(self.collision_button, new_row, 0, 1, 2) + + except Exception as e: + print(f"重新定位按钮失败: {e}") + + def _hideCollisionParameterControls(self): + """隐藏碰撞参数控件(保留按钮)""" + try: + # 清理属性引用,但保留按钮 + param_attrs = [ + 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', + 'collision_radius', + 'collision_width', 'collision_length', 'collision_height', + 'collision_capsule_radius', 'collision_capsule_height', + 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' + ] + + # 隐藏并删除参数控件 + for attr in param_attrs: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget: + widget.setVisible(False) + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 同时清理可能的标签控件 + if hasattr(self, 'collision_layout'): + layout = self.collision_layout + + # 收集需要移除的控件(不包括基本控件和按钮) + widgets_to_remove = [] + + for i in range(layout.rowCount()): + if i >= 2: # 从第3行开始检查 + for j in range(layout.columnCount()): + item = layout.itemAtPosition(i, j) + if item: + widget = item.widget() + if widget and hasattr(widget, 'text'): + # 检查是否是参数相关的标签 + text = widget.text() + if any(keyword in text for keyword in ['位置偏移', 'X:', 'Y:', 'Z:', '半径:', '尺寸:', '宽度:', '长度:', '高度:', '法向量:', 'Nx:', 'Ny:', 'Nz:']): + widgets_to_remove.append(widget) + + # 移除参数标签 + for widget in widgets_to_remove: + widget.setVisible(False) + widget.setParent(None) + widget.deleteLater() + + print("隐藏碰撞参数控件完成(保留按钮)") + + except Exception as e: + print(f"隐藏碰撞参数控件失败: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/ui/widgets.py b/ui/widgets.py index 5ab3f6fc..e6ef58f6 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -2214,7 +2214,6 @@ class CustomTreeWidget(QTreeWidget): if not item: print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") return - try: # 2. 过滤受保护节点 node_type = item.data(0, Qt.UserRole + 1) @@ -2252,6 +2251,24 @@ class CustomTreeWidget(QTreeWidget): import traceback traceback.print_exc() + def clear_tree(self): + """清空UI树""" + print("Clear") + self.clear() + # 创建场景根节点 + sceneRoot = QTreeWidgetItem(self, ['场景']) + sceneRoot.setData(0, Qt.UserRole, self.world.render) + sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") + # 添加相机节点 + cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) + cameraItem.setData(0, Qt.UserRole, self.world.cam) + cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") + # 添加地板节点 + if hasattr(self.world, 'ground') and self.world.ground: + groundItem = QTreeWidgetItem(sceneRoot, ['地板']) + groundItem.setData(0, Qt.UserRole, self.world.ground) + groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") + def _cleanup_panda_node_resources(self, panda_node): """一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。""" if not panda_node or panda_node.is_empty():