283 lines
10 KiB
Python
283 lines
10 KiB
Python
#!/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() |