聚焦功能

This commit is contained in:
Hector 2025-09-17 15:42:42 +08:00
parent a8082bd656
commit d3c6665163
14 changed files with 1625 additions and 507 deletions

View File

@ -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):

View File

@ -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):
"""处理鼠标左键释放事件"""

495
core/patrol_system.py Normal file
View File

@ -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("✓ 默认巡检路线已创建")

View File

@ -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():

View File

@ -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()}轴节点不存在")
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()

View File

@ -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未初始化地板将使用基础渲染")

View File

@ -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}")

89
main.py
View File

@ -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模块的对应功能

View File

@ -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()

View File

@ -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):

View File

@ -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):
# """显示树形控件的右键菜单"""

View File

@ -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设置统一样式 ↓↓↓

View File

@ -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动画] 无法获取有效动画数据")

View File

@ -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)