diff --git a/core/event_handler.py b/core/event_handler.py index 115fed2f..5b35345d 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -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}") diff --git a/core/terrain_manager.py b/core/terrain_manager.py index f9270733..dd940091 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -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 diff --git a/main.py b/main.py index 57151869..610c7abf 100644 --- a/main.py +++ b/main.py @@ -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模块的对应功能 diff --git a/test_axis_fix.py b/test_axis_fix.py deleted file mode 100644 index 8331e606..00000000 --- a/test_axis_fix.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/ui/interface_manager.py b/ui/interface_manager.py index f1d571b3..bca64749 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -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() diff --git a/ui/main_window.py b/ui/main_window.py index ffc83aab..5b8457ab 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -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) diff --git a/ui/property_panel.py b/ui/property_panel.py index eb8df342..b15098d6 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -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): """清理所有控件引用""" # 清理变换控件引用