#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 生产级屏幕投影点击检测系统 适用于Qt嵌入的Panda3D环境 """ from panda3d.core import Point3, Point2, Vec3 from typing import List, Optional, Tuple, Dict, Any import math class ScreenProjectionPicker: def __init__(self, camera, lens, window): """ 初始化屏幕投影拾取器 Args: camera: Panda3D相机节点 lens: 相机镜头 window: 渲染窗口 """ self.camera = camera self.lens = lens self.window = window self.objects = [] # 可选中的对象列表 def registerObject(self, node_path, name: str, priority: int = 0, custom_radius: Optional[float] = None): """ 注册可选中的对象 Args: node_path: Panda3D节点路径 name: 对象名称 priority: 选择优先级(数值越大优先级越高) custom_radius: 自定义选择半径(世界坐标) """ obj_info = { 'node': node_path, 'name': name, 'priority': priority, 'custom_radius': custom_radius, 'world_pos': node_path.getPos(), 'bounds': node_path.getBounds() } self.objects.append(obj_info) def unregisterObject(self, node_path): """取消注册对象""" self.objects = [obj for obj in self.objects if obj['node'] != node_path] def updateObjectPositions(self): """更新所有对象的世界位置(在对象移动后调用)""" for obj_info in self.objects: obj_info['world_pos'] = obj_info['node'].getPos() obj_info['bounds'] = obj_info['node'].getBounds() def worldToScreen(self, world_pos: Point3) -> Optional[Tuple[float, float]]: """ 将世界坐标转换为屏幕像素坐标 Args: world_pos: 世界坐标位置 Returns: (pixel_x, pixel_y) 或 None(如果不在屏幕内) """ # 转换到相机坐标系 cam_pos = self.camera.getRelativePoint(self.camera.getParent(), world_pos) # 投影到屏幕坐标 screen_point = Point2() if self.lens.project(cam_pos, screen_point): # 转换为像素坐标 win_width = self.window.getXSize() win_height = self.window.getYSize() pixel_x = (screen_point.getX() + 1.0) * win_width * 0.5 pixel_y = (1.0 - screen_point.getY()) * win_height * 0.5 return pixel_x, pixel_y return None def getObjectScreenRadius(self, obj_info: Dict) -> float: """ 计算对象在屏幕上的选择半径 Args: obj_info: 对象信息字典 Returns: 屏幕像素半径 """ world_pos = obj_info['world_pos'] # 如果有自定义半径,使用自定义半径 if obj_info['custom_radius'] is not None: world_radius = obj_info['custom_radius'] else: # 否则根据包围盒计算半径 bounds = obj_info['bounds'] if bounds.isEmpty(): world_radius = 1.0 # 默认半径 else: # 使用包围盒的最大尺寸作为半径 min_point = bounds.getMin() max_point = bounds.getMax() size = max_point - min_point world_radius = max(size.getX(), size.getY(), size.getZ()) * 0.5 # 计算屏幕半径 center_screen = self.worldToScreen(world_pos) if not center_screen: return 0 # 使用对象右侧边缘点计算屏幕半径 edge_pos = world_pos + Vec3(world_radius, 0, 0) edge_screen = self.worldToScreen(edge_pos) if edge_screen: return abs(edge_screen[0] - center_screen[0]) # 如果边缘点不可见,使用距离相机的距离估算 cam_distance = (world_pos - self.camera.getPos()).length() if cam_distance > 0: # 基于透视投影的简单估算 angular_size = world_radius / cam_distance fov = self.lens.getHfov() # 水平视场角 win_width = self.window.getXSize() screen_radius = angular_size * (win_width * 0.5) / math.tan(math.radians(fov * 0.5)) return max(10.0, screen_radius) # 最小10像素 return 20.0 # 默认半径 def pickObject(self, mouse_x: int, mouse_y: int, tolerance_multiplier: float = 1.0) -> Optional[Dict]: """ 在指定鼠标位置选择对象 Args: mouse_x: 鼠标X坐标(像素) mouse_y: 鼠标Y坐标(像素) tolerance_multiplier: 容差倍数(增大可使选择更容易) Returns: 选中的对象信息字典,或None """ candidates = [] for obj_info in self.objects: # 计算对象在屏幕上的位置 screen_pos = self.worldToScreen(obj_info['world_pos']) if not screen_pos: continue # 对象不在屏幕内 screen_x, screen_y = screen_pos # 计算屏幕半径 screen_radius = self.getObjectScreenRadius(obj_info) # 应用容差倍数 effective_radius = screen_radius * tolerance_multiplier # 计算鼠标到对象中心的距离 dx = mouse_x - screen_x dy = mouse_y - screen_y distance = math.sqrt(dx * dx + dy * dy) # 检查是否在选择范围内 if distance <= effective_radius: # 计算选择分数(距离越近、优先级越高分数越高) distance_score = 1.0 - (distance / effective_radius) priority_score = obj_info['priority'] * 10.0 total_score = distance_score + priority_score candidates.append((obj_info, distance, total_score)) if not candidates: return None # 按分数排序,选择最高分的对象 candidates.sort(key=lambda x: x[2], reverse=True) return candidates[0][0] def pickMultipleObjects(self, mouse_x: int, mouse_y: int, tolerance_multiplier: float = 1.0) -> List[Dict]: """ 在指定位置选择所有可能的对象(用于上下文菜单等) Args: mouse_x: 鼠标X坐标 mouse_y: 鼠标Y坐标 tolerance_multiplier: 容差倍数 Returns: 按优先级排序的对象列表 """ candidates = [] for obj_info in self.objects: screen_pos = self.worldToScreen(obj_info['world_pos']) if not screen_pos: continue screen_x, screen_y = screen_pos screen_radius = self.getObjectScreenRadius(obj_info) effective_radius = screen_radius * tolerance_multiplier dx = mouse_x - screen_x dy = mouse_y - screen_y distance = math.sqrt(dx * dx + dy * dy) if distance <= effective_radius: distance_score = 1.0 - (distance / effective_radius) priority_score = obj_info['priority'] * 10.0 total_score = distance_score + priority_score candidates.append((obj_info, distance, total_score)) # 按分数排序 candidates.sort(key=lambda x: x[2], reverse=True) return [candidate[0] for candidate in candidates] def getDebugInfo(self, mouse_x: int, mouse_y: int) -> str: """获取调试信息""" debug_lines = [f"鼠标位置: ({mouse_x}, {mouse_y})"] debug_lines.append(f"注册对象数量: {len(self.objects)}") debug_lines.append("") for obj_info in self.objects: screen_pos = self.worldToScreen(obj_info['world_pos']) if screen_pos: screen_x, screen_y = screen_pos screen_radius = self.getObjectScreenRadius(obj_info) dx = mouse_x - screen_x dy = mouse_y - screen_y distance = math.sqrt(dx * dx + dy * dy) debug_lines.append(f"{obj_info['name']}:") debug_lines.append(f" 世界位置: {obj_info['world_pos']}") debug_lines.append(f" 屏幕位置: ({screen_x:.1f}, {screen_y:.1f})") debug_lines.append(f" 屏幕半径: {screen_radius:.1f}") debug_lines.append(f" 鼠标距离: {distance:.1f}") debug_lines.append(f" 可选中: {'是' if distance <= screen_radius else '否'}") debug_lines.append("") else: debug_lines.append(f"{obj_info['name']}: 不在屏幕内") debug_lines.append("") return "\n".join(debug_lines) # 使用示例 def example_usage(): """使用示例""" print(""" # 在您的应用中使用屏幕投影拾取器: # 1. 初始化拾取器 picker = ScreenProjectionPicker(self.cam, self.cam.node().getLens(), self.win) # 2. 注册可选中的对象 picker.registerObject(cube_node, "立方体", priority=1) picker.registerObject(sphere_node, "球体", priority=2, custom_radius=3.0) # 3. 在鼠标点击事件中使用 def on_mouse_click(mouse_x, mouse_y): selected_obj = picker.pickObject(mouse_x, mouse_y, tolerance_multiplier=1.2) if selected_obj: print(f"选中了: {selected_obj['name']}") else: print("没有选中任何对象") # 4. 对象移动后更新位置 picker.updateObjectPositions() # 5. 获取调试信息 debug_info = picker.getDebugInfo(mouse_x, mouse_y) print(debug_info) """) if __name__ == "__main__": example_usage()