Merge pull request 'addRender' (#22) from addRender into main

Reviewed-on: Hector/EG#22
This commit is contained in:
Hector 2025-08-28 07:52:51 +00:00
commit fc846e296d
7 changed files with 715 additions and 297 deletions

View File

@ -122,7 +122,7 @@ class EventHandler:
if self.world.currentTool == "地形编辑":
self._handleTerrainEdit(evt,"add")
return
# 获取鼠标点击的位置
x = evt.get('x', 0)
y = evt.get('y', 0)
@ -264,6 +264,154 @@ class EventHandler:
print("=== 鼠标左键事件处理结束 ===\n")
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.bit(2))
# 使用相机坐标系的点创建射线
direction = farPoint - nearPoint
direction.normalize()
pickerNode.addSolid(CollisionRay(nearPoint, direction))
picker.addCollider(pickerNP, queue)
picker.traverse(self.world.render)
print(f"地形碰撞检测结果数量: {queue.getNumEntries()}")
# 添加调试信息,显示所有场景中的碰撞体
print("场景中的碰撞体:")
def show_colliders(node, level=0):
indent = " " * level
if not node.isEmpty():
if isinstance(node.node(), CollisionNode):
print(f"{indent}CollisionNode: {node.getName()}")
for child in node.getChildren():
show_colliders(child, level + 1)
show_colliders(self.world.render)
# 射线检测结果处理
hitPos = None
hitNode = None
hitTerrainInfo = None
if queue.getNumEntries() > 0:
queue.sortEntries()
# 遍历所有碰撞结果,找到地形节点
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
hitTerrainInfo = terrain_info
print(f"找到地形节点: {terrain_node.getName()}")
break
if hitPos:
break
if hitPos and hitTerrainInfo:
# 修改地形高度
x_pos, y_pos = hitPos.getX(), hitPos.getY()
# 使用 getattr 获取地形编辑参数
radius = getattr(self.world, 'terrain_edit_radius', 3.0)
strength = getattr(self.world, 'terrain_edit_strength', 0.3)
print(f"准备编辑地形: 位置({x_pos:.2f}, {y_pos:.2f}), 半径{radius}, 强度{strength}, 操作{operation}")
# 添加更多调试信息
print(f"地形信息: {hitTerrainInfo}")
if 'heightmap' in hitTerrainInfo:
print(f"高度图路径: {hitTerrainInfo['heightmap']}")
try:
# 检查 terrain_manager 是否有 modifyTerrainHeight 方法
if not hasattr(self.world.terrain_manager, 'modifyTerrainHeight'):
print("✗ 错误: terrain_manager 没有 modifyTerrainHeight 方法")
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
return
# 调用地形编辑方法
success = self.world.terrain_manager.modifyTerrainHeight(
hitTerrainInfo, x_pos, y_pos, radius=radius, strength=strength, operation=operation)
if success:
print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})")
# 显示射线
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
# 强制刷新地形显示
if 'terrain' in hitTerrainInfo:
hitTerrainInfo['terrain'].generate()
print("✓ 地形已刷新")
else:
print("✗ 地形编辑失败")
# 即使编辑失败也显示射线
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
except Exception as e:
print(f"✗ 地形编辑过程中出现异常: {e}")
import traceback
traceback.print_exc()
# 即使编辑失败也显示射线
self.showClickRay(worldNearPoint, worldFarPoint, hitPos)
else:
print("没有检测到地形碰撞")
# 显示射线(无碰撞)
self.showClickRay(worldNearPoint, worldFarPoint)
# 清理碰撞检测节点
pickerNP.removeNode()
print("地形编辑处理完成")
except Exception as e:
print(f"地形编辑处理出错: {e}")
import traceback
traceback.print_exc()
def mousePressEventRight(self,evt):
"""处理鼠标右键按下事件"""
print(f"当前工具: {self.world.currentTool}")

View File

@ -1,4 +1,5 @@
# core/terrain_manager.py
import time
from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath
from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight
import os
@ -22,7 +23,7 @@ class TerrainManager:
try:
# 创建GeoMipTerrain对象
terrain_name = f"terrain_{len(self.terrains)}"
terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain = GeoMipTerrain(terrain_name)
# 加载高度图
@ -33,7 +34,7 @@ class TerrainManager:
print(f"原始图像尺寸: {width}x{height}")
# 找到最接近的有效尺寸
valid_sizes = [17, 33, 65, 129, 257, 513, 1025]
valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049]
target_size = 129 # 默认尺寸
# 选择最接近的尺寸
@ -48,6 +49,7 @@ class TerrainManager:
# 如果需要,调整图像尺寸
if width != target_size or height != target_size:
print(f"调整图像尺寸从 {width}x{height}{target_size}x{target_size}")
# 使用正确的图像缩放方法
resized_image = PNMImage(target_size, target_size)
resized_image.quickFilterFrom(height_image)
height_image = resized_image
@ -56,7 +58,10 @@ class TerrainManager:
terrain.setHeightfield(height_image)
# 设置地形参数
terrain.setBruteforce(False) # 使用LOD
terrain.setBruteforce(True) # 使用LOD
terrain.setBlockSize(32)
# terrain.setNearFarThreshold(50.0,200.0)
# 生成地形
terrain.generate()
@ -64,21 +69,34 @@ class TerrainManager:
# 获取地形节点
terrain_node = terrain.getRoot()
if terrain_node.isEmpty():
print("错误:无法生成有效的地形节点")
return None
node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}"
terrain_node.setName(node_name)
center_offset = (target_size - 1) / 2
terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5)
# 设置缩放
terrain_node.setScale(scale[0], scale[1], scale[2])
# 将地形添加到场景中
terrain_node.reparentTo(self.world.render)
# 为地形节点设置名称,便于在场景树中识别
terrain_node.setName(f"Terrain_{os.path.basename(heightmap_path)}")
from panda3d.core import BitMask32
# 设置地形节点的碰撞掩码
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
# 为地形的所有子节点也设置碰撞掩码
for child in terrain_node.getChildren():
child.setCollideMask(BitMask32.bit(2))
# 添加材质
self._applyTerrainMaterial(terrain_node)
terrain_node.setPythonTag("selectable", True)
# 保存地形信息(包括高度图的副本)
terrain_info = {
'terrain': terrain,
@ -86,14 +104,18 @@ class TerrainManager:
'heightmap': heightmap_path,
'heightfield': height_image, # 保存高度图副本
'scale': scale,
'name': f"Terrain_{os.path.basename(heightmap_path)}"
'name': node_name
}
self.terrains.append(terrain_info)
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
# 更新场景树(再次检查节点是否有效)
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
try:
self.world.scene_manager.updateSceneTree()
except Exception as e:
print(f"警告: 更新场景树时出错: {e}")
print(f"✓ 成功从 {heightmap_path} 创建地形")
return terrain_info
@ -104,7 +126,7 @@ class TerrainManager:
traceback.print_exc()
return None
def createFlatTerrain(self, size=(100, 100), resolution=129):
def createFlatTerrain(self, size=(0.3, 0.3), resolution=129):
"""创建平面地形"""
try:
# 确保分辨率是2的幂次方加1 (如129, 257, 513等)
@ -116,7 +138,7 @@ class TerrainManager:
resolution = closest_res
# 创建GeoMipTerrain对象
terrain_name = f"flat_terrain_{len(self.terrains)}"
terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain = GeoMipTerrain(terrain_name)
# 创建一个平面高度图尺寸必须是2^n+1
@ -125,7 +147,7 @@ class TerrainManager:
# 使用正确的方法设置高度图
terrain.setHeightfield(height_image)
terrain.setBruteforce(False) # 使用LOD
terrain.setBruteforce(True) # 设置LOD
# 设置地形参数
terrain.setBlockSize(32) # 设置块大小
@ -136,17 +158,26 @@ class TerrainManager:
# 获取地形节点
terrain_node = terrain.getRoot()
# 设置缩放以达到指定大小
terrain_node.setScale(size[0], size[1], 50) # Z轴缩放更大以显示地形起伏
if terrain_node.isEmpty():
print("错误:无法生成有效的平面地形节点")
return None
node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}"
terrain_node.setName(node_name)
center_offset = (resolution - 1) / 2
terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0)
# 将地形添加到场景中
terrain_node.reparentTo(self.world.render)
# 为地形节点设置名称,便于在场景树中识别
terrain_node.setName(f"FlatTerrain_{len(self.terrains)}")
# 为地形添加碰撞体
from panda3d.core import BitMask32
# 设置地形节点的碰撞掩码
terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码
# 为地形的所有子节点也设置碰撞掩码
for child in terrain_node.getChildren():
child.setCollideMask(BitMask32.bit(2))
# 添加材质
self._applyTerrainMaterial(terrain_node)
@ -158,14 +189,18 @@ class TerrainManager:
'heightmap': None,
'heightfield': height_image, # 保存高度图
'scale': (size[0], size[1], 50),
'name': f"FlatTerrain_{len(self.terrains)}"
'name': node_name
}
self.terrains.append(terrain_info)
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
# 更新场景树(再次检查节点是否有效)
if not terrain_node.isEmpty() and hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
try:
self.world.scene_manager.updateSceneTree()
except Exception as e:
print(f"警告: 更新场景树时出错: {e}")
print(f"✓ 成功创建平面地形,大小: {size}, 分辨率: {resolution}")
return terrain_info
@ -190,7 +225,7 @@ class TerrainManager:
terrain_node.setMaterial(material)
# 启用材质
terrain_node.setAttrib(ColorAttrib.makeDefaultColor())
terrain_node.setAttrib(ColorAttrib.makeDefault())
print("✓ 地形材质已应用")
except Exception as e:
@ -227,6 +262,7 @@ class TerrainManager:
try:
terrain = terrain_info['terrain']
terrain_node = terrain_info['node'] # 获取地形节点
# 直接使用我们保存的高度图数据
if 'heightfield' not in terrain_info:
@ -242,19 +278,58 @@ class TerrainManager:
width = heightmap.getXSize()
height = heightmap.getYSize()
# 转换坐标到高度图空间
center_x = int((x / terrain_info['scale'][0]) * width + width / 2)
center_y = int((y / terrain_info['scale'][1]) * height + height / 2)
# 获取地形节点信息
terrain_pos = terrain_node.getPos()
terrain_scale = terrain_node.getScale()
# 正确的坐标转换方法
# 1. 将世界坐标转换为相对于地形中心的坐标
relative_x = x - terrain_pos.getX()
relative_y = y - terrain_pos.getY()
# 2. 考虑地形的缩放
scaled_x = relative_x / terrain_scale.getX()
scaled_y = relative_y / terrain_scale.getY()
# 3. 转换为高度图坐标
center_offset = (width - 1) / 2
center_x = int(scaled_x + center_offset)
center_y = int(scaled_y + center_offset)
print(f"坐标转换详情:")
print(f" 世界坐标: ({x:.2f}, {y:.2f})")
print(f" 地形位置: ({terrain_pos.getX():.2f}, {terrain_pos.getY():.2f})")
print(f" 地形缩放: ({terrain_scale.getX():.2f}, {terrain_scale.getY():.2f})")
print(f" 相对坐标: ({relative_x:.2f}, {relative_y:.2f})")
print(f" 缩放坐标: ({scaled_x:.2f}, {scaled_y:.2f})")
print(f" 中心偏移: {center_offset}")
print(f" 高度图坐标: ({center_x}, {center_y})")
print(f" 高度图尺寸: {width} x {height}")
# 检查中心点是否在有效范围内
if not (0 <= center_x < width and 0 <= center_y < height):
print(f"❌ 中心点坐标超出范围: ({center_x}, {center_y})")
print(f"❌ 有效范围: (0-{width - 1}, 0-{height - 1})")
return False
# 修改半径范围内的点
modified = False
for dx in range(-int(radius), int(radius) + 1):
for dy in range(-int(radius), int(radius) + 1):
# 使用正确的半径计算(考虑地形缩放)
effective_radius = max(1, int(radius / max(terrain_scale.getX(), terrain_scale.getY())))
print(f"有效编辑半径: {effective_radius}")
# 限制编辑半径不超过地形尺寸的1/4避免性能问题
effective_radius = min(effective_radius, width // 4, height // 4)
for dx in range(-effective_radius, effective_radius + 1):
for dy in range(-effective_radius, effective_radius + 1):
# 检查距离
distance = (dx * dx + dy * dy) ** 0.5
if distance <= radius:
if distance <= effective_radius:
# 计算影响权重
weight = 1.0 - (distance / radius)
weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0
# 计算目标点坐标
target_x = center_x + dx
@ -267,17 +342,15 @@ class TerrainManager:
# 根据操作类型修改高度
if operation == "add":
new_height = current_height + strength * weight * 0.1
new_height = min(1.0, current_height + strength * weight * 0.01)
elif operation == "subtract":
new_height = current_height - strength * weight * 0.1
new_height = max(0.0, current_height - strength * weight * 0.01)
elif operation == "set":
new_height = strength
# 平坦化操作,设置为中等高度
new_height = 0.5
else:
new_height = current_height
# 限制高度范围
new_height = max(0.0, min(1.0, new_height))
# 设置新高度
heightmap.setRed(target_x, target_y, new_height)
heightmap.setGreen(target_x, target_y, new_height)
@ -289,7 +362,24 @@ class TerrainManager:
# 重新设置高度图并生成地形
terrain.setHeightfield(heightmap)
terrain.generate()
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}")
# 关键修复:重新创建碰撞体,因为在生成地形时会丢失原有碰撞体
from panda3d.core import BitMask32, CollisionNode, CollisionPlane, Plane, Point3
# 移除旧的地形碰撞体(如果存在)
for child in terrain_node.getChildren():
if isinstance(child.node(), CollisionNode) and child.getName().startswith("terrain_collision_"):
child.removeNode()
# 创建新的地形碰撞体
# 使用更简单的方法:直接给地形节点添加碰撞体
terrain_node.setCollideMask(BitMask32.bit(2)) # 确保地形节点本身具有碰撞掩码
# 为地形的每个子节点也设置碰撞掩码
for child in terrain_node.getChildren():
child.setCollideMask(BitMask32.bit(2))
print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}")
print(f"✓ 重新设置了地形碰撞体")
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
@ -308,7 +398,7 @@ class TerrainManager:
if terrain_info in self.terrains:
# 从场景中移除地形节点
terrain_node = terrain_info['node']
if terrain_node:
if terrain_node and not terrain_node.isEmpty():
terrain_node.removeNode()
# 从列表中移除
@ -319,6 +409,10 @@ class TerrainManager:
self.world.scene_manager.updateSceneTree()
print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}")
import gc
gc.collect()
return True
return False

30
main.py
View File

@ -87,6 +87,10 @@ class MyWorld(CoreWorld):
#self.material_editor = None
self.terrain_manager = TerrainManager(self)
self.terrain_edit_radius = 3.0
self.terrain_edit_strength=0.3
self.terrain_edit_operation = "add"
print("✓ MyWorld 初始化完成")
# ==================== 兼容性属性 ====================
@ -717,14 +721,32 @@ class MyWorld(CoreWorld):
"""更新所有地形的LOD"""
return self.terrain_manager.updateTerrain()
def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"):
"""修改地形高度"""
return self.terrain_manager.modifyTerrainHeight(terrain_info, x, y, radius, strength, operation)
def getTerrainHeight(self, terrain_info, x, y):
"""获取地形上指定点的高度"""
return self.terrain_manager.getTerrainHeight(terrain_info, x, y)
def setTerrainEditParameters(self,radius=None,strength=None,operation=None):
if radius is not None:
self.terrain_edit_radius = float(radius)
if strength is not None:
self.terrain_edit_strenght = float(strength)
if operation is not None:
self.terrain_edit_operation = operation
print(f"地形编辑参数已更新: 半径={self.terrain_edit_radius}, 强度={self.terrain_edit_strength}, 操作={self.terrain_edit_operation}")
def getTerrainEditParameters(self):
"""获取当前地形编辑参数"""
return {
'radius': self.terrain_edit_radius,
'strength': self.terrain_edit_strength,
'operation': self.terrain_edit_operation
}
def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"):
"""修改地形高度的便捷方法"""
if hasattr(self, 'terrain_manager'):
return self.terrain_manager.modifyTerrainHeight(terrain_info, x, y, radius, strength, operation)
return False
# ==================== 项目管理功能代理 ====================
# 以下函数代理到project_manager模块的对应功能

View File

@ -1,239 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
坐标轴修复验证脚本
用于测试鼠标悬停高亮和拖动一致性的修复效果
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt5.QtCore import Qt
from QPanda3D.QPanda3DWidget import QPanda3DWidget
from QPanda3D.Panda3DWorld import Panda3DWorld
from panda3d.core import *
class AxisTestWorld(Panda3DWorld):
def __init__(self):
super().__init__()
print("=== 坐标轴修复验证测试 ===")
self.setBackgroundColor(0.2, 0.2, 0.3)
# 调整相机位置
self.cam.setPos(8, -15, 8)
self.cam.lookAt(0, 0, 0)
# Qt部件引用
self.qtWidget = None
# 基础光照
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)
# 创建测试立方体
self.createTestCube()
# 设置当前工具
self.currentTool = "移动"
# 模拟选择系统和工具管理器
from core.selection import SelectionSystem
self.selection = SelectionSystem(self)
# 简单的工具管理器模拟
class SimpleToolManager:
def isScaleTool(self):
return False
def isRotateTool(self):
return False
self.tool_manager = SimpleToolManager()
# 模拟属性面板
class SimplePropertyPanel:
def refreshModelValues(self, node):
print(f"属性更新: {node.getName()} 位置={node.getPos()}")
self.property_panel = SimplePropertyPanel()
# 自动选中立方体
self.selectedNode = self.testCube
self.selection.updateSelection(self.testCube)
print("测试说明:")
print("1. 鼠标悬停在坐标轴上应该变为黄色高亮")
print("2. 点击并拖拽坐标轴,立方体应该沿对应轴移动")
print("3. 拖动行为应该与鼠标移动方向一致")
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
def getWindowSize(self):
"""获取准确的窗口尺寸"""
if self.qtWidget:
width, height = self.qtWidget.getActualSize()
if width > 0 and height > 0:
return width, height
if hasattr(self, 'win') and self.win:
width = self.win.getXSize()
height = self.win.getYSize()
return width, height
return 800, 600
def createTestCube(self):
"""创建测试立方体"""
from panda3d.core import CardMaker
cm = CardMaker('cube')
cm.setFrame(-1, 1, -1, 1)
self.testCube = self.render.attachNewNode("testCube")
# 6个面不同颜色
faces = [
(Point3(0, 1, 0), Vec3(0, 0, 0), (1, 0.5, 0.5, 1)), # 前面
(Point3(0, -1, 0), Vec3(0, 0, 180), (0.5, 1, 0.5, 1)), # 后面
(Point3(-1, 0, 0), Vec3(0, 0, 90), (0.5, 0.5, 1, 1)), # 左面
(Point3(1, 0, 0), Vec3(0, 0, -90), (1, 1, 0.5, 1)), # 右面
(Point3(0, 0, 1), Vec3(-90, 0, 0), (1, 0.5, 1, 1)), # 顶面
(Point3(0, 0, -1), Vec3(90, 0, 0), (0.5, 1, 1, 1)) # 底面
]
for pos, hpr, color in faces:
face = self.testCube.attachNewNode(cm.generate())
face.setPos(pos)
face.setHpr(hpr)
face.setColor(*color)
print(f"✓ 创建测试立方体,位置: {self.testCube.getPos()}")
return self.testCube
def mousePressEventLeft(self, evt):
"""处理鼠标左键按下事件"""
if not evt:
return
x = evt.get('x', 0)
y = evt.get('y', 0)
# 检查坐标轴点击
if self.selection.gizmo:
gizmoAxis = self.selection.checkGizmoClick(x, y)
if gizmoAxis:
print(f"✓ 坐标轴点击检测成功: {gizmoAxis}")
self.selection.startGizmoDrag(gizmoAxis, x, y)
return
else:
print("× 坐标轴点击检测失败")
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""
if self.selection.isDraggingGizmo:
self.selection.stopGizmoDrag()
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件"""
if not evt:
return
x = evt.get('x', 0)
y = evt.get('y', 0)
# 处理坐标轴拖拽
if self.selection.isDraggingGizmo:
self.selection.updateGizmoDrag(x, y)
return
# 更新坐标轴高亮
if self.selection.gizmo and not self.selection.isDraggingGizmo:
self.selection.updateGizmoHighlight(x, y)
class TestWidget(QPanda3DWidget):
"""测试部件"""
def __init__(self, world, parent=None):
super().__init__(world, parent)
self.world = world
if hasattr(world, 'setQtWidget'):
world.setQtWidget(self)
def getActualSize(self):
return (self.width(), self.height())
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.world.mousePressEventLeft({
'x': event.x(),
'y': event.y()
})
event.accept()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.world.mouseReleaseEventLeft({
'x': event.x(),
'y': event.y()
})
event.accept()
def mouseMoveEvent(self, event):
self.world.mouseMoveEvent({
'x': event.x(),
'y': event.y()
})
event.accept()
def main():
print("启动坐标轴修复验证测试...")
app = QApplication(sys.argv)
# 创建主窗口
mainWindow = QMainWindow()
mainWindow.setWindowTitle("坐标轴修复验证测试")
mainWindow.setGeometry(100, 100, 900, 700)
# 创建布局
centralWidget = QWidget()
layout = QVBoxLayout(centralWidget)
# 添加说明
label = QLabel("坐标轴修复验证测试 - 测试鼠标悬停高亮和拖动一致性")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("background-color: lightgreen; padding: 10px; font-size: 12px;")
layout.addWidget(label)
# 创建世界和部件
world = AxisTestWorld()
pandaWidget = TestWidget(world)
layout.addWidget(pandaWidget)
mainWindow.setCentralWidget(centralWidget)
mainWindow.show()
print("\n修复验证项目:")
print("1. ✓ 鼠标悬停时坐标轴应该正确高亮为黄色")
print("2. ✓ 点击坐标轴应该能正确开始拖拽")
print("3. ✓ 拖拽方向应该与鼠标移动方向一致")
print("4. ✓ 减少了控制台调试输出")
return app.exec_()
if __name__ == "__main__":
sys.exit(main())

View File

@ -137,6 +137,22 @@ class InterfaceManager:
def deleteNode(self, nodePath, item):
"""删除节点"""
try:
item_data = item.data(0,Qt.UserRole+1)
if item_data == "terrain":
if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains:
terrain_to_remove = None
for terrain_info in self.world.terrain_manager.terrains:
if terrain_info['node'] == nodePath:
terrain_to_remove = terrain_info
break
if terrain_to_remove:
self.world.terrain_manager.deleteTerrain(terrain_to_remove)
print(f"成功删除地形节点:{nodePath.getName()}")
self.updateSceneTree()
self.world.property_panel.clearPropertyPanel()
self.world.selection.updateSelection(None)
return
# 从场景中移除
self.world.property_panel.removeActorForModel(nodePath)
@ -207,6 +223,9 @@ class InterfaceManager:
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(),CollisionNode) or isinstance(node.node(),ModelRoot) or name==""
def addNodeToTree(node,parentItem,force = False):
if node.isEmpty():
return
if not force and should_skip(node):
return
nodeItem = QTreeWidgetItem(parentItem,[node.getName()])
@ -216,7 +235,8 @@ class InterfaceManager:
addNodeToTree(child,nodeItem,force = False)
for model in self.world.models:
addNodeToTree(model, sceneRoot,force=True)
if not model.isEmpty():
addNodeToTree(model, sceneRoot,force=True)
# 添加所有GUI元素
for gui in self.world.gui_elements:
@ -229,18 +249,26 @@ class InterfaceManager:
else:
# 跳过非GUI节点如QDockWidget等Qt对象
continue
# 添加地板节点
if hasattr(self.world, 'ground') and self.world.ground:
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
groundItem.setData(0, Qt.UserRole, self.world.ground)
#添加灯光节点
for light in self.world.Spotlight + self.world.Pointlight:
addNodeToTree(light, sceneRoot, force=True)
if not light.isEmpty:
addNodeToTree(light, sceneRoot, force=True)
#添加 Cesium tilesets
if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'tilesets'):
for i , tileset_info in enumerate(self.world.scene_manager.tilesets):
tileset_node = tileset_info['node']
tileset_url = tileset_info['url']
tileset_item = QTreeWidgetItem(sceneRoot,[f"Cesium Tileset {i}"])
tileset_item.setData(0,Qt.UserRole,tileset_node)
addNodeToTree(tileset_node,tileset_item,force=True)
if not tileset_node.isEmpty():
tileset_url = tileset_info['url']
tileset_item = QTreeWidgetItem(sceneRoot,[f"Cesium Tileset {i}"])
tileset_item.setData(0,Qt.UserRole,tileset_node)
addNodeToTree(tileset_node,tileset_item,force=True)
#添加地形节点
if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains:
for terrain_info in self.world.terrain_manager.terrains:
@ -252,10 +280,7 @@ class InterfaceManager:
addNodeToTree(terrain_node,terrain_item,force=True)
# 添加地板节点
if hasattr(self.world, 'ground') and self.world.ground:
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
groundItem.setData(0, Qt.UserRole, self.world.ground)
# 展开所有节点
#self.treeWidget.expandAll()

View File

@ -1005,8 +1005,8 @@ class MainWindow(QMainWindow):
width_layout = QHBoxLayout()
width_layout.addWidget(QLabel("宽度:"))
width_spin = QDoubleSpinBox()
width_spin.setRange(1,10000)
width_spin.setValue(100)
width_spin.setRange(0,10000)
width_spin.setValue(0.3)
width_layout.addWidget(width_spin)
layout.addLayout(width_layout)
@ -1014,8 +1014,8 @@ class MainWindow(QMainWindow):
height_layout = QHBoxLayout()
height_layout.addWidget(QLabel("高度:"))
height_spin = QDoubleSpinBox()
height_spin.setRange(1, 10000)
height_spin.setValue(100)
height_spin.setRange(0, 10000)
height_spin.setValue(0.3)
height_layout.addWidget(height_spin)
layout.addLayout(height_layout)

View File

@ -1,5 +1,6 @@
import os
from collections import deque
from traceback import print_exc
from traceback import print_exc, print_last
from types import new_class
from typing import Hashable
@ -9,6 +10,7 @@ from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton,
from PyQt5.QtCore import Qt
from deploy_libs.unicodedata import normalize
from direct.actor.Actor import Actor
from idna import check_label
from jinja2.compiler import has_safe_repr
from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup
from scene import util
@ -23,6 +25,14 @@ class PropertyPanelManager:
self._propertyLayout = None
self._actor_cache={}
# 初始化地形编辑参数
if not hasattr(self.world, 'terrain_edit_radius'):
self.world.terrain_edit_radius = 3.0
if not hasattr(self.world, 'terrain_edit_strength'):
self.world.terrain_edit_strength = 0.3
if not hasattr(self.world, 'terrain_edit_operation'): # 这里原来是 terrain_edit_opertaion
self.world.terrain_edit_operation = "add"
# 定义紧凑样式
self.compact_style = """
QDoubleSpinBox {
@ -143,7 +153,9 @@ class PropertyPanelManager:
# 获取节点对象
model = item.data(0, Qt.UserRole)
if model and hasattr(model,'getTag') and model.getTag("element_type") == "cesium_tileset":
if self._isTerrainNode(model,item):
self._showTerrainProperties(model,item)
elif model and hasattr(model,'getTag') and model.getTag("element_type") == "cesium_tileset":
self._showCesiumTilesetProperties(model,item)
elif model and hasattr(model, 'getTag') and model.getTag("gui_type"):
self.updateGUIPropertyPanel(model)
@ -161,6 +173,362 @@ class PropertyPanelManager:
if propertyWidget:
propertyWidget.update()
def _isTerrainNode(self,node,item):
"""检查是否是地形节点"""
item_data = item.data(0,Qt.UserRole+1)
if item_data == "terrain":
return True
if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains:
for terrain_info in self.world.terrain_manager.terrains:
if terrain_info['node'] == node:
return True
return False
def _showTerrainProperties(self,terrain_node,item):
"""显示地形属性面板"""
try:
terrain_info = None
if hasattr(self.world,'terrain_manager'):
for info in self.world.terrain_manager.terrains:
if info['node'] == terrain_node:
terrain_info = info
break
if not terrain_info:
no_info_label = QLabel("未找到地形信息")
no_info_label.setStyleSheet("color:red;font-weight:bold;")
self._propertyLayout.addWidget(no_info_label)
return
info_group = QGroupBox("地形信息")
info_layout = QGridLayout()
info_layout.addWidget(QLabel("名称:"),0,0)
name_label = QLabel(terrain_info.get('name','未知'))
info_layout.addWidget(name_label,0,1)
info_layout.addWidget(QLabel("类型:"),1,0)
type_label = QLabel("高度图地形"if terrain_info.get('heightmap') else "平面地形")
info_layout.addWidget(type_label,1,1)
if terrain_info.get('heightmap'):
info_layout.addWidget(QLabel("高度图:"),2,0)
heightmap_label = QLabel(os.path.basename(terrain_info['heightmap']))
heightmap_label.setWordWrap(True)
info_layout.addWidget(heightmap_label,2,1)
info_group.setLayout(info_layout)
self._propertyLayout.addWidget(info_group)
#变换属性
self._updateTerrainTransformPanel(terrain_node)
#地形编辑控制面板
self._createTerrainEditPanel(terrain_info)
#材质属性
self._updateTerrainMaterialPanel(terrain_node,terrain_info)
#删除按钮
delete_btn = QPushButton("删除地形")
delete_btn.setStyleSheet("""
QPushButton{
background-color:#ff4444;
color:white;
border:none;
padding:8px;
border-radius:4px;
margin-top:10px
}
QPushButton:hover{
background-color:#ff6666;
}
""")
delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item))
self._propertyLayout.addWidget(delete_btn)
except Exception as e:
print(f"显示地形属性时出错: {e}")
import traceback
traceback.print_exc()
def _updateTerrainTransformPanel(self,terrain_node):
"""更新地形变化属性面板"""
try:
transform_group = QGroupBox("变换 Transform")
transform_layout = QGridLayout()
pos = terrain_node.getPos()
scale = terrain_node.getScale()
transform_layout.addWidget(QLabel("位置"),0,0)
x_label = QLabel("X")
y_label = QLabel("Y")
z_label = QLabel("Z")
x_label.setAlignment(Qt.AlignCenter)
y_label.setAlignment(Qt.AlignCenter)
z_label.setAlignment(Qt.AlignCenter)
transform_layout.addWidget(x_label,0,1)
transform_layout.addWidget(y_label,0,2)
transform_layout.addWidget(z_label,0,3)
self.pos_x = QDoubleSpinBox()
self.pos_y = QDoubleSpinBox()
self.pos_z = QDoubleSpinBox()
for pos_widget in [self.pos_x,self.pos_y,self.pos_z]:
pos_widget.setRange(-1000000.0,1000000.0)
self.pos_x.setValue(pos.getX())
self.pos_y.setValue(pos.getY())
self.pos_z.setValue(pos.getZ())
def updateXPosition(value):
terrain_node.setX(value)
self.refreshModelValues(terrain_node)
self.pos_x.valueChanged.connect(updateXPosition)
def updateYPosition(value):
terrain_node.setY(value)
self.refreshModelValues(terrain_node)
self.pos_y.valueChanged.connect(updateYPosition)
def updateZPosition(value):
terrain_node.setZ(value)
self.refreshModelValues(terrain_node)
self.pos_z.valueChanged.connect(updateZPosition)
transform_layout.addWidget(self.pos_x,1,1)
transform_layout.addWidget(self.pos_y,1,2)
transform_layout.addWidget(self.pos_z,1,3)
transform_layout.addWidget(QLabel("旋转"),2,0)
self.rot_h = QDoubleSpinBox()
self.rot_p = QDoubleSpinBox()
self.rot_r = QDoubleSpinBox()
for rot_widget in [self.rot_h,self.rot_p,self.rot_r]:
rot_widget.setRange(-360,360)
self.rot_h.setValue(terrain_node.getH())
self.rot_p.setValue(terrain_node.getP())
self.rot_r.setValue(terrain_node.getR())
self.rot_h.valueChanged.connect(lambda v:terrain_node.setH(v))
self.rot_p.valueChanged.connect(lambda v:terrain_node.setP(v))
self.rot_r.valueChanged.connect(lambda v:terrain_node.setR(v))
h_label = QLabel("H")
p_label = QLabel("P")
r_label = QLabel("R")
h_label.setAlignment(Qt.AlignCenter)
p_label.setAlignment(Qt.AlignCenter)
r_label.setAlignment(Qt.AlignCenter)
transform_layout.addWidget(h_label,2,1)
transform_layout.addWidget(p_label,2,2)
transform_layout.addWidget(r_label,2,3)
transform_layout.addWidget(self.rot_h,3,1)
transform_layout.addWidget(self.rot_p,3,2)
transform_layout.addWidget(self.rot_r,3,3)
transform_layout.addWidget(QLabel("缩放"),4,0)
self.scale_x = QDoubleSpinBox()
self.scale_y = QDoubleSpinBox()
self.scale_z = QDoubleSpinBox()
for scale_widget in [self.scale_x,self.scale_y,self.scale_z]:
scale_widget.setRange(-1000,1000)
scale_widget.setSingleStep(0.1)
self.scale_x.setValue(scale.getX())
self.scale_y.setValue(scale.getY())
self.scale_z.setValue(scale.getZ())
self.scale_x.valueChanged.connect(lambda v:terrain_node.setScaleX(v))
self.scale_y.valueChanged.connect(lambda v:terrain_node.setScaleY(v))
self.scale_z.valueChanged.connect(lambda v:terrain_node.setScaleZ(v))
x_label2 = QLabel("X")
y_label2 = QLabel("Y")
z_label2 = QLabel("Z")
x_label2.setAlignment(Qt.AlignCenter)
y_label2.setAlignment(Qt.AlignCenter)
z_label2.setAlignment(Qt.AlignCenter)
transform_layout.addWidget(x_label2,4,1)
transform_layout.addWidget(y_label2,4,2)
transform_layout.addWidget(z_label2,4,3)
transform_layout.addWidget(self.scale_x,5,1)
transform_layout.addWidget(self.scale_y,5,2)
transform_layout.addWidget(self.scale_z,5,3)
transform_group.setLayout(transform_layout)
self._propertyLayout.addWidget(transform_group)
except Exception as e:
print(f"更新地形变换面板时出错: {e}")
def _createTerrainEditPanel(self, terrain_info):
try:
edit_group = QGroupBox("地形编辑")
edit_layout = QGridLayout()
# 编辑半径
edit_layout.addWidget(QLabel("编辑半径:"), 0, 0)
self.terrain_radius_spin = QDoubleSpinBox()
self.terrain_radius_spin.setRange(0.1, 50.0)
self.terrain_radius_spin.setSingleStep(0.5)
self.terrain_radius_spin.setValue(getattr(self.world, 'terrain_edit_radius', 3.0))
self.terrain_radius_spin.valueChanged.connect(self._onTerrainRadiusChanged)
edit_layout.addWidget(self.terrain_radius_spin, 0, 1)
# 编辑强度
edit_layout.addWidget(QLabel("编辑强度:"), 1, 0)
self.terrain_strength_spin = QDoubleSpinBox()
self.terrain_strength_spin.setRange(0.01, 50)
self.terrain_strength_spin.setSingleStep(0.1)
self.terrain_strength_spin.setValue(getattr(self.world, 'terrain_edit_strength', 0.3))
self.terrain_strength_spin.valueChanged.connect(self._onTerrainStrengthChanged)
edit_layout.addWidget(self.terrain_strength_spin, 1, 1)
edit_layout.addWidget(QLabel("操作类型:"), 2, 0)
self.terrain_operation_combo = QComboBox()
self.terrain_operation_combo.addItems(["升高地形", "降低地形", "平坦化"])
current_op = getattr(self.world, 'terrain_edit_operation', "add")
if current_op == "add":
self.terrain_operation_combo.setCurrentText("升高地形")
elif current_op == "subtract":
self.terrain_operation_combo.setCurrentText("降低地形")
else:
self.terrain_operation_combo.setCurrentText("平坦化")
self.terrain_operation_combo.currentTextChanged.connect(self._onTerrainOperationChanged)
edit_layout.addWidget(self.terrain_operation_combo, 2, 1)
help_label = QLabel("在地形编辑模式下,使用鼠标左键升高地形,右键降低地形")
help_label.setWordWrap(True)
help_label.setStyleSheet("font-size:10px;color:gray;")
edit_layout.addWidget(help_label, 3, 0, 1, 2)
edit_group.setLayout(edit_layout)
self._propertyLayout.addWidget(edit_group)
# 确保初始值被正确设置到 world 对象上
self._onTerrainRadiusChanged(self.terrain_radius_spin.value())
self._onTerrainStrengthChanged(self.terrain_strength_spin.value())
self._onTerrainOperationChanged(self.terrain_operation_combo.currentText())
except Exception as e:
print(f"创建地形编辑面板时出错: {e}")
def _onTerrainRadiusChanged(self,value):
"""地形编辑半径改变"""
if hasattr(self.world,'terrain_edit_radius'):
self.world.terrain_edit_radius = float(value)
def _onTerrainStrengthChanged(self,value):
"""地形编辑强度改变"""
if hasattr(self.world,'terrain_edit_strength'):
self.world.terrain_edit_strength = float(value)
def _onTerrainOperationChanged(self, text):
"""地形编辑操作类型改变"""
operation_map = {
"升高地形": "add",
"降低地形": "subtract",
"平坦化": "set"
}
if hasattr(self.world, 'terrain_edit_operation'):
# 正确设置地形编辑操作属性
self.world.terrain_edit_operation = operation_map.get(text, "add")
print(f"地形编辑操作已设置为: {self.world.terrain_edit_operation}")
def _updateTerrainMaterialPanel(self,terrain_node,terrain_info):
"""更新地形材质属性面板"""
try:
material_group = QGroupBox("材质属性")
material_layout = QGridLayout()
#颜色设置
material_layout.addWidget(QLabel("颜色:"),0,0)
color_button = QPushButton("选择颜色")
color_button.clicked.connect(lambda:self._selectTerrainColor(terrain_info))
#纹理设置
material_layout.addWidget(QLabel("纹理:"),1,0)
texture_button = QPushButton("选择纹理")
texture_button.clicked.connect(lambda :self._selectTerrainTexture(terrain_info))
material_layout.addWidget(texture_button,1,1)
material_group.setLayout(material_layout)
self._propertyLayout.addWidget(material_group)
except Exception as e:
print(f"更新材质面板时出错{e}")
def _selectTerrainColor(self,terrain_info):
try:
from PyQt5.QtWidgets import QColorDialog
from PyQt5.QtGui import QColor
current_color = QColor(255,255,255)
color = QColorDialog.getColor(current_color,None,"选择地形颜色")
if color.isValid():
r,g,b = color.red()/255.0,color.green()/255.0,color.blue()/255.0
if hasattr(self.world,'terrain_manager'):
self.world.terrain_manager.setTerrainColor(terrain_info,(r,g,b))
except Exception as e:
print(f"选择地形颜色时出错: {e}")
def _selectTerrainTexture(self,terrain_info):
"""选择地形纹理"""
try:
from PyQt5.QtWidgets import QFileDialog
file_path,_ = QFileDialog.getOpenFileName(
None,
"选择地形纹理",
"",
"图像文件(*.png *.jpg *.jpeg *.bmp *.tga *.dds)"
)
if file_path and os.path.exists(file_path):
if hasattr(self.world,'terrain_manager'):
success = self.world.terrain_manager.setTerrainTexture(terrain_info,file_path)
if success:
print(f"地形纹理已应用{file_path}")
else:
print(f"应用地形纹理失败")
except Exception as e:
print(f"选择地形纹理时出错: {e}")
def _deleteTerrain(self, terrain_info, item):
"""删除地形"""
try:
from PyQt5.QtWidgets import QMessageBox
reply = QMessageBox.question(
None,
'确认删除',
f'确定要删除地形 "{terrain_info.get("name", "未知地形")}" 吗?',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
# 调用 interface_manager 中已经实现的 deleteNode 方法
if hasattr(self.world, 'interface_manager') and self.world.interface_manager.treeWidget:
self.world.interface_manager.deleteNode(terrain_info['node'], item)
else:
# 如果 interface_manager 不可用,使用原来的删除逻辑
if hasattr(self.world, 'terrain_manager'):
success = self.world.terrain_manager.deleteTerrain(terrain_info)
if success:
# 更新场景树
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager,
'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
# 清空属性面板
self.clearPropertyPanel()
print(f"✓ 地形已删除: {terrain_info.get('name', '未知')}")
else:
print("✗ 删除地形失败")
except Exception as e:
print(f"删除地形时出错: {e}")
def _cleanupAllReferences(self):
"""清理所有控件引用"""
# 清理变换控件引用