EG/demo/production_picking_system.py
2025-07-02 09:49:59 +08:00

283 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()