EG/demo/standalone_gizmo_test.py
2025-12-12 16:16:15 +08:00

581 lines
22 KiB
Python
Raw 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 -*-
"""
独立的坐标轴点击测试demo
专门用于测试和调试坐标轴的点击检测和拖拽约束功能
"""
import sys
import math
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Vec3, Vec4, Point3, Point2, LineSegs,
AmbientLight, DirectionalLight, TextNode,
CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode,
CardMaker, Material
)
from direct.task import Task
from direct.gui.DirectGui import DirectFrame, DirectLabel
class GizmoTest(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# 关闭默认的鼠标控制
self.disableMouse()
print("坐标轴测试程序启动")
# 设置背景色
self.setBackgroundColor(0.2, 0.2, 0.3)
# 设置相机位置
self.cam.setPos(10, -20, 10)
self.cam.lookAt(0, 0, 0)
# 添加光照
self.setupLighting()
# 创建参考立方体
self.createReferenceCube()
# 创建坐标轴
self.createGizmo()
# 设置点击检测
self.setupClickDetection()
# 创建信息显示
self.createInfoDisplay()
# 拖拽状态变量
self.isDragging = False
self.dragAxis = None
self.dragStartMousePos = None
self.dragStartCubePos = None
# 高亮状态
self.highlightAxis = None
# 设置事件处理
self.accept("mouse1", self.onMouseClick)
self.accept("mouse1-up", self.onMouseRelease)
self.taskMgr.add(self.mouseTask, "mouseTask")
# 添加颜色测试任务
self.taskMgr.add(self.colorTestTask, "colorTest")
self.test_time = 0
print("=== Gizmo Click Test Demo Started ===")
print("Instructions:")
print("- ONLY click on red/green/blue gizmo axes to drag")
print("- Each axis constrains movement to that direction only")
print("- Click elsewhere should do nothing")
print("- Real-time display of mouse and projection info")
print("- Color test will cycle through colors every 3 seconds")
def setupLighting(self):
"""设置光照"""
# 环境光
alight = AmbientLight('alight')
alight.setColor((0.3, 0.3, 0.3, 1))
alnp = self.render.attachNewNode(alight)
self.render.setLight(alnp)
# 定向光
dlight = DirectionalLight('dlight')
dlight.setColor((0.8, 0.8, 0.8, 1))
dlnp = self.render.attachNewNode(dlight)
dlnp.setHpr(45, -45, 0)
self.render.setLight(dlnp)
def createReferenceCube(self):
"""创建参考立方体"""
# 创建立方体
cm = CardMaker('cube')
cm.setFrame(-1, 1, -1, 1)
# 创建6个面
faces = [
('front', (0, 1, 0), (0, 0, 0), (1, 0, 0, 1)), # 红色前面
('back', (0, -1, 0), (0, 0, 180), (0, 1, 0, 1)), # 绿色后面
('left', (-1, 0, 0), (0, 0, 90), (0, 0, 1, 1)), # 蓝色左面
('right', (1, 0, 0), (0, 0, -90), (1, 1, 0, 1)), # 黄色右面
('top', (0, 0, 1), (-90, 0, 0), (1, 0, 1, 1)), # 紫色顶面
('bottom', (0, 0, -1), (90, 0, 0), (0, 1, 1, 1)) # 青色底面
]
self.cube = self.render.attachNewNode("cube")
for name, pos, hpr, color in faces:
face = self.cube.attachNewNode(cm.generate())
face.setPos(*pos)
face.setHpr(*hpr)
face.setColor(*color)
face.setName(name)
print(f"✓ 参考立方体创建完成,位置: {self.cube.getPos()}")
def createGizmo(self):
"""创建坐标轴"""
self.gizmo = self.render.attachNewNode("gizmo")
self.gizmo.setPos(0, 0, 3) # 放在立方体上方
self.axis_length = 3.0
self.click_threshold = 25 # 点击检测阈值(像素)
# 创建X轴不在LineSegs级别设置颜色
x_lines = LineSegs("x_axis")
x_lines.setThickness(6.0)
# 不在LineSegs级别设置颜色
x_lines.moveTo(0, 0, 0)
x_lines.drawTo(self.axis_length, 0, 0)
# 箭头
x_lines.moveTo(self.axis_length - 0.3, -0.1, 0)
x_lines.drawTo(self.axis_length, 0, 0)
x_lines.drawTo(self.axis_length - 0.3, 0.1, 0)
x_geom = x_lines.create()
self.gizmoXAxis = self.gizmo.attachNewNode(x_geom)
self.gizmoXAxis.setLightOff()
# 使用RenderState强制设置颜色
from panda3d.core import RenderState, ColorAttrib
red_state = RenderState.make(ColorAttrib.makeFlat((1, 0, 0, 1)))
self.gizmoXAxis.setState(red_state)
self.gizmoXAxis.setColor(1, 0, 0, 1)
# 创建Y轴不在LineSegs级别设置颜色
y_lines = LineSegs("y_axis")
y_lines.setThickness(6.0)
# 不在LineSegs级别设置颜色
y_lines.moveTo(0, 0, 0)
y_lines.drawTo(0, self.axis_length, 0)
# 箭头
y_lines.moveTo(-0.1, self.axis_length - 0.3, 0)
y_lines.drawTo(0, self.axis_length, 0)
y_lines.drawTo(0.1, self.axis_length - 0.3, 0)
y_geom = y_lines.create()
self.gizmoYAxis = self.gizmo.attachNewNode(y_geom)
self.gizmoYAxis.setLightOff()
# 使用RenderState强制设置颜色
green_state = RenderState.make(ColorAttrib.makeFlat((0, 1, 0, 1)))
self.gizmoYAxis.setState(green_state)
self.gizmoYAxis.setColor(0, 1, 0, 1)
# 创建Z轴不在LineSegs级别设置颜色
z_lines = LineSegs("z_axis")
z_lines.setThickness(6.0)
# 不在LineSegs级别设置颜色
z_lines.moveTo(0, 0, 0)
z_lines.drawTo(0, 0, self.axis_length)
# 箭头
z_lines.moveTo(-0.1, 0, self.axis_length - 0.3)
z_lines.drawTo(0, 0, self.axis_length)
z_lines.drawTo(0.1, 0, self.axis_length - 0.3)
z_geom = z_lines.create()
self.gizmoZAxis = self.gizmo.attachNewNode(z_geom)
self.gizmoZAxis.setLightOff()
# 使用RenderState强制设置颜色
blue_state = RenderState.make(ColorAttrib.makeFlat((0, 0, 1, 1)))
self.gizmoZAxis.setState(blue_state)
self.gizmoZAxis.setColor(0, 0, 1, 1)
# 轴颜色字典
self.axis_colors = {
"x": (1, 0, 0, 1), # 红色
"y": (0, 1, 0, 1), # 绿色
"z": (0, 0, 1, 1) # 蓝色
}
print(f"✓ 坐标轴创建完成,长度={self.axis_length}")
print(f"X轴节点颜色: {self.gizmoXAxis.getColor()}")
print(f"Y轴节点颜色: {self.gizmoYAxis.getColor()}")
print(f"Z轴节点颜色: {self.gizmoZAxis.getColor()}")
def setupClickDetection(self):
"""设置点击检测"""
self.picker = CollisionTraverser()
self.pq = CollisionHandlerQueue()
self.pickerNode = CollisionNode('mouseRay')
self.pickerNP = self.camera.attachNewNode(self.pickerNode)
self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
self.picker.addCollider(self.pickerNP, self.pq)
def createInfoDisplay(self):
"""创建信息显示面板"""
self.infoPanel = DirectFrame(
frameColor=(0, 0, 0, 0.7),
frameSize=(-0.4, 0.4, -0.3, 0.3),
pos=(-0.6, 0, 0.8)
)
self.infoText = DirectLabel(
parent=self.infoPanel,
text="Mouse Info\nWaiting...",
text_scale=0.05,
text_fg=(1, 1, 1, 1),
frameColor=(0, 0, 0, 0),
pos=(0, 0, 0)
)
def worldToScreen(self, world_pos):
"""将世界坐标转换为屏幕坐标"""
try:
# 转换为相机坐标系
cam_pos = self.cam.getRelativePoint(self.render, world_pos)
# 检查是否在相机前方
if cam_pos.getY() <= 0:
return None
# 使用相机lens进行投影
screen_pos = Point2()
if self.cam.node().getLens().project(cam_pos, screen_pos):
# 转换为像素坐标
win_x = (screen_pos.getX() + 1.0) * 0.5 * self.win.getXSize()
win_y = (1.0 - screen_pos.getY()) * 0.5 * self.win.getYSize()
return (win_x, win_y)
return None
except:
return None
def distanceToLine(self, point, line_start, line_end):
"""计算点到线段的距离"""
try:
px, py = point
x1, y1 = line_start
x2, y2 = line_end
# 计算线段长度
line_length_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2
if line_length_sq < 0.001:
return math.sqrt((px - x1) ** 2 + (py - y1) ** 2)
# 计算投影参数
t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_length_sq))
# 计算投影点
projection_x = x1 + t * (x2 - x1)
projection_y = y1 + t * (y2 - y1)
# 返回距离
distance = math.sqrt((px - projection_x) ** 2 + (py - projection_y) ** 2)
return distance
except:
return float('inf')
def checkGizmoClick(self, mouse_x, mouse_y):
"""检查是否点击了坐标轴"""
if not self.gizmo:
return None
# 获取坐标轴中心的世界坐标
gizmo_world_pos = self.gizmo.getPos(self.render)
# 计算各轴端点的世界坐标
x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0)
y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0)
z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length)
# 投影到屏幕坐标
center_screen = self.worldToScreen(gizmo_world_pos)
x_screen = self.worldToScreen(x_end)
y_screen = self.worldToScreen(y_end)
z_screen = self.worldToScreen(z_end)
if not center_screen:
return None
# 检测各个轴
axes_data = [
("x", x_screen, "X轴"),
("y", y_screen, "Y轴"),
("z", z_screen, "Z轴")
]
closest_axis = None
closest_distance = float('inf')
for axis_name, axis_screen, axis_label in axes_data:
if axis_screen:
distance = self.distanceToLine(
(mouse_x, mouse_y), center_screen, axis_screen
)
if distance < self.click_threshold and distance < closest_distance:
closest_axis = axis_name
closest_distance = distance
return closest_axis
def setAxisColor(self, axis, color):
"""设置坐标轴颜色 - 使用RenderState强制覆盖"""
from panda3d.core import RenderState, ColorAttrib
# 创建强制颜色状态
color_state = RenderState.make(ColorAttrib.makeFlat(color))
if axis == "x" and self.gizmoXAxis:
self.gizmoXAxis.setState(color_state)
self.gizmoXAxis.setColor(*color)
self.gizmoXAxis.setColorScale(*color)
print(f"✓ 强制设置X轴颜色: {color}")
elif axis == "y" and self.gizmoYAxis:
self.gizmoYAxis.setState(color_state)
self.gizmoYAxis.setColor(*color)
self.gizmoYAxis.setColorScale(*color)
print(f"✓ 强制设置Y轴颜色: {color}")
elif axis == "z" and self.gizmoZAxis:
self.gizmoZAxis.setState(color_state)
self.gizmoZAxis.setColor(*color)
self.gizmoZAxis.setColorScale(*color)
print(f"✓ 强制设置Z轴颜色: {color}")
def mouseTask(self, task):
"""鼠标任务 - 处理高亮和拖拽"""
if not self.mouseWatcherNode.hasMouse():
return task.cont
# 获取鼠标屏幕坐标
mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize()
mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize()
# 检查是否在颜色测试周期内
cycle_time = int(self.test_time) % 15
is_color_testing = (cycle_time >= 3 and cycle_time < 12) # 在黄、白、橙色测试期间
# 更新信息显示
info_text = f"Mouse Info:\n"
info_text += f"Normalized: ({self.mouseWatcherNode.getMouseX():.3f}, {self.mouseWatcherNode.getMouseY():.3f})\n"
info_text += f"Pixels: ({mouse_x:.1f}, {mouse_y:.1f})\n"
info_text += f"Window Size: {self.win.getXSize()} x {self.win.getYSize()}\n"
if is_color_testing:
info_text += f"Color Test Active (cycle: {cycle_time})\n"
# 只在不拖拽且不在颜色测试时检测高亮
if not self.isDragging and not is_color_testing:
# 简化的高亮检测
hovered_axis = None
if self.gizmo:
# 获取坐标轴中心的世界坐标
gizmo_world_pos = self.gizmo.getPos(self.render)
# 计算各轴端点的世界坐标
x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0)
y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0)
z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length)
# 投影到屏幕坐标
center_screen = self.worldToScreen(gizmo_world_pos)
x_screen = self.worldToScreen(x_end)
y_screen = self.worldToScreen(y_end)
z_screen = self.worldToScreen(z_end)
if center_screen:
# 检测距离最近的轴
min_distance = float('inf')
for axis_name, axis_screen in [("x", x_screen), ("y", y_screen), ("z", z_screen)]:
if axis_screen:
distance = self.distanceToLine((mouse_x, mouse_y), center_screen, axis_screen)
if distance < self.click_threshold and distance < min_distance:
min_distance = distance
hovered_axis = axis_name
# 处理高亮状态变化
if hovered_axis != self.highlightAxis:
# 恢复之前高亮的轴
if self.highlightAxis:
self.setAxisColor(self.highlightAxis, self.axis_colors[self.highlightAxis])
print(f"恢复 {self.highlightAxis} 轴原色")
# 高亮新的轴
if hovered_axis:
self.setAxisColor(hovered_axis, (1, 1, 0, 1)) # 纯黄色高亮
print(f"高亮 {hovered_axis} 轴为黄色")
self.highlightAxis = hovered_axis
# 更新信息显示
if hovered_axis:
info_text += f"Hover: {hovered_axis.upper()} axis (YELLOW)"
else:
info_text += "Hover: None"
elif is_color_testing:
info_text += "Hover: Disabled (Color Test)"
else:
info_text += f"Dragging: {self.dragAxis.upper()} axis"
# 处理拖拽
if self.isDragging:
self.handleDrag(mouse_x, mouse_y)
self.infoText.setText(info_text)
return task.cont
def onMouseClick(self):
"""鼠标点击事件 - 只有点击坐标轴才开始拖拽"""
if not self.mouseWatcherNode.hasMouse():
return
# 检查是否在颜色测试期间
cycle_time = int(self.test_time) % 15
is_color_testing = (cycle_time >= 3 and cycle_time < 12)
if is_color_testing:
print("点击被禁用 - 颜色测试进行中")
return
# 获取鼠标坐标
mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize()
mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize()
print(f"\n=== Mouse Click ===")
print(f"Pixel Coords: ({mouse_x:.1f}, {mouse_y:.1f})")
# 检测坐标轴点击
clicked_axis = self.checkGizmoClick(mouse_x, mouse_y)
if clicked_axis:
print(f"✓ Clicked {clicked_axis.upper()} Axis - Starting drag")
# 只有真正点击到轴才开始拖拽
self.isDragging = True
self.dragAxis = clicked_axis
self.dragStartMousePos = (mouse_x, mouse_y)
self.dragStartCubePos = self.cube.getPos()
# 显示拖拽颜色 - 使用更明显的橙色
self.setAxisColor(clicked_axis, (1, 0.5, 0, 1)) # 更亮的橙色表示拖拽
print(f" 开始位置: {self.dragStartCubePos}")
else:
print("✗ No axis clicked - No action")
# 确保没有点击到轴时不开始拖拽
self.isDragging = False
self.dragAxis = None
def onMouseRelease(self):
"""鼠标释放事件"""
if self.isDragging and self.dragAxis:
print(f"Stop dragging {self.dragAxis.upper()} axis")
# 恢复轴颜色
self.setAxisColor(self.dragAxis, self.axis_colors[self.dragAxis])
# 清理所有拖拽状态
self.isDragging = False
self.dragAxis = None
self.dragStartMousePos = None
self.dragStartCubePos = None
def handleDrag(self, mouse_x, mouse_y):
"""处理拖拽 - 真正的轴约束拖拽"""
if not self.dragAxis or not self.dragStartMousePos or not self.dragStartCubePos:
return
# 计算从拖拽开始到现在的总鼠标移动距离
total_dx = mouse_x - self.dragStartMousePos[0]
total_dy = mouse_y - self.dragStartMousePos[1]
# 根据拖拽的轴,确定世界空间中的轴方向向量
if self.dragAxis == "x":
world_axis_dir = Vec3(1, 0, 0) # X轴方向
elif self.dragAxis == "y":
world_axis_dir = Vec3(0, 1, 0) # Y轴方向
elif self.dragAxis == "z":
world_axis_dir = Vec3(0, 0, 1) # Z轴方向
else:
return
# 将世界轴方向投影到屏幕空间
axis_start_world = self.dragStartCubePos
axis_end_world = self.dragStartCubePos + world_axis_dir
axis_start_screen = self.worldToScreen(axis_start_world)
axis_end_screen = self.worldToScreen(axis_end_world)
if not axis_start_screen or not axis_end_screen:
return
# 计算轴在屏幕空间的方向向量
screen_axis_dir = (
axis_end_screen[0] - axis_start_screen[0],
axis_end_screen[1] - axis_start_screen[1]
)
# 归一化屏幕轴方向
screen_axis_length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2)
if screen_axis_length > 0.001:
screen_axis_dir = (
screen_axis_dir[0] / screen_axis_length,
screen_axis_dir[1] / screen_axis_length
)
else:
return
# 将总鼠标移动向量投影到屏幕轴方向上
projected_distance = (
total_dx * screen_axis_dir[0] +
total_dy * screen_axis_dir[1]
)
# 将投影距离转换为世界空间移动量
move_scale = 0.02 # 移动灵敏度
world_movement = world_axis_dir * projected_distance * move_scale
# 从拖拽开始位置计算新位置(确保只沿选定轴移动)
new_cube_pos = self.dragStartCubePos + world_movement
self.cube.setPos(new_cube_pos)
# 坐标轴跟随立方体移动
self.gizmo.setPos(new_cube_pos + Vec3(0, 0, 3))
def colorTestTask(self, task):
"""颜色测试任务 - 每3秒循环测试坐标轴颜色"""
from panda3d.core import RenderState, ColorAttrib
self.test_time += globalClock.getDt()
# 每3秒切换一次测试
cycle_time = int(self.test_time) % 15 # 15秒一个完整周期
if cycle_time < 3:
# 正常颜色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 0, 0, 1)) # 红色
self.setAxisColor("y", (0, 1, 0, 1)) # 绿色
self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色
elif cycle_time < 6:
# 全部黄色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 1, 0, 1))
self.setAxisColor("y", (1, 1, 0, 1))
self.setAxisColor("z", (1, 1, 0, 1))
elif cycle_time < 9:
# 全部白色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 1, 1, 1))
self.setAxisColor("y", (1, 1, 1, 1))
self.setAxisColor("z", (1, 1, 1, 1))
elif cycle_time < 12:
# 全部橙色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 0.5, 0, 1))
self.setAxisColor("y", (1, 0.5, 0, 1))
self.setAxisColor("z", (1, 0.5, 0, 1))
else:
# 恢复正常颜色
if hasattr(self, 'gizmoXAxis'):
self.setAxisColor("x", (1, 0, 0, 1)) # 红色
self.setAxisColor("y", (0, 1, 0, 1)) # 绿色
self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色
return task.cont
if __name__ == "__main__":
app = GizmoTest()
app.run()