From d3c66651630c39f71c875b59e80c061460252997 Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Wed, 17 Sep 2025 15:42:42 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=9A=E7=84=A6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/collision_manager.py | 2 +- core/event_handler.py | 124 +----- core/patrol_system.py | 495 +++++++++++++++++++++++ core/script_system.py | 30 ++ core/selection.py | 819 ++++++++++++++++++++++++++++++++++---- core/world.py | 109 ++++- demo/test_gizmo_drag.py | 2 +- main.py | 89 ++++- scene/scene_manager.py | 287 ++----------- scene/util.py | 10 +- ui/interface_manager.py | 38 +- ui/main_window.py | 80 ++-- ui/property_panel.py | 38 +- ui/widgets.py | 9 +- 14 files changed, 1625 insertions(+), 507 deletions(-) create mode 100644 core/patrol_system.py diff --git a/core/collision_manager.py b/core/collision_manager.py index 041571bf..fb247f55 100644 --- a/core/collision_manager.py +++ b/core/collision_manager.py @@ -364,7 +364,7 @@ class CollisionManager: collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices]) return collision_poly else: - print("⚠️ 多边形至少需要3个顶点,回退到球体") + #print("⚠️ 多边形至少需要3个顶点,回退到球体") return CollisionSphere(center, radius) def _determineOptimalShape(self, model, bounds): diff --git a/core/event_handler.py b/core/event_handler.py index ff7b17c2..91904e57 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -167,7 +167,7 @@ class EventHandler: picker.addCollider(pickerNP, queue) picker.traverse(self.world.render) - print(f"碰撞检测结果数量: {queue.getNumEntries()}") + #print(f"碰撞检测结果数量: {queue.getNumEntries()}") # 射线检测结果处理 hitPos = None @@ -184,13 +184,13 @@ class EventHandler: self.showClickRay(worldNearPoint, worldFarPoint, hitPos) # 优先检查是否点击了坐标轴 - print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") + #print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") if self.world.selection.gizmo: - print("准备检查坐标轴点击...") + #print("准备检查坐标轴点击...") try: gizmoAxis = self.world.selection.checkGizmoClick(x, y) if gizmoAxis: - print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") # 开始坐标轴拖拽 self.world.selection.startGizmoDrag(gizmoAxis, x, y) pickerNP.removeNode() @@ -203,16 +203,16 @@ class EventHandler: traceback.print_exc() print("继续处理模型选择...") - print("继续处理碰撞结果...") + #print("继续处理碰撞结果...") if hitPos and hitNode: - print(f"✓ 检测到碰撞,开始处理点击事件") - print(f"GUI编辑模式: {self.world.guiEditMode}") - print(f"当前工具: {self.world.currentTool}") + #print(f"✓ 检测到碰撞,开始处理点击事件") + #print(f"GUI编辑模式: {self.world.guiEditMode}") + #print(f"当前工具: {self.world.currentTool}") # 处理GUI编辑模式 if self.world.guiEditMode: - print("处理GUI编辑模式点击") + #print("处理GUI编辑模式点击") # 检查是否点击了GUI元素 clickedGUI = self.world.gui_manager.findClickedGUI(hitNode) if clickedGUI: @@ -411,103 +411,6 @@ class EventHandler: import traceback traceback.print_exc() - - def mousePressEventRight(self,evt): - """处理鼠标右键按下事件""" - print(f"当前工具: {self.world.currentTool}") - - # 检查是否是地形编辑模式 - if self.world.currentTool == "地形编辑": - self._handleTerrainEdit(evt, "subtract") # 降低地形 - return - - # 其他右键处理逻辑可以在这里添加 - print("鼠标右键事件处理") - - def _handleTerrainEdit(self,evt,operation): - try: - x = evt.get('x',0) - y = evt.get('y',0) - - winWidth,winHeight = self.world.getWindowSize() - - mx = 2.0 * x/float(winWidth) - 1.0 - my = 1.0 -2.0*y/float(winHeight) - - nearPoint = Point3() - farPoint = Point3() - self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) - - worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) - worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) - - picker = CollisionTraverser() - queue = CollisionHandlerQueue() - - pickerNode = CollisionNode('terrain_edit_ray') - pickerNP = self.world.cam.attachNewNode(pickerNode) - - from panda3d.core import BitMask32 - pickerNode.setFromCollideMask(BitMask32.allOn()) # 检查所有碰撞 - - # 使用相机坐标系的点创建射线 - direction = farPoint - nearPoint - direction.normalize() - pickerNode.addSolid(CollisionRay(nearPoint, direction)) - - picker.addCollider(pickerNP, queue) - picker.traverse(self.world.render) - print(f"地形碰撞检测结果数量: {queue.getNumEntries()}") - - # 射线检测结果处理 - hitPos = None - hitNode = None - - if queue.getNumEntries() > 0: - # 遍历所有碰撞结果,找到地形节点 - for i in range(queue.getNumEntries()): - entry = queue.getEntry(i) - collided_node = entry.getIntoNodePath() - print(f"碰撞到节点: {collided_node.getName()}") - - # 检查是否是地形节点 - for terrain_info in self.world.terrain_manager.terrains: - terrain_node = terrain_info['node'] - if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node): - hitPos = entry.getSurfacePoint(self.world.render) - hitNode = collided_node - print(f"找到地形节点: {terrain_node.getName()}") - - # 修改地形高度 - x_pos, y_pos = hitPos.getX(), hitPos.getY() - success = self.world.modifyTerrainHeight( - terrain_info, x_pos, y_pos, radius=3.0, strength=0.3, operation=operation) - - if success: - print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})") - # 显示射线 - self.showClickRay(worldNearPoint, worldFarPoint, hitPos) - else: - print("✗ 地形编辑失败") - break - - if hitPos: - break - - if not hitPos: - print("没有检测到地形碰撞") - # 显示射线(无碰撞) - self.showClickRay(worldNearPoint, worldFarPoint) - - # 清理碰撞检测节点 - pickerNP.removeNode() - print("地形编辑处理完成") - - except Exception as e: - print(f"地形编辑处理出错: {e}") - import traceback - traceback.print_exc() - def _handleSelectionClick(self, hitNode): """处理选择工具的点击事件""" print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") @@ -541,28 +444,28 @@ class EventHandler: selectedModel = model print(f"找到父模型: {selectedModel.getName()}") break - + if selectedModel: break current = current.getParent() if selectedModel: - print(f"✓ 最终选中模型: {selectedModel.getName()}") + #print(f"✓ 最终选中模型: {selectedModel.getName()}") # 更新选择状态并显示选择框和坐标轴 self.world.selection.updateSelection(selectedModel) # 在树形控件中查找并选中对应的项 if self.world.interface_manager.treeWidget: - print("查找树形控件中的对应项...") + #print("查找树形控件中的对应项...") root = self.world.interface_manager.treeWidget.invisibleRootItem() foundItem = None for i in range(root.childCount()): sceneItem = root.child(i) if sceneItem.text(0) == "场景": - print(f"在场景节点下查找...") + #print(f"在场景节点下查找...") foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem) if foundItem: print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}") @@ -578,6 +481,7 @@ class EventHandler: print("× 树形控件不存在") else: print("× 没有找到可选择的模型节点") + self.world.selection.updateSelection(None) def mouseReleaseEventLeft(self, evt): """处理鼠标左键释放事件""" diff --git a/core/patrol_system.py b/core/patrol_system.py new file mode 100644 index 00000000..ab951b47 --- /dev/null +++ b/core/patrol_system.py @@ -0,0 +1,495 @@ +from direct.showbase.ShowBaseGlobal import globalClock +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import Point3, Vec3 +import math + + +class PatrolSystem: + """巡检系统类""" + + def __init__(self, world): + """初始化巡检系统 + + Args: + world: 核心世界对象引用 + """ + self.world = world + + # 巡检状态 + self.is_patrolling = False + self.patrol_points = [] # 巡检点列表 [(pos, hpr, wait_time), ...] + self.current_patrol_index = 0 + self.patrol_task = None + + # 巡检参数 + self.patrol_speed = 5.0 # 巡检移动速度(单位/秒) + self.patrol_turn_speed = 90.0 # 转向速度(度/秒) + self.patrol_wait_timer = 0.0 + self.patrol_state = "moving" # "moving", "turning_to_target", "waiting", "turning_back" + + # 相机状态保存 + self.original_cam_pos = None + self.original_cam_hpr = None + + print("✓ 巡检系统初始化完成") + + def add_patrol_point(self, position, heading=None, wait_time=3.0): + if heading is None: + if self.patrol_points: + last_pos = self.patrol_points[-1][0] + direction_x = position[0] - last_pos.x + direction_y = position[1] - last_pos.y + direction_z = position[2] - last_pos.z + + import math + h=math.degrees(math.atan2(-direction_x,-direction_y)) + + distance_xy = math.sqrt(direction_x**2+direction_y**2) + p = math.degrees(math.atan2(direction_z,distance_xy)) + p = max(-89,min(89,p)) + + r=0 + + heading = (h,p,r) + + else: + # 使用当前相机朝向 + current_hpr = self.world.cam.getHpr() + heading = (current_hpr.x, current_hpr.y, current_hpr.z) + + pos = Point3(position[0], position[1], position[2]) + hpr = Vec3(heading[0], heading[1], heading[2]) + + self.patrol_points.append((pos, hpr, wait_time)) + print(f"✓ 添加巡检点 {len(self.patrol_points)}: 位置{position}, 朝向{heading}, 停留{wait_time}秒") + + # 在 PatrolSystem 类中添加以下方法 + + def add_auto_heading_patrol_point(self, position, wait_time=3.0): + """添加自动计算朝向的巡检点(朝向路径前进方向) + + Args: + position: 相机位置 (x, y, z) + wait_time: 在该点停留时间(秒) + """ + heading = None # 将自动计算朝向 + + # 复用原有的 add_patrol_point 方法 + self.add_patrol_point(position, heading, wait_time) + + def add_patrol_point_looking_at(self, position, look_at_position, wait_time=3.0): + """添加朝向指定位置的巡检点 + + Args: + position: 相机位置 (x, y, z) + look_at_position: 相机朝向的目标位置 (x, y, z) + wait_time: 在该点停留时间(秒) + """ + import math + + # 计算从当前位置到目标位置的方向向量 + direction_x = look_at_position[0] - position[0] + direction_y = look_at_position[1] - position[1] + direction_z = look_at_position[2] - position[2] + + # 计算HPR朝向 + h = math.degrees(math.atan2(-direction_x, -direction_y)) + + distance_xy = math.sqrt(direction_x ** 2 + direction_y ** 2) + p = math.degrees(math.atan2(direction_z, distance_xy)) + p = max(-89, min(89, p)) # 限制pitch角度在合理范围内 + + r = 0 # roll通常为0 + + heading = (h, p, r) + self.add_patrol_point(position, heading, wait_time) + + def clear_patrol_points(self): + """清空所有巡检点""" + self.patrol_points = [] + print("✓ 巡检点已清空") + + def set_patrol_speed(self, move_speed, turn_speed=None): + """设置巡检速度 + + Args: + move_speed: 移动速度(单位/秒) + turn_speed: 转向速度(度/秒),如果为None则保持当前值 + """ + self.patrol_speed = move_speed + if turn_speed is not None: + self.patrol_turn_speed = turn_speed + print(f"✓ 巡检速度已设置: 移动{move_speed}, 转向{turn_speed or self.patrol_turn_speed}") + + def start_patrol(self): + """开始巡检""" + if not self.patrol_points: + print("✗ 没有设置巡检点,无法开始巡检") + return False + + if self.is_patrolling: + print("⚠ 巡检已在进行中") + return True + + # 保存当前相机状态 + self.original_cam_pos = Point3(self.world.cam.getPos()) + self.original_cam_hpr = Vec3(self.world.cam.getHpr()) + + # 重置巡检状态 + self.current_patrol_index = 0 + self.patrol_state = "moving" + self.patrol_wait_timer = 0.0 + self.is_patrolling = True + + # 启动巡检任务 + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task") + + print(f"✓ 开始巡检,共{len(self.patrol_points)}个巡检点") + return True + + def stop_patrol(self): + """停止巡检""" + if not self.is_patrolling: + print("⚠ 巡检未在进行中") + return False + + # 停止巡检任务 + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = None + + self.is_patrolling = False + self.patrol_state = "moving" + self.patrol_wait_timer = 0.0 + + print("✓ 巡检已停止") + return True + + def pause_patrol(self): + """暂停巡检""" + if not self.is_patrolling: + print("⚠ 巡检未在进行中") + return False + + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = None + + print("✓ 巡检已暂停") + return True + + def resume_patrol(self): + """恢复巡检""" + if self.is_patrolling: + print("⚠ 巡检已在进行中") + return False + + if not self.patrol_points: + print("✗ 没有设置巡检点") + return False + + self.is_patrolling = True + self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task") + + print("✓ 巡检已恢复") + return True + + def reset_to_original_position(self): + """重置相机到原始位置""" + if self.original_cam_pos and self.original_cam_hpr: + self.world.cam.setPos(self.original_cam_pos) + self.world.cam.setHpr(self.original_cam_hpr) + print("✓ 相机已重置到原始位置") + return True + else: + print("✗ 没有保存的原始位置") + return False + + def _patrol_task(self, task): + """巡检主任务""" + try: + if not self.is_patrolling or not self.patrol_points: + return task.done + + # 获取当前巡检点 + current_point = self.patrol_points[self.current_patrol_index] + target_pos, target_hpr, wait_time = current_point + + # 根据当前状态执行不同操作 + if self.patrol_state == "moving": + self._handle_moving_state(target_pos) + elif self.patrol_state == "turning_to_target": + self._handle_turning_to_target_state(target_hpr) + elif self.patrol_state == "waiting": + self._handle_waiting_state(wait_time) + elif self.patrol_state == "turning_back": + self._handle_turning_back_state() + + return task.cont + + except Exception as e: + print(f"巡检任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def _handle_moving_state(self, target_pos): + """处理移动状态""" + current_pos = self.world.cam.getPos() + distance = (target_pos - current_pos).length() + + if distance < 0.1: # 到达目标点 + print(f"✓ 到达巡检点 {self.current_patrol_index + 1}") + self.patrol_state = "turning_to_target" + return + + # 计算移动方向和距离 + direction = target_pos - current_pos + direction.normalize() + + # 计算目标朝向(看向目标点) + target_hpr = self._look_at_to_hpr(direction) + current_hpr = self.world.cam.getHpr() + + # 平滑转向到目标朝向 + h_diff = self._normalize_angle(target_hpr.x - current_hpr.x) + p_diff = self._normalize_angle(target_hpr.y - current_hpr.y) + r_diff = self._normalize_angle(target_hpr.z - current_hpr.z) + + # 计算本帧应转动的角度 + dt = globalClock.getDt() + turn_amount = self.patrol_turn_speed * dt + + # 逐步转向目标角度 + new_hpr = Vec3(current_hpr) + + if abs(h_diff) > turn_amount: + new_hpr.x += turn_amount if h_diff > 0 else -turn_amount + else: + new_hpr.x = target_hpr.x + + if abs(p_diff) > turn_amount: + new_hpr.y += turn_amount if p_diff > 0 else -turn_amount + else: + new_hpr.y = target_hpr.y + + if abs(r_diff) > turn_amount: + new_hpr.z += turn_amount if r_diff > 0 else -turn_amount + else: + new_hpr.z = target_hpr.z + + self.world.cam.setHpr(new_hpr) + + # 计算本帧应移动的距离 + move_distance = self.patrol_speed * dt + + # 如果移动距离大于剩余距离,则直接移动到目标点 + if move_distance >= distance: + self.world.cam.setPos(target_pos) + else: + # 否则按方向移动 + new_pos = current_pos + direction * move_distance + self.world.cam.setPos(new_pos) + + def _handle_turning_to_target_state(self, target_hpr): + """处理转向目标状态""" + # 检查是否需要朝向下一个点 + if target_hpr == "look_next": + # 计算朝向下一个点的方向 + next_index = (self.current_patrol_index + 1) % len(self.patrol_points) + next_point_pos = self.patrol_points[next_index][0] + + current_pos = self.world.cam.getPos() + direction = next_point_pos - current_pos + direction.normalize() + + # 计算目标朝向 + target_hpr = self._look_at_to_hpr(direction) + + current_hpr = self.world.cam.getHpr() + + # 计算角度差 + h_diff = self._normalize_angle(target_hpr.x - current_hpr.x) + p_diff = self._normalize_angle(target_hpr.y - current_hpr.y) + r_diff = self._normalize_angle(target_hpr.z - current_hpr.z) + + # 检查是否已完成转向 + if abs(h_diff) < 1.0 and abs(p_diff) < 1.0 and abs(r_diff) < 1.0: + print(f"✓ 完成转向,开始停留") + self.patrol_state = "waiting" + self.patrol_wait_timer = 0.0 + return + + # 计算本帧应转动的角度 + dt = globalClock.getDt() + turn_amount = self.patrol_turn_speed * dt + + # 逐步转向目标角度 + new_hpr = Vec3(current_hpr) + + if abs(h_diff) > turn_amount: + new_hpr.x += turn_amount if h_diff > 0 else -turn_amount + else: + new_hpr.x = target_hpr.x + + if abs(p_diff) > turn_amount: + new_hpr.y += turn_amount if p_diff > 0 else -turn_amount + else: + new_hpr.y = target_hpr.y + + if abs(r_diff) > turn_amount: + new_hpr.z += turn_amount if r_diff > 0 else -turn_amount + else: + new_hpr.z = target_hpr.z + + self.world.cam.setHpr(new_hpr) + + def _handle_waiting_state(self, wait_time): + """处理等待状态""" + self.patrol_wait_timer += globalClock.getDt() + + if self.patrol_wait_timer >= wait_time: + print(f"✓ 停留结束,准备转回原朝向") + self.patrol_state = "turning_back" + + # 修改 core/patrol_system.py 中的 _handle_turning_back_state 方法 + + def _handle_turning_back_state(self): + """处理转回原朝向状态""" + # 直接完成转向状态,进入移动状态 + print(f"✓ 停留结束,开始移动到下一个点") + # 移动到下一个巡检点 + next_index = (self.current_patrol_index + 1) % len(self.patrol_points) + self.current_patrol_index = next_index + self.patrol_state = "moving" + return + + def _normalize_angle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _look_at_to_hpr(self, direction): + """将方向向量转换为HPR角度""" + # 简化的转换,实际应用中可能需要更精确的计算 + h = math.degrees(math.atan2(-direction.x, -direction.y)) + p = math.degrees(math.asin(direction.z)) + return Vec3(h, p, 0) + + def get_patrol_status(self): + """获取巡检状态信息""" + return { + "is_patrolling": self.is_patrolling, + "current_point": self.current_patrol_index, + "total_points": len(self.patrol_points), + "state": self.patrol_state, + "wait_timer": self.patrol_wait_timer + } + + def list_patrol_points(self): + """列出所有巡检点""" + if not self.patrol_points: + print("没有设置巡检点") + return + + print(f"巡检点列表 (共{len(self.patrol_points)}个):") + for i, (pos, hpr, wait_time) in enumerate(self.patrol_points): + current_marker = " >>>" if i == self.current_patrol_index and self.is_patrolling else "" + print(f" {i + 1}. 位置:({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f}) " + f"朝向:({hpr.x:.1f}, {hpr.y:.1f}, {hpr.z:.1f}) " + f"停留:{wait_time}秒{current_marker}") + + def remove_patrol_point(self, index): + """移除指定索引的巡检点""" + if 0 <= index < len(self.patrol_points): + removed_point = self.patrol_points.pop(index) + print( + f"✓ 移除巡检点 {index + 1}: 位置({removed_point[0].x:.1f}, {removed_point[0].y:.1f}, {removed_point[0].z:.1f})") + + # 调整当前索引 + if self.current_patrol_index >= len(self.patrol_points) and self.patrol_points: + self.current_patrol_index = len(self.patrol_points) - 1 + elif self.current_patrol_index >= len(self.patrol_points): + self.current_patrol_index = 0 + else: + print(f"✗ 无效的巡检点索引: {index}") + + def insert_patrol_point(self, index, position, heading=None, wait_time=3.0): + """在指定位置插入巡检点""" + if index < 0 or index > len(self.patrol_points): + print(f"✗ 无效的插入位置: {index}") + return + + if heading is None: + # 使用当前相机朝向 + current_hpr = self.world.cam.getHpr() + heading = (current_hpr.x, current_hpr.y, current_hpr.z) + + pos = Point3(position[0], position[1], position[2]) + hpr = Vec3(heading[0], heading[1], heading[2]) + + self.patrol_points.insert(index, (pos, hpr, wait_time)) + print(f"✓ 在位置 {index + 1} 插入巡检点: 位置{position}, 朝向{heading}, 停留{wait_time}秒") + + def update_patrol_point(self, index, position=None, heading=None, wait_time=None): + """更新指定巡检点的信息""" + if 0 <= index < len(self.patrol_points): + pos, hpr, wt = self.patrol_points[index] + + if position is not None: + pos = Point3(position[0], position[1], position[2]) + if heading is not None: + hpr = Vec3(heading[0], heading[1], heading[2]) + if wait_time is not None: + wt = wait_time + + self.patrol_points[index] = (pos, hpr, wt) + print(f"✓ 更新巡检点 {index + 1}") + else: + print(f"✗ 无效的巡检点索引: {index}") + + def goto_patrol_point(self, index): + """直接跳转到指定巡检点""" + if not self.patrol_points: + print("✗ 没有设置巡检点") + return False + + if 0 <= index < len(self.patrol_points): + pos, hpr, _ = self.patrol_points[index] + self.world.cam.setPos(pos) + self.world.cam.setHpr(hpr) + self.current_patrol_index = index + print(f"✓ 跳转到巡检点 {index + 1}") + return True + else: + print(f"✗ 无效的巡检点索引: {index}") + return False + + def cleanup(self): + """清理巡检系统资源""" + self.stop_patrol() + self.clear_patrol_points() + self.original_cam_pos = None + self.original_cam_hpr = None + print("✓ 巡检系统资源已清理") + + +# 使用示例和便捷函数 +def create_default_patrol_route(patrol_system): + """创建默认的巡检路线(示例)""" + # 清空现有巡检点 + patrol_system.clear_patrol_points() + + # 添加一些示例巡检点 + patrol_system.add_patrol_point((0, -20, 5), (0, -15, 0), 2.0) # 点1:前方低位置 + patrol_system.add_patrol_point((0, 0, 10), (0, -30, 0), 3.0) # 点2:中央高位置 + patrol_system.add_patrol_point((15, 10, 5), (-45, -10, 0), 2.5) # 点3:右侧位置 + patrol_system.add_patrol_point((-15, 10, 5), (45, -10, 0), 2.5) # 点4:左侧位置 + + print("✓ 默认巡检路线已创建") + diff --git a/core/script_system.py b/core/script_system.py index 07d6e96e..2191077c 100644 --- a/core/script_system.py +++ b/core/script_system.py @@ -769,6 +769,36 @@ class {class_name}(ScriptBase): print("==================\n") + def save_object_scripts(self,game_object,node_data:dict): + try: + if game_object in self.object_scripts: + scripts_data = [] + for script_commponent in self.object_scripts[game_object]: + script_info = { + 'script_name':script_commponent.script_name, + 'enabled':script_commponent.enabled, + 'script_class':script_commponent.script_instance.__class__.__name__ + } + scripts_data.append(script_info) + if scripts_data: + node_data['scripts'] = scripts_data + print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}") + except Exception as e: + print(f"保存对象脚本信息失败: {e}") + traceback.print_exc() + + # def restore_object_scripts(self,game_object,node_data:dict): + # try: + # if 'scripts' in node_data: + # scripts_data = node_data['scripts'] + # restored_count = 0 + # for script_info in scripts_data: + # script_name = script_info.get('script_name') + # enabled = script_info.get('enabled',True) + # + # #检查脚本是否可用 + # if script_name in self.loader.script_classes: + #为 # 添加全局便捷函数,让脚本更容易使用API def get_script_api(): diff --git a/core/selection.py b/core/selection.py index 6f160c1e..62986afc 100644 --- a/core/selection.py +++ b/core/selection.py @@ -8,6 +8,7 @@ - 射线检测和碰撞检测 """ from PIL.ImageChops import lighter +from direct.showbase.ShowBaseGlobal import globalClock from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, @@ -66,6 +67,11 @@ class SelectionSystem: self._current_cursor = None self._default_cursor = None + self._last_click_time = 0 + self._double_click_threshold = 0.3 + self._last_clicked_node = None + self._double_click_task = None + print("✓ 选择和变换系统初始化完成") # ==================== 光标设置 ==================== def _setCursor(self,cursor_type): @@ -112,7 +118,7 @@ class SelectionSystem: def createSelectionBox(self, nodePath): """为选中的节点创建选择框""" try: - print(f" 开始创建选择框,目标节点: {nodePath.getName()}") + #print(f" 开始创建选择框,目标节点: {nodePath.getName()}") # 如果已有选择框,先移除 if self.selectionBox: @@ -127,17 +133,17 @@ class SelectionSystem: # 创建选择框作为render的子节点,但会实时跟踪目标节点 self.selectionBox = self.world.render.attachNewNode("selectionBox") self.selectionBoxTarget = nodePath # 保存目标节点引用 - print(f" 选择框节点创建完成: {self.selectionBox}") + #print(f" 选择框节点创建完成: {self.selectionBox}") # 启动选择框更新任务 taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") - print(" 选择框更新任务已启动") + #print(" 选择框更新任务已启动") # 初始更新选择框 - print(" 开始初始化选择框几何体...") + #print(" 开始初始化选择框几何体...") self.updateSelectionBoxGeometry() - print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") + #print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") except Exception as e: print(f" ✗ 创建选择框失败: {str(e)}") @@ -335,7 +341,7 @@ class SelectionSystem: def createGizmo(self, nodePath): """为选中的节点创建坐标轴工具 - 保留箭头版本""" try: - print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") + #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") # 如果已有坐标轴,先移除 if self.gizmo: @@ -350,6 +356,22 @@ class SelectionSystem: self.gizmo = self.world.render.attachNewNode("gizmo") self.gizmoTarget = nodePath + # 添加标识标签,便于识别 + self.gizmo.setTag("is_gizmo", "1") + if hasattr(nodePath, 'getName'): + self.gizmo.setTag("gizmo_target", nodePath.getName()) + + # 为各轴添加标签 + if hasattr(self, 'gizmoXAxis') and self.gizmoXAxis: + self.gizmoXAxis.setTag("is_gizmo", "1") + self.gizmoXAxis.setTag("gizmo_axis", "x") + if hasattr(self, 'gizmoYAxis') and self.gizmoYAxis: + self.gizmoYAxis.setTag("is_gizmo", "1") + self.gizmoYAxis.setTag("gizmo_axis", "y") + if hasattr(self, 'gizmoZAxis') and self.gizmoZAxis: + self.gizmoZAxis.setTag("is_gizmo", "1") + self.gizmoZAxis.setTag("gizmo_axis", "z") + # 设置位置和朝向 minPoint = Point3() maxPoint = Point3() @@ -385,7 +407,7 @@ class SelectionSystem: # 只启动一次更新任务 taskMgr.add(self.updateGizmoTask, "updateGizmo") - print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") + #print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") except Exception as e: print(f"创建坐标轴失败: {str(e)}") @@ -431,7 +453,7 @@ class SelectionSystem: else: gizmo_model = self.world.loader.loadModel(path) if gizmo_model: - print(f"成功加载模型: {path}") + #print(f"成功加载模型: {path}") break except: continue @@ -1177,7 +1199,7 @@ class SelectionSystem: return self.checkGizmoClickFallback(mouseX, mouseY) # 计算点击阈值 - click_threshold = 30 # 增大检测范围 + click_threshold = 15 # 增大检测范围 # 检测各个轴,对于端点在屏幕外的轴提供回退方案 def getClickDetectionPoint(axis_name, original_screen_pos): @@ -1657,14 +1679,12 @@ class SelectionSystem: if parent_node and parent_node != self.world.render: try: - if parent_node.getTransform().hasMat(): - transform_mat = parent_node.getMat(self.world.render) - if not transform_mat.isSingular(): - world_axis_vector = transform_mat.xformVec(local_axis_vector) - else: - print("警告: 检测到奇异变换矩阵,使用默认轴向量") + #获取变换矩阵 + transfrom_mat = parent_node.getMat(self.world.render) + if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat): + world_axis_vector = transfrom_mat.xformVec(local_axis_vector) else: - print("警告: 父节点没有有效的变换矩阵,使用默认轴向量") + print("警告: 检测到无效变换矩阵,使用默认轴向量") except Exception as e: print(f"变换计算出错: {e},使用默认轴向量") else: @@ -1812,6 +1832,23 @@ class SelectionSystem: import traceback traceback.print_exc() + def _isMatrixValid(self, matrix): + """检查矩阵是否有效,替代 isSingular 方法""" + try: + # 检查矩阵元素是否为有效数字 + for i in range(4): + for j in range(4): + element = matrix.getCell(i, j) + # 检查是否为 NaN 或无穷大 + if str(element) == 'nan' or str(element) == 'inf' or str(element) == '-inf': + return False + # 检查是否过大 + if abs(element) > 1e10: + return False + return True + except: + return False + def _safeUpdatePropertyPanel(self): """安全地更新属性面板""" try: @@ -1871,58 +1908,80 @@ class SelectionSystem: # ==================== 选择管理 ==================== def updateSelection(self, nodePath): - """更新选择状态""" - print(f"\n=== 更新选择状态 ===") + try: + # 检查是否要选择的对象已经是当前选中的对象 + if self.selectedNode == nodePath: + #print("要选择的对象已经是当前选中的对象,跳过重复更新") + return + print(f"\n=== 更新选择状态 ===") + + # 如果正在删除节点,避免更新选择 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,跳过选择更新") + print("=== 选择状态更新完成 ===\n") + return + + node_name = "None" + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + print(f"新选择的节点: {node_name}") + + # 检查是否为双击 + is_double_click = self.checkDoubleClick(nodePath) + if is_double_click: + print(f"检测到双击 {node_name},执行聚焦") + # 启动聚焦(在下一帧执行,确保选择状态已更新) + taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus") + + self.selectedNode = nodePath + # 添加兼容性属性 + self.selectedObject = nodePath + + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + #print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + + # 创建选择框 + #print("创建选择框...") + self.createSelectionBox(nodePath) + if self.selectionBox: + box_name = "Unknown" + if self.selectionBox and not self.selectionBox.isEmpty(): + box_name = self.selectionBox.getName() + #print(f"✓ 选择框创建成功: {box_name}") + else: + print("× 选择框创建失败") + + # 创建坐标轴 + #print("创建坐标轴...") + self.createGizmo(nodePath) + if self.gizmo: + gizmo_name = "Unknown" + if self.gizmo and not self.gizmo.isEmpty(): + gizmo_name = self.gizmo.getName() + #print(f"✓ 坐标轴创建成功: {gizmo_name}") + else: + print("× 坐标轴创建失败") + + print(f"✓ 选中了节点: {node_name}") + else: + print("清除选择...") + self.clearSelectionBox() + self.clearGizmo() + print("✓ 取消选择") + + #当取消选择时,同步清空树形控件的选中状态 + if (hasattr(self.world,'interface_manager')and + self.world.interface_manager and + self.world.interface_manager.treeWidget): + self.world.interface_manager.treeWidget.setCurrentItem(None) + print("✓ 树形控件选中状态已清空") - # 如果正在删除节点,避免更新选择 - if hasattr(self, '_deleting_node') and self._deleting_node: - print("正在删除节点,跳过选择更新") print("=== 选择状态更新完成 ===\n") - return - - node_name = "None" - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - print(f"新选择的节点: {node_name}") - - self.selectedNode = nodePath - # 添加兼容性属性 - self.selectedObject = nodePath - - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - - # 创建选择框 - print("创建选择框...") - self.createSelectionBox(nodePath) - if self.selectionBox: - box_name = "Unknown" - if self.selectionBox and not self.selectionBox.isEmpty(): - box_name = self.selectionBox.getName() - print(f"✓ 选择框创建成功: {box_name}") - else: - print("× 选择框创建失败") - - # 创建坐标轴 - print("创建坐标轴...") - self.createGizmo(nodePath) - if self.gizmo: - gizmo_name = "Unknown" - if self.gizmo and not self.gizmo.isEmpty(): - gizmo_name = self.gizmo.getName() - print(f"✓ 坐标轴创建成功: {gizmo_name}") - else: - print("× 坐标轴创建失败") - - print(f"✓ 选中了节点: {node_name}") - else: - print("清除选择...") - self.clearSelectionBox() - self.clearGizmo() - print("✓ 取消选择") - - print("=== 选择状态更新完成 ===\n") + except Exception as e: + print(f"更新选择状态失败{str(e)}") + import traceback + traceback.print_exc() def getSelectedNode(self): """获取当前选中的节点""" @@ -2010,7 +2069,7 @@ class SelectionSystem: collision_np.hide() # 隐藏碰撞体,只用于检测 - print(f"✓ 成功创建 {axis_name} 轴碰撞体") + #print(f"✓ 成功创建 {axis_name} 轴碰撞体") except Exception as e: print(f"创建{axis_name}轴碰撞体失败: {e}") @@ -2098,4 +2157,628 @@ class SelectionSystem: if not collision_node.isEmpty(): print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") else: - print(f"{axis_name.upper()}轴节点不存在") \ No newline at end of file + print(f"{axis_name.upper()}轴节点不存在") + + def focusCameraOnSelectedNodeAdvanced(self): + """高级版的摄像机聚焦功能,包含平滑动画效果""" + try: + if not self.selectedNode or self.selectedNode.isEmpty(): + print("没有选中的节点,无法聚焦") + return False + + # 获取选中节点的边界框 + minPoint = Point3() + maxPoint = Point3() + + if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): + print("无法计算选中节点的边界框") + return False + + # 计算节点中心点和大小 + center = Point3( + (minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5 + ) + + # 计算节点的对角线长度 + size = (maxPoint - minPoint).length() + + # 如果节点太小,使用默认大小 + if size < 0.01: + size = 1.0 + + # 获取当前摄像机位置和朝向 + current_cam_pos = Point3(self.world.cam.getPos()) + current_cam_hpr = Vec3(self.world.cam.getHpr()) + + # 计算观察方向 + view_direction = current_cam_pos - center + if view_direction.length() < 0.001: + view_direction = Vec3(5, -5, 2) + + view_direction.normalize() + + # 计算合适的观察距离 + optimal_distance = max(size * 2.0, 5.0) + + # 计算目标摄像机位置 + target_cam_pos = center + (view_direction * optimal_distance) + + # 计算目标朝向(不直接使用lookAt,而是计算目标HPR) + # 创建临时节点用于计算目标朝向 + temp_node = self.world.render.attachNewNode("temp_lookat_target") + temp_node.setPos(center) + + # 创建另一个临时节点用于计算朝向 + dummy_cam = self.world.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + # 清理临时节点 + temp_node.removeNode() + dummy_cam.removeNode() + + # 使用任务来实现平滑移动动画 + self._startCameraFocusAnimation(current_cam_pos, target_cam_pos, + current_cam_hpr, target_cam_hpr) + + print(f"开始聚焦到节点: {self.selectedNode.getName()}") + return True + + except Exception as e: + print(f"高级聚焦功能失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): + """启动摄像机聚焦动画""" + try: + # 创建动画任务 + class CameraFocusData: + def __init__(self, start_pos, end_pos, start_hpr, end_hpr): + self.start_pos = Point3(start_pos) # 确保是Point3类型 + self.end_pos = Point3(end_pos) # 确保是Point3类型 + self.start_hpr = Vec3(start_hpr) # 确保是Vec3类型 + self.end_hpr = Vec3(end_hpr) # 确保是Vec3类型 + self.elapsed_time = 0.0 + self.duration = 0.8 # 增加动画持续时间到0.8秒,让动画更平滑 + + self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) + + # 移除之前的任务(如果存在) + taskMgr.remove("cameraFocusTask") + + # 添加新任务 + taskMgr.add(self._cameraFocusTask, "cameraFocusTask") + + except Exception as e: + print(f"启动摄像机聚焦动画失败: {e}") + + def _normalizeAngle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _cameraFocusTask(self, task): + """摄像机聚焦动画任务""" + try: + if not hasattr(self, '_camera_focus_data'): + return task.done + + data = self._camera_focus_data + data.elapsed_time += globalClock.getDt() + + # 计算插值因子 + t = min(1.0, data.elapsed_time / data.duration) + + # 使用更平滑的插值函数 + smooth_t = t * t * (3 - 2 * t) # 平滑步进插值 + + # 手动实现lerp功能 + def lerp_point3(start, end, factor): + return Point3( + start.x + (end.x - start.x) * factor, + start.y + (end.y - start.y) * factor, + start.z + (end.z - start.z) * factor + ) + + # 角度插值需要特殊处理,确保选择最短路径 + def lerp_hpr(start, end, factor): + # 规范化角度 + start_x = self._normalizeAngle(start.x) + start_y = self._normalizeAngle(start.y) + start_z = self._normalizeAngle(start.z) + + end_x = self._normalizeAngle(end.x) + end_y = self._normalizeAngle(end.y) + end_z = self._normalizeAngle(end.z) + + # 计算最短旋转路径 + diff_x = self._normalizeAngle(end_x - start_x) + diff_y = self._normalizeAngle(end_y - start_y) + diff_z = self._normalizeAngle(end_z - start_z) + + # 插值 + result_x = start_x + diff_x * factor + result_y = start_y + diff_y * factor + result_z = start_z + diff_z * factor + + # 再次规范化 + result_x = self._normalizeAngle(result_x) + result_y = self._normalizeAngle(result_y) + result_z = self._normalizeAngle(result_z) + + return Vec3(result_x, result_y, result_z) + + # 计算当前位置和朝向 + current_pos = lerp_point3(data.start_pos, data.end_pos, smooth_t) + current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) + + # 应用到摄像机 + self.world.cam.setPos(current_pos) + self.world.cam.setHpr(current_hpr) + + # 检查是否完成 + if t >= 1.0: + if hasattr(self, '_camera_focus_data'): + delattr(self, '_camera_focus_data') + print("摄像机聚焦动画完成") + return task.done + + return task.cont + + except Exception as e: + print(f"摄像机聚焦任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def focusCameraOnSelectedNode(self): + """将摄像机聚焦到选中的节点(无动画版本,但仍保持平滑转向)""" + try: + if not self.selectedNode or self.selectedNode.isEmpty(): + print("没有选中的节点,无法聚焦") + return False + + # 获取选中节点的边界框 + minPoint = Point3() + maxPoint = Point3() + + if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): + print("无法计算选中节点的边界框") + return False + + # 计算节点中心点 + center = Point3( + (minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5 + ) + + # 计算节点的大小(直径) + size = (maxPoint - minPoint).length() + + # 如果节点太小,使用默认大小 + if size < 0.1: + size = 5.0 + + # 获取当前摄像机位置 + current_cam_pos = Point3(self.world.cam.getPos()) + + # 计算观察方向 + view_direction = current_cam_pos - center + if view_direction.length() < 0.001: + # 如果摄像机正好在中心点,使用默认方向 + view_direction = Vec3(5, -5, 2) # 默认观察方向 + + # 标准化方向向量 + view_direction.normalize() + + # 计算合适的距离(基于节点大小) + optimal_distance = max(size * 3.0, 10.0) # 距离节点的距离是节点大小的3倍或至少10个单位 + + # 计算新的摄像机位置 + new_cam_pos = center + (view_direction * optimal_distance) + + # 平滑地设置摄像机位置和朝向 + # 创建临时节点用于计算目标朝向 + temp_lookat = self.world.render.attachNewNode("temp_lookat") + temp_lookat.setPos(center) + + # 获取当前朝向和目标朝向 + current_hpr = Vec3(self.world.cam.getHpr()) + + # 设置摄像机到目标位置 + self.world.cam.setPos(new_cam_pos) + self.world.cam.lookAt(temp_lookat) + target_hpr = Vec3(self.world.cam.getHpr()) + + # 恢复当前位置 + self.world.cam.setPos(current_cam_pos) + self.world.cam.setHpr(current_hpr) + + # 清理临时节点 + temp_lookat.removeNode() + + # 使用一个简单的任务来平滑过渡 + class SmoothCameraMoveData: + def __init__(self, start_pos, end_pos, start_hpr, end_hpr): + self.start_pos = Point3(start_pos) + self.end_pos = Point3(end_pos) + self.start_hpr = Vec3(start_hpr) + self.end_hpr = Vec3(end_hpr) + self.elapsed_time = 0.0 + self.duration = 0.5 # 0.5秒的平滑过渡 + + self._smooth_camera_move_data = SmoothCameraMoveData( + current_cam_pos, new_cam_pos, current_hpr, target_hpr + ) + + taskMgr.remove("smoothCameraMoveTask") + taskMgr.add(self._smoothCameraMoveTask, "smoothCameraMoveTask") + + print(f"摄像机开始聚焦到节点: {self.selectedNode.getName()}") + print(f"节点中心: {center}, 大小: {size:.2f}") + return True + + except Exception as e: + print(f"聚焦摄像机到选中节点失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _smoothCameraMoveTask(self, task): + """平滑摄像机移动任务""" + try: + if not hasattr(self, '_smooth_camera_move_data'): + return task.done + + data = self._smooth_camera_move_data + data.elapsed_time += globalClock.getDt() + + # 计算插值因子 + t = min(1.0, data.elapsed_time / data.duration) + + # 使用平滑插值 + smooth_t = t * t * (3 - 2 * t) + + # 角度插值需要特殊处理 + def lerp_hpr(start, end, factor): + # 规范化角度 + start_x = self._normalizeAngle(start.x) + start_y = self._normalizeAngle(start.y) + start_z = self._normalizeAngle(start.z) + + end_x = self._normalizeAngle(end.x) + end_y = self._normalizeAngle(end.y) + end_z = self._normalizeAngle(end.z) + + # 计算最短旋转路径 + diff_x = self._normalizeAngle(end_x - start_x) + diff_y = self._normalizeAngle(end_y - start_y) + diff_z = self._normalizeAngle(end_z - start_z) + + # 插值 + result_x = start_x + diff_x * factor + result_y = start_y + diff_y * factor + result_z = start_z + diff_z * factor + + return Vec3(result_x, result_y, result_z) + + # 计算当前位置和朝向 + current_pos = Point3( + data.start_pos.x + (data.end_pos.x - data.start_pos.x) * smooth_t, + data.start_pos.y + (data.end_pos.y - data.start_pos.y) * smooth_t, + data.start_pos.z + (data.end_pos.z - data.start_pos.z) * smooth_t + ) + + current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) + + # 应用到摄像机 + self.world.cam.setPos(current_pos) + self.world.cam.setHpr(current_hpr) + + # 检查是否完成 + if t >= 1.0: + if hasattr(self, '_smooth_camera_move_data'): + delattr(self, '_smooth_camera_move_data') + print("摄像机平滑移动完成") + return task.done + + return task.cont + + except Exception as e: + print(f"平滑摄像机移动任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def handleMouseClick(self, nodePath, mouseX=None, mouseY=None): + """处理鼠标点击事件 - 支持坐标轴双击聚焦""" + try: + # 如果正在删除节点,忽略鼠标点击 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,忽略鼠标点击") + return + + import time + current_time = time.time() + + # 检查是否点击了坐标轴 + is_gizmo_click = False + target_node = nodePath + + # 判断是否点击了坐标轴 + if (nodePath and hasattr(nodePath, 'getName') and + (nodePath.getName().startswith("gizmo") or + "gizmo" in nodePath.getName().lower() or + (hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))): + is_gizmo_click = True + # 如果有选中的模型,使用选中的模型作为聚焦目标 + if self.selectedNode and not self.selectedNode.isEmpty(): + target_node = self.selectedNode + print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}") + + # 检查是否为双击(同一节点且在时间阈值内) + is_double_click = (self._last_clicked_node == target_node and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 双击 detected + node_name = target_node.getName() if target_node else "None" + print(f"检测到双击节点: {node_name}") + + # 无论是点击模型还是坐标轴,都执行聚焦 + if target_node and not target_node.isEmpty(): + print(f"双击聚焦到节点: {target_node.getName()}") + # 执行聚焦 + self.focusCameraOnSelectedNodeAdvanced() + + # 重置状态以避免三击等误触发 + self._last_click_time = 0 + self._last_clicked_node = None + else: + # 单击,更新状态 + self._last_click_time = current_time + self._last_clicked_node = target_node + + # 如果点击的是坐标轴,保持当前选择不变 + if is_gizmo_click: + print("坐标轴单击,保持当前选择") + else: + # 正常的单击选择 + self.updateSelection(nodePath) + + except Exception as e: + print(f"处理鼠标点击事件失败: {e}") + + def _onDoubleClick(self, nodePath): + """双击事件处理""" + try: + # 获取实际要聚焦的目标节点 + target_node = nodePath + + # 如果是坐标轴,确保使用关联的模型作为目标 + if (nodePath and hasattr(nodePath, 'hasTag') and + nodePath.hasTag("is_gizmo")): + if self.selectedNode and not self.selectedNode.isEmpty(): + target_node = self.selectedNode + print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}") + else: + print("坐标轴双击,但没有关联的选中模型") + return + + if target_node and not target_node.isEmpty(): + print(f"双击聚焦到节点: {target_node.getName()}") + # 更新选择(如果需要) + if self.selectedNode != target_node: + self.updateSelection(target_node) + + # 执行聚焦 + self.focusCameraOnSelectedNodeAdvanced() + else: + print("双击事件:没有有效的目标节点") + + except Exception as e: + print(f"双击事件处理失败: {e}") + + # 添加一个更精确的双击检测方法 + + def checkDoubleClick(self, nodePath): + """检查是否为双击,返回布尔值""" + try: + import time + current_time = time.time() + + is_double_click = (self._last_clicked_node == nodePath and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 双击 detected,重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + else: + # 更新状态为单击 + self._last_click_time = current_time + self._last_clicked_node = nodePath + + return is_double_click + + except Exception as e: + print(f"双击检测失败: {e}") + return False + + # 添加一个定时重置方法,用于清除长时间未完成的双击状态 + + def _resetDoubleClickState(self): + """重置双击状态""" + self._last_click_time = 0 + self._last_clicked_node = None + + # 添加一个任务来自动重置双击状态 + + def _startDoubleClickResetTask(self): + """启动双击状态重置任务""" + if self._double_click_task: + taskMgr.remove(self._double_click_task) + + self._double_click_task = taskMgr.doMethodLater( + self._double_click_threshold * 2, # 等待2倍阈值时间 + self._resetDoubleClickStateTask, + "resetDoubleClickState" + ) + + def _resetDoubleClickStateTask(self, task): + """任务:重置双击状态""" + self._resetDoubleClickState() + self._double_click_task = None + return task.done + + # 修改 updateSelection 方法以集成双击检测 + + # def updateSelection(self, nodePath): + # """更新选择状态""" + # print(f"\n=== 更新选择状态 ===") + # + # # 如果正在删除节点,避免更新选择 + # if hasattr(self, '_deleting_node') and self._deleting_node: + # print("正在删除节点,跳过选择更新") + # print("=== 选择状态更新完成 ===\n") + # return + # + # node_name = "None" + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"新选择的节点: {node_name}") + # + # # 检查是否为双击 + # is_double_click = self.checkDoubleClick(nodePath) + # if is_double_click: + # print(f"检测到双击 {node_name},执行聚焦") + # # 启动聚焦(在下一帧执行,确保选择状态已更新) + # taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus") + # + # self.selectedNode = nodePath + # # 添加兼容性属性 + # self.selectedObject = nodePath + # + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + # + # # 创建选择框 + # print("创建选择框...") + # self.createSelectionBox(nodePath) + # if self.selectionBox: + # box_name = "Unknown" + # if self.selectionBox and not self.selectionBox.isEmpty(): + # box_name = self.selectionBox.getName() + # print(f"✓ 选择框创建成功: {box_name}") + # else: + # print("× 选择框创建失败") + # + # # 创建坐标轴 + # print("创建坐标轴...") + # self.createGizmo(nodePath) + # if self.gizmo: + # gizmo_name = "Unknown" + # if self.gizmo and not self.gizmo.isEmpty(): + # gizmo_name = self.gizmo.getName() + # print(f"✓ 坐标轴创建成功: {gizmo_name}") + # else: + # print("× 坐标轴创建失败") + # + # print(f"✓ 选中了节点: {node_name}") + # else: + # print("清除选择...") + # self.clearSelectionBox() + # self.clearGizmo() + # print("✓ 取消选择") + # + # print("=== 选择状态更新完成 ===\n") + + def _delayedFocusTask(self, task): + """延迟执行聚焦任务""" + try: + self.focusCameraOnSelectedNodeAdvanced() + except Exception as e: + print(f"延迟聚焦任务失败: {e}") + return task.done + + # 添加一个更智能的双击检测方法,考虑鼠标位置 + + def checkDoubleClickWithPosition(self, nodePath, mouse_x=None, mouse_y=None): + """检查是否为双击,同时考虑鼠标位置""" + try: + import time + current_time = time.time() + + # 如果没有提供鼠标位置,直接使用基本双击检测 + if mouse_x is None or mouse_y is None: + return self.checkDoubleClick(nodePath) + + # 检查节点和时间 + time_diff = current_time - self._last_click_time + is_same_node = (self._last_clicked_node == nodePath) + + # 如果是同一节点且在时间阈值内,认为是双击 + if is_same_node and time_diff < self._double_click_threshold: + # 重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + return True + else: + # 更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"带位置的双击检测失败: {e}") + return False + + # 添加一个公共方法来设置双击阈值 + + def setDoubleClickThreshold(self, threshold_seconds): + """设置双击时间阈值""" + if threshold_seconds > 0: + self._double_click_threshold = threshold_seconds + print(f"双击阈值已设置为: {threshold_seconds} 秒") + else: + print("无效的双击阈值") + + # 添加一个方法来手动触发双击聚焦(可用于测试或其他触发方式) + + def triggerDoubleClickFocus(self, nodePath=None): + """手动触发双击聚焦""" + try: + target_node = nodePath if nodePath else self.selectedNode + if target_node and not target_node.isEmpty(): + print(f"手动触发聚焦到节点: {target_node.getName()}") + if self.selectedNode != target_node: + self.updateSelection(target_node) + self.focusCameraOnSelectedNodeAdvanced() + return True + else: + print("没有有效的目标节点进行聚焦") + return False + except Exception as e: + print(f"手动触发聚焦失败: {e}") + return False + + def cleanup(self): + """清理选择系统资源""" + # 清理双击任务 + if self._double_click_task: + taskMgr.remove(self._double_click_task) + self._double_click_task = None + + # 清理其他资源 + self.clearSelectionBox() + self.clearGizmo() \ No newline at end of file diff --git a/core/world.py b/core/world.py index 36481f66..4ed52631 100644 --- a/core/world.py +++ b/core/world.py @@ -329,10 +329,51 @@ class CoreWorld(Panda3DWorld): mat.setName("GroundMaterial") color = LColor(1, 1, 1, 0.8) mat.set_base_color(color) - mat.set_roughness(0.5) # 设置合适的初始粗糙度 + mat.set_roughness(1) # 设置合适的初始粗糙度 mat.set_metallic(0.5) # 设置较低的初始金属性 self.ground.set_material(mat) + #创建第二个相同的地面,位置稍有偏移 + self.ground2 = self.render.attachNewNode(cm.generate()) + self.ground2.setH(-90) + self.ground2.setZ(-1.0) + self.ground2.setX(50) # 在X轴方向偏移 + self.ground2.setZ(49) # 在X轴方向偏移 + self.ground2.setColor(0.8, 0.8, 0.8, 1) + self.ground2.set_material(mat) + + # 创建第三个相同的地面,位置在另一个方向 + self.ground3 = self.render.attachNewNode(cm.generate()) + self.ground3.setH(90) + self.ground3.setZ(-1.0) + self.ground3.setX(-50) # 在X轴负方向偏移 + self.ground3.setZ(49) # 在X轴负方向偏移 + self.ground3.setColor(0.8, 0.8, 0.8, 1) + self.ground3.set_material(mat) + + self.ground4 = self.render.attachNewNode(cm.generate()) + # self.ground3.setR(90) + self.ground4.setZ(-1.0) + self.ground4.setY(50) # 在X轴负方向偏移 + self.ground4.setZ(49) # 在X轴负方向偏移 + self.ground4.setColor(0.8, 0.8, 0.8, 1) + self.ground4.set_material(mat) + + self.ground5 = self.render.attachNewNode(cm.generate()) + self.ground5.setP(180) + self.ground5.setZ(-1) + self.ground5.setY(-50) # 在X轴负方向偏移 + self.ground5.setZ(49) # 在X轴负方向偏移 + self.ground5.setColor(0.8, 0.8, 0.8, 1) + self.ground5.set_material(mat) + + self.ground6 = self.render.attachNewNode(cm.generate()) + self.ground6.setP(90) + self.ground6.setZ(-1) + self.ground6.setZ(99) # 在X轴负方向偏移 + self.ground6.setColor(0.8, 0.8, 0.8, 1) + self.ground6.set_material(mat) + # 应用默认PBR效果,确保支持贴图 try: if hasattr(self, 'render_pipeline') and self.render_pipeline: @@ -349,6 +390,72 @@ class CoreWorld(Panda3DWorld): }, 50 ) + # 为其他两个地面也应用相同的效果 + self.render_pipeline.set_effect( + self.ground2, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + self.render_pipeline.set_effect( + self.ground3, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + self.render_pipeline.set_effect( + self.ground4, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + self.render_pipeline.set_effect( + self.ground5, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + self.render_pipeline.set_effect( + self.ground6, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) print("✓ 地板PBR效果已应用") else: print("⚠️ RenderPipeline未初始化,地板将使用基础渲染") diff --git a/demo/test_gizmo_drag.py b/demo/test_gizmo_drag.py index 81718bc3..4a1b39d2 100644 --- a/demo/test_gizmo_drag.py +++ b/demo/test_gizmo_drag.py @@ -359,7 +359,7 @@ class GizmoDragTestWorld(Panda3DWorld): distance = distance_to_line( (mouseX, mouseY), center_screen, axis_screen ) - print(f"{axis_label}距离: {distance:.2f}") + #print(f"{axis_label}距离: {distance:.2f}") if distance < click_threshold: print(f"✓ 点击了{axis_label}") diff --git a/main.py b/main.py index 9bca08bc..4d150534 100644 --- a/main.py +++ b/main.py @@ -22,6 +22,7 @@ from core.script_system import ScriptManager from core.vr_manager import VRManager from core.vr_input_handler import VRInputHandler from core.alvr_streamer import ALVRStreamer +from core.patrol_system import PatrolSystem from gui.gui_manager import GUIManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager @@ -55,7 +56,16 @@ class MyWorld(CoreWorld): # 初始化选择和变换系统 self.selection = SelectionSystem(self) - + + # 绑定F键用于聚焦选中节点 + self.accept("f", self.onFocusKeyPressed) + self.accept("F", self.onFocusKeyPressed) # 大写F + + #初始化巡检系统 + self.patrol_system = PatrolSystem(self) + self.accept("p",self.onPatrolKeyPressed) + self.accept("P",self.onPatrolKeyPressed) + # 初始化事件处理系统 self.event_handler = EventHandler(self) @@ -105,7 +115,7 @@ class MyWorld(CoreWorld): self.collision_manager = CollisionManager(self) # 调试选项 - self.debug_collision = False # 是否显示碰撞体 + self.debug_collision = True # 是否显示碰撞体 # 默认启用模型间碰撞检测(可选) self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5) @@ -810,6 +820,81 @@ class MyWorld(CoreWorld): """获取碰撞统计""" return self.collision_manager.getCollisionStatistics() + def setupKeyboardEvents(self): + """设置键盘事件""" + try: + # 绑定 F 键用于聚焦选中节点 + self.accept("f", self.onFocusKeyPressed) + self.accept("F", self.onFocusKeyPressed) # 大写F + + print("✓ 键盘事件绑定完成") + + except Exception as e: + print(f"设置键盘事件失败: {e}") + + def onFocusKeyPressed(self): + """处理 F 键按下事件""" + try: + #print("检测到 F 键按下") + + # 检查是否有选中的节点 + if hasattr(self, 'selection') and self.selection.selectedNode: + #print(f"当前选中节点: {self.selection.selectedNode.getName()}") + # 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本) + # self.selection.focusCameraOnSelectedNode() # 无动画版本 + self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本 + else: + print("当前没有选中任何节点") + + except Exception as e: + print(f"处理 F 键事件失败: {e}") + + def onPatrolKeyPressed(self): + """处理 P 键按下事件 - 控制巡检系统""" + try: + print("检测到 P 键按下") + + if not self.patrol_system.is_patrolling: + # 如果巡检系统没有点,创建默认巡检路线 + if not self.patrol_system.patrol_points: + self.createDefaultPatrolRoute() + + # 开始巡检 + if self.patrol_system.start_patrol(): + print("✓ 巡检已开始") + else: + print("✗ 巡检启动失败") + else: + # 停止巡检 + if self.patrol_system.stop_patrol(): + print("✓ 巡检已停止") + else: + print("✗ 巡检停止失败") + + except Exception as e: + print(f"处理 P 键事件失败: {e}") + + def createDefaultPatrolRoute(self): + """创建默认巡检路线(使用自动朝向)""" + try: + # 清空现有巡检点 + self.patrol_system.clear_patrol_points() + + # 添加巡检点,使用None表示朝向下一个点 + self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5) + + # 最后一个点可以指定特定的朝向,或者也设为None继续循环 + self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5) + + print("✓ 默认自动朝向巡检路线已创建") + self.patrol_system.list_patrol_points() + + except Exception as e: + print(f"创建默认自动朝向巡检路线失败: {e}") + # ==================== 项目管理功能代理 ==================== # 以下函数代理到project_manager模块的对应功能 diff --git a/scene/scene_manager.py b/scene/scene_manager.py index d2d5d31c..41175d72 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -101,8 +101,8 @@ class SceneManager: try: print(f"\n=== 开始导入模型: {filepath} ===") - print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}") - print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}") + #print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}") + #print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}") filepath = util.normalize_model_path(filepath) original_filepath = filepath @@ -117,10 +117,10 @@ class SceneManager: # 检查是否需要转换为GLB以获得更好的动画支持 if auto_convert_to_glb and self._shouldConvertToGLB(filepath): - print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") + #print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") converted_path = self._convertToGLBWithProgress(filepath) if converted_path: - print(f"✅ 转换成功: {converted_path}") + #print(f"✅ 转换成功: {converted_path}") filepath = converted_path # 显示成功消息 try: @@ -135,7 +135,7 @@ class SceneManager: # 总是重新加载模型以确保材质信息完整 # 不使用ModelPool缓存,避免材质信息丢失问题 - print("直接从文件加载模型...") + #print("直接从文件加载模型...") model = self.world.loader.loadModel(filepath) if not model: print("加载模型失败") @@ -170,12 +170,12 @@ class SceneManager: # 可选的单位转换(主要针对FBX) if apply_unit_conversion and filepath.lower().endswith('.fbx'): - print("应用FBX单位转换(厘米到米)...") + #print("应用FBX单位转换(厘米到米)...") self._applyUnitConversion(model, 0.01) # 智能缩放标准化(处理FBX子节点的大缩放值) if normalize_scales and filepath.lower().endswith('.fbx'): - print("标准化FBX模型缩放层级...") + #print("标准化FBX模型缩放层级...") self._normalizeModelScales(model) # 调整模型位置到地面 @@ -219,9 +219,9 @@ class SceneManager: if root_item: qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE") if qt_item: - tree_widget.setCurrentItem(qt_item) + #tree_widget.setCurrentItem(qt_item) # 更新选择和属性面板 - tree_widget.update_selection_and_properties(model, qt_item) + #tree_widget.update_selection_and_properties(model, qt_item) print("✅ Qt树节点添加成功") else: print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") @@ -282,11 +282,23 @@ class SceneManager: node_path.clearTransform() return - # 方法2: 检查矩阵是否奇异 + # 方法2: 简单的矩阵有效性检查(移除 isSingular 调用) try: matrix = node_path.getMat() - if matrix.isSingular(): - print(f"⚠️ 节点 {node_name} 有奇异矩阵") + # 检查矩阵元素是否有效 + 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: @@ -403,11 +415,11 @@ class SceneManager: def apply_material(node_path, depth=0): indent = " " * depth try: - print(f"{indent}处理节点: {node_path.getName()}") - print(f"{indent}节点类型: {node_path.node().__class__.__name__}") + #print(f"{indent}处理节点: {node_path.getName()}") + #print(f"{indent}节点类型: {node_path.node().__class__.__name__}") if isinstance(node_path.node(), GeomNode): - print(f"{indent}发现GeomNode,处理材质") + #print(f"{indent}发现GeomNode,处理材质") geom_node = node_path.node() # 检查所有几何体的状态 @@ -423,11 +435,11 @@ class SceneManager: if node_material.hasBaseColor(): color = node_material.getBaseColor() has_color = True - print(f"{indent}从节点材质获取基础颜色: {color}") + #print(f"{indent}从节点材质获取基础颜色: {color}") elif node_material.hasDiffuse(): color = node_material.getDiffuse() has_color = True - print(f"{indent}从节点材质获取漫反射颜色: {color}") + #print(f"{indent}从节点材质获取漫反射颜色: {color}") # 检查几何体材质 if not has_color: @@ -444,12 +456,12 @@ class SceneManager: if orig_material.hasBaseColor(): color = orig_material.getBaseColor() has_color = True - print(f"{indent}从几何体材质获取基础颜色: {color}") + #print(f"{indent}从几何体材质获取基础颜色: {color}") break elif orig_material.hasDiffuse(): color = orig_material.getDiffuse() has_color = True - print(f"{indent}从几何体材质获取漫反射颜色: {color}") + #print(f"{indent}从几何体材质获取漫反射颜色: {color}") break # 检查颜色属性 @@ -458,7 +470,7 @@ class SceneManager: if not color_attrib.isOff(): color = color_attrib.getColor() has_color = True - print(f"{indent}从颜色属性获取: {color}") + #print(f"{indent}从颜色属性获取: {color}") break except Exception as geom_error: print(f"{indent}处理几何体 {i} 时出错: {geom_error}") @@ -467,7 +479,7 @@ class SceneManager: # 创建新材质 material = Material() if has_color and color: - print(f"{indent}应用找到的颜色: {color}") + #print(f"{indent}应用找到的颜色: {color}") try: # 确保颜色值有效 if (color.getX() == color.getX() and color.getY() == color.getY() and @@ -496,18 +508,18 @@ class SceneManager: # 应用材质 try: node_path.setMaterial(material, 1) # 1表示强制应用 - print(f"{indent}材质应用成功") + #print(f"{indent}材质应用成功") except Exception as mat_error: print(f"{indent}⚠️ 应用材质时出错: {mat_error}") - print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") + #print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") except Exception as node_error: print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}") # 递归处理子节点 child_count = node_path.getNumChildren() - print(f"{indent}子节点数量: {child_count}") + #print(f"{indent}子节点数量: {child_count}") for i in range(child_count): try: child = node_path.getChild(i) @@ -517,99 +529,13 @@ class SceneManager: continue # 应用材质 - print("\n开始递归应用材质...") + #print("\n开始递归应用材质...") try: apply_material(model) except Exception as e: print(f"应用材质时出错: {e}") print("=== 材质设置完成 ===\n") - def setupCollision(self, model): - """为模型设置碰撞检测(增强版本)""" - try: - if model.isEmpty(): - print("⚠️ 空模型无法设置碰撞检测") - return None - - # 验证模型变换 - self._fixNodeTransform(model) - - # 创建碰撞节点 - cNode = CollisionNode(f'modelCollision_{model.getName()}') - - # 设置碰撞掩码 - cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 - - # 如果启用了模型间碰撞检测,添加额外的掩码 - if (hasattr(self.world, 'collision_manager') 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) - print(f"为 {model.getName()} 启用模型间碰撞检测") - - # 获取模型的边界 - bounds = model.getBounds() - if bounds.isEmpty(): - print(f"⚠️ 模型 {model.getName()} 边界为空,使用默认碰撞体") - # 使用默认的小球体 - cSphere = CollisionSphere(0, 0, 0, 1.0) - else: - try: - center = bounds.getCenter() - radius = bounds.getRadius() - - # 确保半径不为零 - if radius <= 0 or radius != radius: # 检查NaN - radius = 1.0 - print(f"⚠️ 模型 {model.getName()} 半径无效,使用默认半径 1.0") - - # 确保中心点有效 - if (center.getX() != center.getX() or - center.getY() != center.getY() or - center.getZ() != center.getZ()): - center = Point3(0, 0, 0) - print(f"⚠️ 模型 {model.getName()} 中心点无效,使用默认中心点") - - cSphere = CollisionSphere(center, radius) - except Exception as e: - print(f"⚠️ 创建碰撞体时出错: {e}") - cSphere = CollisionSphere(0, 0, 0, 1.0) - - cNode.addSolid(cSphere) - - # 将碰撞节点附加到模型上 - try: - cNodePath = model.attachNewNode(cNode) - except Exception as e: - print(f"⚠️ 附加碰撞节点时出错: {e}") - # 创建一个新的节点来附加 - cNodePath = self.world.render.attachNewNode(cNode) - cNodePath.reparentTo(model) - - # 根据调试设置决定是否显示碰撞体 - if hasattr(self.world, 'debug_collision') and self.world.debug_collision: - cNodePath.show() - else: - cNodePath.hide() - - # 为模型添加碰撞相关标签 - model.setTag("has_collision", "true") - try: - model.setTag("collision_radius", str(bounds.getRadius() if not bounds.isEmpty() else 1.0)) - except: - model.setTag("collision_radius", "1.0") - - print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成") - - return cNodePath - - except Exception as e: - print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}") - import traceback - traceback.print_exc() - return None def _applyModelScale(self, model, scale_factor): """应用模型特定缩放 @@ -640,140 +566,11 @@ class SceneManager: except Exception as e: print(f"应用模型缩放失败: {str(e)}") - def _applyMaterialsToModel(self, model): - """递归应用材质到模型的所有GeomNode""" - - def apply_material(node_path, depth=0): - indent = " " * depth - try: - print(f"{indent}处理节点: {node_path.getName()}") - print(f"{indent}节点类型: {node_path.node().__class__.__name__}") - - if isinstance(node_path.node(), GeomNode): - print(f"{indent}发现GeomNode,处理材质") - geom_node = node_path.node() - - # 检查所有几何体的状态 - has_color = False - color = None - - # 首先检查节点自身的状态 - node_state = node_path.getState() - if node_state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) - node_material = mat_attrib.getMaterial() - if node_material and node_material.hasDiffuse(): - color = node_material.getDiffuse() - has_color = True - print(f"{indent}从节点材质获取颜色: {color}") - - # 检查FBX特有的属性 - for tag_key in node_path.getTagKeys(): - print(f"{indent}发现标签: {tag_key}") - if "color" in tag_key.lower() or "diffuse" in tag_key.lower(): - tag_value = node_path.getTag(tag_key) - print(f"{indent}颜色相关标签: {tag_key} = {tag_value}") - - # 如果还没找到颜色,检查几何体 - if not has_color: - for i in range(geom_node.getNumGeoms()): - try: - geom = geom_node.getGeom(i) - state = geom_node.getGeomState(i) - - # 检查顶点颜色 - vdata = geom.getVertexData() - if vdata: - format = vdata.getFormat() - if format: - for j in range(format.getNumColumns()): - try: - column = format.getColumn(j) - # InternalName对象需要使用getName()转换为字符串 - column_name = column.getName().getName() - if "color" in column_name.lower(): - print(f"{indent}发现顶点颜色数据: {column_name}") - except Exception: - continue - - # 检查材质属性 - if state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) - orig_material = mat_attrib.getMaterial() - if orig_material: - if orig_material.hasBaseColor(): - color = orig_material.getBaseColor() - has_color = True - print(f"{indent}从基础颜色获取: {color}") - break - elif orig_material.hasDiffuse(): - color = orig_material.getDiffuse() - has_color = True - print(f"{indent}从漫反射颜色获取: {color}") - break - - # 检查颜色属性 - if not has_color and state.hasAttrib(ColorAttrib.getClassType()): - color_attrib = state.getAttrib(ColorAttrib.getClassType()) - if not color_attrib.isOff(): - color = color_attrib.getColor() - has_color = True - print(f"{indent}从颜色属性获取: {color}") - break - except Exception as geom_error: - print(f"{indent}处理几何体 {i} 时出错: {geom_error}") - continue - - # 创建新材质 - material = Material() - if has_color: - print(f"{indent}应用找到的颜色: {color}") - try: - material.setDiffuse(color) - material.setBaseColor(color) # 同时设置基础颜色 - node_path.setColor(color) - except Exception as color_error: - print(f"{indent}设置颜色时出错: {color_error}") - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - else: - print(f"{indent}使用默认颜色") - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - - # 设置其他材质属性 - material.setAmbient((0.2, 0.2, 0.2, 1.0)) - material.setSpecular((0.5, 0.5, 0.5, 1.0)) - material.setShininess(32.0) - - # 应用材质 - node_path.setMaterial(material) - print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") - - except Exception as node_error: - print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}") - - # 递归处理子节点 - child_count = node_path.getNumChildren() - print(f"{indent}子节点数量: {child_count}") - for i in range(child_count): - try: - child = node_path.getChild(i) - apply_material(child, depth + 1) - except Exception as child_error: - print(f"{indent}处理子节点 {i} 时出错: {child_error}") - continue - - # 应用材质 - print("\n开始递归应用材质...") - try: - apply_material(model) - except Exception as e: - print(f"应用材质时出错: {e}") - print("=== 材质设置完成 ===\n") def _adjustModelToGround(self, model): """智能调整模型到地面,但保持原有缩放结构""" try: - print("调整模型位置到地面...") + #print("调整模型位置到地面...") # 获取模型的边界框 bounds = model.getBounds() @@ -793,9 +590,9 @@ class SceneManager: # 设置模型位置:X,Y居中,Z调整到地面 model.setPos(0, 0, ground_offset) - print(f"模型边界: 最小点{min_point}, 中心{center}") - print(f"地面偏移: {ground_offset}") - print(f"最终位置: {model.getPos()}") + #print(f"模型边界: 最小点{min_point}, 中心{center}") + #print(f"地面偏移: {ground_offset}") + #print(f"最终位置: {model.getPos()}") except Exception as e: print(f"调整模型位置失败: {str(e)}") @@ -1071,7 +868,7 @@ class SceneManager: # 根据调试设置决定是否显示碰撞体 if hasattr(self.world, 'debug_collision') and self.world.debug_collision: - cNodePath.hide() + cNodePath.show() else: cNodePath.hide() diff --git a/scene/util.py b/scene/util.py index 678beba0..06ad4b23 100644 --- a/scene/util.py +++ b/scene/util.py @@ -25,9 +25,9 @@ class CrossPlatformPathHandler: def normalize_model_path(self, filepath): """标准化模型文件路径""" try: - print(f"\n=== 路径标准化处理 ===") - print(f"原始路径: {filepath}") - print(f"当前系统: {self.system}") + #print(f"\n=== 路径标准化处理 ===") + #print(f"原始路径: {filepath}") + #print(f"当前系统: {self.system}") # 步骤1: 检查原始路径是否存在 if self._check_file_exists(filepath): @@ -54,10 +54,6 @@ class CrossPlatformPathHandler: def _check_file_exists(self, filepath): """检查文件是否存在""" exists = os.path.exists(filepath) - if exists: - print(f"✓ 文件存在: {filepath}") - else: - print(f"⚠️ 文件不存在: {filepath}") return exists def _panda3d_normalize(self, filepath): diff --git a/ui/interface_manager.py b/ui/interface_manager.py index 17042936..e5e0c626 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -25,10 +25,39 @@ class InterfaceManager: # 更新场景树 self.world.scene_manager.updateSceneTree() + def onTreeWidgetClicked(self, index): + """处理树形控件点击事件(包括空白区域)""" + # 检查点击的是否是空白区域 + if not self.treeWidget.itemFromIndex(index): # 点击的是空白区域 + self.world.selection.updateSelection(None) + self.world.property_panel.clearPropertyPanel() + print("点击树形控件空白区域,清除选中状态") + + def onTreeCurrentItemChanged(self, current, previous): + """处理树形控件当前选中项改变事件""" + # 当 current 为 None 时,表示点击了空白区域 + if current is None: + self.world.selection.updateSelection(None) + print("点击空白区域,清除选中状态") + # 当 current 不为 None 时,表示选中了某个项目 + else: + # 更新选择状态 + nodePath = current.data(0, Qt.UserRole) + if nodePath: + self.world.selected_np = nodePath + self.world.selection.updateSelection(nodePath) + self.world.property_panel.updatePropertyPanel(current) + print(f"树形控件选中项改变: {current.text(0)}") + def onTreeItemClicked(self, item, column): """处理树形控件项目点击事件""" - if not item: + print(f"树形控件点击事件触发,item: {item}, column: {column}") + + # 检查是否点击了空白区域 + # 当点击空白区域时,item可能是一个空的QTreeWidgetItem对象 + if not item or (item.text(0) == "" and item.data(0, Qt.UserRole) is None): self.world.selection.updateSelection(None) + print("点击空白区域,清除选中状态") return self.world.property_panel.updatePropertyPanel(item) @@ -39,15 +68,12 @@ class InterfaceManager: # 更新选择状态 self.world.selected_np = nodePath self.world.selection.updateSelection(nodePath) - - # 更新属性面板 - #self.world.property_panel.updatePropertyPanel(item) - print(f"树形控件点击: {item.text(0)}") else: # 如果没有节点对象,清除选择 self.world.selection.updateSelection(None) - #self.world.property_panel.clearPropertyPanel() + self.world.property_panel.clearPropertyPanel() + print("点击了无数据项,清除选中状态") # def showTreeContextMenu(self, position): # """显示树形控件的右键菜单""" diff --git a/ui/main_window.py b/ui/main_window.py index 3f856329..df48d9fa 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -932,7 +932,6 @@ class MainWindow(QMainWindow): padding-top: 10px; /* 增加顶部内边距,使标题和内容分离 */ } QGroupBox::title { - subline-offset: -2px; padding: 0 8px; color: #c0c0e0; font-weight: 500; @@ -1029,7 +1028,6 @@ class MainWindow(QMainWindow): padding-top: 10px; /* 增加顶部内边距,使标题和内容分离 */ } QGroupBox::title { - subline-offset: -2px; padding: 0 8px; color: #c0c0e0; font-weight: 500; @@ -1139,44 +1137,44 @@ class MainWindow(QMainWindow): self.bottomDock.setWidget(self.fileView) self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) - # # 创建底部停靠控制台 - # self.consoleDock = QDockWidget("控制台", self) - # self.consoleDock.setStyleSheet(""" - # QDockWidget { - # background-color: #252538; - # color: #e0e0ff; - # border: 1px solid #3a3a4a; - # } - # QDockWidget::title { - # background-color: #2d2d44; - # padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ - # border-bottom: 0px solid #3a3a4a; - # } - # QDockWidget::close-button, QDockWidget::float-button { - # background-color: #8b5cf6; - # border: none; - # icon-size: 8px; /* 调整图标大小 */ - # border-radius: 4px; /* 增加圆角 */ - # } - # QDockWidget::close-button:hover, QDockWidget::float-button:hover { - # background-color: #7c3aed; /* 悬停时显示较亮的背景 */ - # } - # QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { - # background-color: #8b5cf6; /* 按下时显示紫色高亮 */ - # } - # """) - # self.consoleView = CustomConsoleDockWidget(self.world) - # # 为控制台添加样式 - # self.consoleView.setStyleSheet(""" - # QTextEdit { - # background-color: #1e1e2e; - # color: #e0e0ff; - # border: 1px solid #3a3a4a; - # font-family: 'Consolas', 'Monaco', monospace; - # } - # """) - # self.consoleDock.setWidget(self.consoleView) - # self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) + # 创建底部停靠控制台 + self.consoleDock = QDockWidget("控制台", self) + self.consoleDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button, QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + """) + self.consoleView = CustomConsoleDockWidget(self.world) + # 为控制台添加样式 + self.consoleView.setStyleSheet(""" + QTextEdit { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + font-family: 'Consolas', 'Monaco', monospace; + } + """) + self.consoleDock.setWidget(self.consoleView) + self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) # 将右侧停靠窗口设为标签形式 # self.tabifyDockWidget(self.rightDock, self.scriptDock) @@ -1186,7 +1184,7 @@ class MainWindow(QMainWindow): self.bottomDock.raise_() self.rightDock.raise_() self.scriptDock.raise_() - # self.consoleDock.raise_() + self.consoleDock.raise_() self.leftDock.raise_() # ========================================================================= # ↓↓↓ 新增代码:为停靠窗口的标签栏(QTabBar)设置统一样式 ↓↓↓ diff --git a/ui/property_panel.py b/ui/property_panel.py index d7262ee6..6b63abac 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -4725,7 +4725,7 @@ class PropertyPanelManager: material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" unique_name = f"{material_name}({model_name})" - print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") + #print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") # 处理重复名称 if unique_name in name_counter: @@ -4764,7 +4764,7 @@ class PropertyPanelManager: # 基础颜色编辑 base_color = self._getOrCreateMaterialBaseColor(material) if base_color is not None: - print(f"材质基础颜色: {base_color}") + #print(f"材质基础颜色: {base_color}") # 基础颜色标题 color_row = 2 if material_status != "标准PBR材质" else 1 @@ -5153,7 +5153,7 @@ class PropertyPanelManager: try: # 方法1: 尝试获取base_color属性 if hasattr(material, 'base_color') and material.base_color is not None: - print(f"✓ 找到base_color属性: {material.base_color}") + #print(f"✓ 找到base_color属性: {material.base_color}") return material.base_color # 方法2: 尝试调用get_base_color方法 @@ -6786,7 +6786,7 @@ class PropertyPanelManager: #print(f"找到匹配的几何节点: {geom_np.get_name()}") return geom_np - print("未找到匹配的几何节点") + #print("未找到匹配的几何节点") return None def _findSpecificGeomNodeForMaterial(self, target_material): @@ -7755,19 +7755,19 @@ class PropertyPanelManager: # 方法1: 直接控制Day Time Editor中的sun_azimuth节点 success = self._updateDayTimeEditorSetting("scattering", "sun_azimuth", azimuth_value) if success: - print(f"✅ 通过Day Time Editor设置方位角: {azimuth_value}°") + #print(f"✅ 通过Day Time Editor设置方位角: {azimuth_value}°") return # 方法2: 使用我们的太阳控制器作为备用 if hasattr(self.world, 'sun_controller'): self.world.sun_controller.set_sun_azimuth(azimuth_value) - print(f"✅ 通过太阳控制器设置方位角: {azimuth_value}°") + #print(f"✅ 通过太阳控制器设置方位角: {azimuth_value}°") return # 方法3: 直接调用主程序的太阳控制方法 if hasattr(self.world, 'setSunAzimuth'): self.world.setSunAzimuth(azimuth_value) - print(f"✅ 通过主程序方法设置方位角: {azimuth_value}°") + #print(f"✅ 通过主程序方法设置方位角: {azimuth_value}°") return print("⚠️ 所有方位角设置方法都不可用") @@ -7783,19 +7783,19 @@ class PropertyPanelManager: # 方法1: 直接控制Day Time Editor中的sun_altitude节点 success = self._updateDayTimeEditorSetting("scattering", "sun_altitude", altitude_value) if success: - print(f"✅ 通过Day Time Editor设置高度角: {altitude_value}°") + #print(f"✅ 通过Day Time Editor设置高度角: {altitude_value}°") return # 方法2: 使用我们的太阳控制器作为备用 if hasattr(self.world, 'sun_controller'): self.world.sun_controller.set_sun_altitude(altitude_value) - print(f"✅ 通过太阳控制器设置高度角: {altitude_value}°") + #print(f"✅ 通过太阳控制器设置高度角: {altitude_value}°") return # 方法3: 直接调用主程序的太阳控制方法 if hasattr(self.world, 'setSunAltitude'): self.world.setSunAltitude(altitude_value) - print(f"✅ 通过主程序方法设置高度角: {altitude_value}°") + #print(f"✅ 通过主程序方法设置高度角: {altitude_value}°") return print("⚠️ 所有高度角设置方法都不可用") @@ -8072,7 +8072,7 @@ class PropertyPanelManager: format_info = self._getModelFormat(origin_model) processed = [] - print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + #print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") for name in anim_names: display_name = name @@ -8106,7 +8106,7 @@ class PropertyPanelManager: display_name = name processed.append((display_name, original_name)) - print(f"[动画分析] {original_name} → {display_name}") + #print(f"[动画分析] {original_name} → {display_name}") return processed @@ -8140,7 +8140,7 @@ class PropertyPanelManager: if frames > 1: valid_anims += 1 total_frames += frames - print(f"[动画分析] '{anim_name}': {frames} 帧") + #print(f"[动画分析] '{anim_name}': {frames} 帧") else: print(f"[动画分析] '{anim_name}': 无有效帧数 ({frames})") except Exception as e: @@ -8165,7 +8165,7 @@ class PropertyPanelManager: if not filepath: return None - print(f"[Actor加载] 尝试加载: {filepath}") + #print(f"[Actor加载] 尝试加载: {filepath}") # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 if filepath.lower().endswith('.fbx'): @@ -8174,13 +8174,13 @@ class PropertyPanelManager: # 其他格式使用标准 Actor 加载 try: import gltf - print(f"[GLTF加载] 尝试加载: {filepath}") + #print(f"[GLTF加载] 尝试加载: {filepath}") # test_actor=Actor(NodePath(gltf._loader.GltfLoader.load_file(filepath,None))) test_actor=Actor(NodePath(gltf.load_model(filepath,None))) anims = test_actor.getAnimNames() test_actor.reparentTo(self.world.render) self._actor_cache[origin_model] = test_actor - print(f"[Actor加载] 标准加载检测到动画: {anims}") + #print(f"[Actor加载] 标准加载检测到动画: {anims}") if not anims: test_actor.cleanup() test_actor.removeNode() @@ -8193,14 +8193,14 @@ class PropertyPanelManager: def _createFBXActor(self, origin_model, filepath): """专门为 FBX 文件创建 Actor,使用转换方式获取真实动画""" try: - print(f"[FBX动画] 开始处理 FBX 动画: {filepath}") + #print(f"[FBX动画] 开始处理 FBX 动画: {filepath}") # 方法1: 尝试转换 FBX 为包含动画的格式 converted_actor = self._convertFBXToActor(filepath) if converted_actor: converted_actor.reparentTo(self.world.render) self._actor_cache[origin_model] = converted_actor - print(f"[FBX动画] 转换成功,动画: {converted_actor.getAnimNames()}") + #print(f"[FBX动画] 转换成功,动画: {converted_actor.getAnimNames()}") return converted_actor # 方法2: 直接加载但进行动画数据修复 @@ -8210,7 +8210,7 @@ class PropertyPanelManager: if fixed_actor and fixed_actor.getAnimNames(): fixed_actor.reparentTo(self.world.render) self._actor_cache[origin_model] = fixed_actor - print(f"[FBX动画] 修复成功,动画: {fixed_actor.getAnimNames()}") + #print(f"[FBX动画] 修复成功,动画: {fixed_actor.getAnimNames()}") return fixed_actor print(f"[FBX动画] 无法获取有效动画数据") diff --git a/ui/widgets.py b/ui/widgets.py index d8cbe44f..5ab3f6fc 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -47,7 +47,6 @@ class NewProjectDialog(QDialog): padding-top: 10px; /* 增加顶部内边距,为标题留出空间 */ } QGroupBox::title { - subline-offset: -2px; padding: 0 8px; color: #c0c0e0; font-weight: 500; @@ -217,7 +216,7 @@ class CustomPanda3DWidget(QPanda3DWidget): event.acceptProposedAction() else: event.ignore() - + def wheelEvent(self, event): """处理滚轮事件""" if event.angleDelta().y() > 0: @@ -1523,9 +1522,7 @@ class CustomTreeWidget(QTreeWidget): self.initData() self.setupUI() # 初始化界面 self.setupContextMenu() # 初始化右键菜单 - self.setupDragDrop() # 设置拖拽功能 - self.original_scales={} self.setStyleSheet(""" @@ -1960,7 +1957,7 @@ class CustomTreeWidget(QTreeWidget): elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: indicator_str = "OnViewport" - print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') + #print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') if event.source() != self: event.ignore() @@ -2192,7 +2189,7 @@ class CustomTreeWidget(QTreeWidget): # 获取对应的Panda3D节点 new_panda_node = new_selection_item.data(0, Qt.UserRole) # 调用您提供的函数来更新选择状态和属性面板 - self.update_selection_and_properties(new_panda_node, new_selection_item) + #self.update_selection_and_properties(new_panda_node, new_selection_item) else: # 如果连根节点都没有了(例如清空场景),则清空选择 self.update_selection_and_properties(None, None)