From 1ad208652505ebc45196d648ca77777374f292d2 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Thu, 8 Jan 2026 16:30:59 +0800 Subject: [PATCH] =?UTF-8?q?ui=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/event_handler.py | 1173 +++++++++++++++++----------------- core/imgui_manager.py | 166 +++++ core/imgui_style_manager.py | 286 +++++++++ core/simple_imgui_manager.py | 204 ++++++ demo.py | 679 ++++++++++++++++++++ imgui.ini | 55 ++ test_chinese_font.py | 149 +++++ testgui.py | 21 + 8 files changed, 2150 insertions(+), 583 deletions(-) create mode 100644 core/imgui_manager.py create mode 100644 core/imgui_style_manager.py create mode 100644 core/simple_imgui_manager.py create mode 100644 demo.py create mode 100644 imgui.ini create mode 100644 test_chinese_font.py create mode 100644 testgui.py diff --git a/core/event_handler.py b/core/event_handler.py index e9f2a40d..08326d03 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -1,584 +1,591 @@ -from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState, - DepthTestAttrib, ColorAttrib) - - -class EventHandler: - """事件处理器 - 处理鼠标和键盘输入事件""" - - def __init__(self, world): - """初始化事件处理器""" - self.world = world - - # 射线显示相关 - self.showRay = False # 是否显示射线(默认关闭) - self.rayNode = None # 当前显示的射线节点 - self.rayLifetime = 2.0 # 射线显示时间(秒) - - def showClickRay(self, nearPoint, farPoint, hitPos=None): - """显示鼠标点击的射线""" - if not self.showRay: - return - - try: - # 清除之前的射线 - self.clearRay() - - # 创建射线几何体 - lines = LineSegs() - lines.setThickness(3.0) - - # 设置射线颜色 - if hitPos: - # 有碰撞:射线分两段,起点到碰撞点为绿色,碰撞点到终点为红色 - lines.setColor(0, 1, 0, 1) # 绿色 - lines.moveTo(nearPoint) - lines.drawTo(hitPos) - - lines.setColor(1, 0, 0, 1) # 红色 - lines.moveTo(hitPos) - lines.drawTo(farPoint) - - # 在碰撞点添加一个小球 - lines.setColor(1, 1, 0, 1) # 黄色 - self._addHitMarker(lines, hitPos) - else: - # 无碰撞:整条射线为蓝色 - lines.setColor(0, 0, 1, 1) # 蓝色 - lines.moveTo(nearPoint) - lines.drawTo(farPoint) - - # 创建射线节点 - geomNode = lines.create() - self.rayNode = self.world.render.attachNewNode(geomNode) - self.rayNode.setName("clickRay") - - # 设置渲染状态,确保射线总是可见 - state = RenderState.make( - DepthTestAttrib.make(DepthTestAttrib.MAlways), # 总是通过深度测试 - ColorAttrib.makeFlat((1.0, 1.0, 1.0, 1.0)) - ) - self.rayNode.setState(state) - self.rayNode.setLightOff() # 不受光照影响 - - # 设置自动清除任务(先清除可能存在的旧任务) - from direct.task.TaskManagerGlobal import taskMgr - taskMgr.remove("clearRay") # 清除可能存在的旧任务 - taskMgr.doMethodLater(self.rayLifetime, self.clearRayTask, "clearRay") - - print(f"✓ 射线已显示,{self.rayLifetime}秒后自动清除") - - except Exception as e: - print(f"显示射线失败: {str(e)}") - - def _addHitMarker(self, lines, hitPos): - """在碰撞点添加标记""" - # 创建一个小十字标记 - marker_size = 0.5 - - # X方向线 - lines.moveTo(hitPos.x - marker_size, hitPos.y, hitPos.z) - lines.drawTo(hitPos.x + marker_size, hitPos.y, hitPos.z) - - # Y方向线 - lines.moveTo(hitPos.x, hitPos.y - marker_size, hitPos.z) - lines.drawTo(hitPos.x, hitPos.y + marker_size, hitPos.z) - - # Z方向线 - lines.moveTo(hitPos.x, hitPos.y, hitPos.z - marker_size) - lines.drawTo(hitPos.x, hitPos.y, hitPos.z + marker_size) - - def clearRay(self): - """清除当前显示的射线""" - if self.rayNode: - self.rayNode.removeNode() - self.rayNode = None - - # 同时清除可能存在的任务 - from direct.task.TaskManagerGlobal import taskMgr - taskMgr.remove("clearRay") - - def clearRayTask(self, task): - """清除射线的任务回调""" - self.clearRay() - return task.done - - def toggleRayDisplay(self): - """切换射线显示状态""" - self.showRay = not self.showRay - if not self.showRay: - self.clearRay() - print(f"射线显示: {'开启' if self.showRay else '关闭'}") - return self.showRay - - def mousePressEventLeft(self, evt): - """处理鼠标左键按下事件""" - print("\n=== 开始处理鼠标左键事件 ===") - #print(f"当前工具: {self.world.currentTool}") - - if not evt: - print("事件为空") - return - if self.world.currentTool == "地形编辑": - self._handleTerrainEdit(evt,"add") - return - - # 获取鼠标点击的位置 - x = evt.get('x', 0) - y = evt.get('y', 0) - #print(f"鼠标点击位置: ({x}, {y})") - - # 获取准确的窗口尺寸 - winWidth, winHeight = self.world.getWindowSize() - - # 直接使用 x, y 创建鼠标位置 - mx = 2.0 * x / float(winWidth) - 1.0 - my = 1.0 - 2.0 * y / float(winHeight) - #print(f"转换后的坐标: ({mx}, {my})") - - # 创建射线 - nearPoint = Point3() - farPoint = Point3() - self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) - #print(f"相机坐标系射线起点: {nearPoint}") - #print(f"相机坐标系射线终点: {farPoint}") - - # 将相机坐标系的点转换到世界坐标系 - worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) - worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) - #print(f"世界坐标系射线起点: {worldNearPoint}") - #print(f"世界坐标系射线终点: {worldFarPoint}") - - # 进行射线检测 - picker = CollisionTraverser() - queue = CollisionHandlerQueue() - - pickerNode = CollisionNode('mouseRay') - pickerNP = self.world.cam.attachNewNode(pickerNode) - # 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) - from panda3d.core import BitMask32 - pickerNode.setFromCollideMask(BitMask32.bit(2)) - - # 使用相机坐标系的点创建射线(因为pickerNP是相机的子节点) - direction = farPoint - nearPoint - direction.normalize() - pickerNode.addSolid(CollisionRay(nearPoint, direction)) - - picker.addCollider(pickerNP, queue) - picker.traverse(self.world.render) - - #print(f"碰撞检测结果数量: {queue.getNumEntries()}") - - # 射线检测结果处理 - hitPos = None - hitNode = None - - if queue.getNumEntries() > 0: - # 获取最近的碰撞点 - entry = queue.getEntry(0) - hitPos = entry.getSurfacePoint(self.world.render) - hitNode = entry.getIntoNodePath() - print(f"碰撞到节点: {hitNode.getName()}") - - # 显示射线(使用世界坐标系的点) - self.showClickRay(worldNearPoint, worldFarPoint, hitPos) - - # 优先检查是否点击了坐标轴 - #print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") - if self.world.selection.gizmo: - #print("准备检查坐标轴点击...") - try: - highlighted_axis = self.world.selection.gizmoHighlightAxis - if highlighted_axis: - print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽") - # 直接使用高亮轴开始拖拽 - self.world.selection.startGizmoDrag(highlighted_axis, x, y) - pickerNP.removeNode() - return - - # 如果没有高亮轴,再尝试检测点击 - gizmoAxis = self.world.selection.checkGizmoClick(x, y) - if gizmoAxis: - print(f"✓ 检测到坐标轴点击: {gizmoAxis}") - # 开始坐标轴拖拽 - self.world.selection.startGizmoDrag(gizmoAxis, x, y) - pickerNP.removeNode() - return - else: - print("× 没有点击到坐标轴") - - # gizmoAxis = self.world.selection.checkGizmoClick(x, y) - # if gizmoAxis: - # #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") - # # 开始坐标轴拖拽 - # self.world.selection.startGizmoDrag(gizmoAxis, x, y) - # pickerNP.removeNode() - # return - # else: - # print("× 没有点击到坐标轴") - except Exception as e: - print(f"❌ 坐标轴点击检测出现异常: {str(e)}") - import traceback - traceback.print_exc() - print("继续处理模型选择...") - - #print("继续处理碰撞结果...") - - if hitPos and hitNode: - #print(f"✓ 检测到碰撞,开始处理点击事件") - #print(f"GUI编辑模式: {self.world.guiEditMode}") - #print(f"当前工具: {self.world.currentTool}") - - # 处理GUI编辑模式 - if self.world.guiEditMode: - #print("处理GUI编辑模式点击") - # 检查是否点击了GUI元素 - clickedGUI = self.world.gui_manager.findClickedGUI(hitNode) - if clickedGUI: - # 选中GUI元素 - self.world.selection.updateSelection(clickedGUI) - self.world.gui_manager.selectGUIInTree(clickedGUI) - print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}") - elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool: - # 在点击位置创建新GUI元素 - self.world.gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool) - pickerNP.removeNode() - return - - # 根据当前工具处理点击事件 - if self.world.currentTool == "选择": - print("✓ 使用选择工具处理点击") - try: - self._handleSelectionClick(hitNode) - print("✓ 选择处理完成") - except Exception as e: - print(f"❌ 选择处理出现异常: {str(e)}") - import traceback - traceback.print_exc() - else: - print(f"当前工具不是'选择',无法处理: {self.world.currentTool}") - else: - print("没有检测到碰撞") - - # 如果不在GUI编辑模式,清除选择 - if not self.world.guiEditMode: - self.world.selection.updateSelection(None) - - # 在GUI编辑模式下,即使没有碰撞也可以在空白区域创建GUI - if (self.world.guiEditMode and - hasattr(self.world, 'currentGUITool') and - self.world.currentGUITool): - # 使用默认的地面高度创建GUI - default_height = 0.0 - world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换 - world_pos.setZ(default_height) - self.world.gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool) - - # 确保总是清理碰撞检测节点 - try: - pickerNP.removeNode() - print("✓ 碰撞检测节点已清理") - except Exception as e: - print(f"清理碰撞检测节点失败: {str(e)}") - - 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 _handleSelectionClick(self, hitNode): - """处理选择工具的点击事件""" - print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") - - # 查找对应的实际模型节点 - selectedModel = None - - # 如果点击的是碰撞节点,找到它的父模型 - if isinstance(hitNode.node(), CollisionNode): - print(f"点击的是碰撞节点: {hitNode.getName()}") - # 碰撞节点的父节点应该是模型 - parent = hitNode.getParent() - if parent in self.world.models: - selectedModel = parent - print(f"找到对应的模型: {selectedModel.getName()}") - else: - print(f"碰撞节点的父节点不是模型: {parent.getName()}") - else: - # 查找可选择的节点(模型或其子节点) - current = hitNode - while current != self.world.render: - # 检查是否是模型 - if current in self.world.models: - selectedModel = current - print(f"找到模型节点: {selectedModel.getName()}") - break - - # 检查是否是模型的子节点 - for model in self.world.models: - if current.getParent() == model or current.isAncestorOf(model): - selectedModel = model - print(f"找到父模型: {selectedModel.getName()}") - break - - if selectedModel: - break - - current = current.getParent() - - if selectedModel: - #print(f"✓ 最终选中模型: {selectedModel.getName()}") - self.world.selection.handleMouseClick(selectedModel) - - # 更新选择状态并显示选择框和坐标轴 - self.world.selection.updateSelection(selectedModel) - - # 在树形控件中查找并选中对应的项 - if self.world.interface_manager.treeWidget: - #print("查找树形控件中的对应项...") - root = self.world.interface_manager.treeWidget.invisibleRootItem() - foundItem = None - - for i in range(root.childCount()): - sceneItem = root.child(i) - if sceneItem.text(0) == "场景": - #print(f"在场景节点下查找...") - foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem) - if foundItem: - print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}") - try: - self.world.interface_manager.treeWidget.itemClicked.disconnect() - except TypeError: - pass - - self.world.interface_manager.treeWidget.setCurrentItem(foundItem) - - self.world.interface_manager.treeWidget.itemClicked.connect( - self.world.interface_manager.onTreeItemClicked) - else: - print("× 在树形控件中没有找到对应项") - break - - if not foundItem: - print("× 没有找到场景节点或对应的树形项") - else: - print("× 树形控件不存在") - else: - print("× 没有找到可选择的模型节点") - self.world.selection.updateSelection(None) - - def mouseReleaseEventLeft(self, evt): - """处理鼠标左键释放事件""" - # 处理坐标轴拖拽结束 - if self.world.selection.isDraggingGizmo: - self.world.selection.stopGizmoDrag() - return - - def wheelForward(self, data=None): - """处理滚轮向前滚动(前进)""" - # 调用CoreWorld的父类方法 - super(type(self.world), self.world).wheelForward(data) - - # 更新属性面板 - if (self.world.interface_manager.treeWidget and - self.world.interface_manager.treeWidget.currentItem() and - self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): - self.world.property_panel.updatePropertyPanel( - self.world.interface_manager.treeWidget.currentItem()) - - def wheelBackward(self, data=None): - """处理滚轮向后滚动(后退)""" - # 调用CoreWorld的父类方法 - super(type(self.world), self.world).wheelBackward(data) - - # 更新属性面板 - if (self.world.interface_manager.treeWidget and - self.world.interface_manager.treeWidget.currentItem() and - self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): - self.world.property_panel.updatePropertyPanel( - self.world.interface_manager.treeWidget.currentItem()) - - def mousePressEventMiddle(self, evt): - """处理鼠标中键按下事件""" - # 已移除原有的Z轴拖拽功能 - pass - - def mouseReleaseEventMiddle(self, evt): - """处理鼠标中键释放事件""" - # 已移除原有的Z轴拖拽功能 - pass - - def mouseMoveEvent(self, evt): - """处理鼠标移动事件""" - if not evt: - return - - # 处理坐标轴拖拽 - if self.world.selection.isDraggingGizmo: - 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) - - # 更新坐标轴拖拽 - self.world.selection.updateGizmoDrag(x, y) - return - - # 更新坐标轴高亮(鼠标悬停效果) - if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo: - x = evt.get('x', 0) - y = evt.get('y', 0) - # 减少高亮调试输出,只在需要时输出 - # 已静默处理,避免控制台刷屏 - self.world.selection.updateGizmoHighlight(x, y) - - # 调用CoreWorld的父类方法处理基础的相机旋转 +from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState, + DepthTestAttrib, ColorAttrib) + + +class EventHandler: + """事件处理器 - 处理鼠标和键盘输入事件""" + + def __init__(self, world): + """初始化事件处理器""" + self.world = world + + # 射线显示相关 + self.showRay = False # 是否显示射线(默认关闭) + self.rayNode = None # 当前显示的射线节点 + self.rayLifetime = 2.0 # 射线显示时间(秒) + + def showClickRay(self, nearPoint, farPoint, hitPos=None): + """显示鼠标点击的射线""" + if not self.showRay: + return + + try: + # 清除之前的射线 + self.clearRay() + + # 创建射线几何体 + lines = LineSegs() + lines.setThickness(3.0) + + # 设置射线颜色 + if hitPos: + # 有碰撞:射线分两段,起点到碰撞点为绿色,碰撞点到终点为红色 + lines.setColor(0, 1, 0, 1) # 绿色 + lines.moveTo(nearPoint) + lines.drawTo(hitPos) + + lines.setColor(1, 0, 0, 1) # 红色 + lines.moveTo(hitPos) + lines.drawTo(farPoint) + + # 在碰撞点添加一个小球 + lines.setColor(1, 1, 0, 1) # 黄色 + self._addHitMarker(lines, hitPos) + else: + # 无碰撞:整条射线为蓝色 + lines.setColor(0, 0, 1, 1) # 蓝色 + lines.moveTo(nearPoint) + lines.drawTo(farPoint) + + # 创建射线节点 + geomNode = lines.create() + self.rayNode = self.world.render.attachNewNode(geomNode) + self.rayNode.setName("clickRay") + + # 设置渲染状态,确保射线总是可见 + state = RenderState.make( + DepthTestAttrib.make(DepthTestAttrib.MAlways), # 总是通过深度测试 + ColorAttrib.makeFlat((1.0, 1.0, 1.0, 1.0)) + ) + self.rayNode.setState(state) + self.rayNode.setLightOff() # 不受光照影响 + + # 设置自动清除任务(先清除可能存在的旧任务) + from direct.task.TaskManagerGlobal import taskMgr + taskMgr.remove("clearRay") # 清除可能存在的旧任务 + taskMgr.doMethodLater(self.rayLifetime, self.clearRayTask, "clearRay") + + print(f"✓ 射线已显示,{self.rayLifetime}秒后自动清除") + + except Exception as e: + print(f"显示射线失败: {str(e)}") + + def _addHitMarker(self, lines, hitPos): + """在碰撞点添加标记""" + # 创建一个小十字标记 + marker_size = 0.5 + + # X方向线 + lines.moveTo(hitPos.x - marker_size, hitPos.y, hitPos.z) + lines.drawTo(hitPos.x + marker_size, hitPos.y, hitPos.z) + + # Y方向线 + lines.moveTo(hitPos.x, hitPos.y - marker_size, hitPos.z) + lines.drawTo(hitPos.x, hitPos.y + marker_size, hitPos.z) + + # Z方向线 + lines.moveTo(hitPos.x, hitPos.y, hitPos.z - marker_size) + lines.drawTo(hitPos.x, hitPos.y, hitPos.z + marker_size) + + def clearRay(self): + """清除当前显示的射线""" + if self.rayNode: + self.rayNode.removeNode() + self.rayNode = None + + # 同时清除可能存在的任务 + from direct.task.TaskManagerGlobal import taskMgr + taskMgr.remove("clearRay") + + def clearRayTask(self, task): + """清除射线的任务回调""" + self.clearRay() + return task.done + + def toggleRayDisplay(self): + """切换射线显示状态""" + self.showRay = not self.showRay + if not self.showRay: + self.clearRay() + print(f"射线显示: {'开启' if self.showRay else '关闭'}") + return self.showRay + + def mousePressEventLeft(self, evt): + """处理鼠标左键按下事件""" + print("\n=== 开始处理鼠标左键事件 ===") + #print(f"当前工具: {self.world.currentTool}") + + if not evt: + print("事件为空") + return + if self.world.currentTool == "地形编辑": + self._handleTerrainEdit(evt,"add") + return + + # 获取鼠标点击的位置 + x = evt.get('x', 0) + y = evt.get('y', 0) + #print(f"鼠标点击位置: ({x}, {y})") + + # 获取准确的窗口尺寸 + winWidth, winHeight = self.world.getWindowSize() + + # 直接使用 x, y 创建鼠标位置 + mx = 2.0 * x / float(winWidth) - 1.0 + my = 1.0 - 2.0 * y / float(winHeight) + #print(f"转换后的坐标: ({mx}, {my})") + + # 创建射线 + nearPoint = Point3() + farPoint = Point3() + self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) + #print(f"相机坐标系射线起点: {nearPoint}") + #print(f"相机坐标系射线终点: {farPoint}") + + # 将相机坐标系的点转换到世界坐标系 + worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) + worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) + #print(f"世界坐标系射线起点: {worldNearPoint}") + #print(f"世界坐标系射线终点: {worldFarPoint}") + + # 进行射线检测 + picker = CollisionTraverser() + queue = CollisionHandlerQueue() + + pickerNode = CollisionNode('mouseRay') + pickerNP = self.world.cam.attachNewNode(pickerNode) + # 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) + from panda3d.core import BitMask32 + pickerNode.setFromCollideMask(BitMask32.bit(2)) + + # 使用相机坐标系的点创建射线(因为pickerNP是相机的子节点) + direction = farPoint - nearPoint + direction.normalize() + pickerNode.addSolid(CollisionRay(nearPoint, direction)) + + picker.addCollider(pickerNP, queue) + picker.traverse(self.world.render) + + #print(f"碰撞检测结果数量: {queue.getNumEntries()}") + + # 射线检测结果处理 + hitPos = None + hitNode = None + + if queue.getNumEntries() > 0: + # 获取最近的碰撞点 + entry = queue.getEntry(0) + hitPos = entry.getSurfacePoint(self.world.render) + hitNode = entry.getIntoNodePath() + print(f"碰撞到节点: {hitNode.getName()}") + + # 显示射线(使用世界坐标系的点) + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + + # 处理ImGui鼠标点击 + if hasattr(self.world, 'imgui_manager') and self.world.imgui_manager: + if self.world.processImGuiMouseClick(x, y): + # 如果ImGui处理了点击,不再进行其他处理 + pickerNP.removeNode() + return + + # 优先检查是否点击了坐标轴 + #print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") + if self.world.selection.gizmo: + #print("准备检查坐标轴点击...") + try: + highlighted_axis = self.world.selection.gizmoHighlightAxis + if highlighted_axis: + print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽") + # 直接使用高亮轴开始拖拽 + self.world.selection.startGizmoDrag(highlighted_axis, x, y) + pickerNP.removeNode() + return + + # 如果没有高亮轴,再尝试检测点击 + gizmoAxis = self.world.selection.checkGizmoClick(x, y) + if gizmoAxis: + print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + # 开始坐标轴拖拽 + self.world.selection.startGizmoDrag(gizmoAxis, x, y) + pickerNP.removeNode() + return + else: + print("× 没有点击到坐标轴") + + # gizmoAxis = self.world.selection.checkGizmoClick(x, y) + # if gizmoAxis: + # #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + # # 开始坐标轴拖拽 + # self.world.selection.startGizmoDrag(gizmoAxis, x, y) + # pickerNP.removeNode() + # return + # else: + # print("× 没有点击到坐标轴") + except Exception as e: + print(f"❌ 坐标轴点击检测出现异常: {str(e)}") + import traceback + traceback.print_exc() + print("继续处理模型选择...") + + #print("继续处理碰撞结果...") + + if hitPos and hitNode: + #print(f"✓ 检测到碰撞,开始处理点击事件") + #print(f"GUI编辑模式: {self.world.guiEditMode}") + #print(f"当前工具: {self.world.currentTool}") + + # 处理GUI编辑模式 + if self.world.guiEditMode: + #print("处理GUI编辑模式点击") + # 检查是否点击了GUI元素 + clickedGUI = self.world.gui_manager.findClickedGUI(hitNode) + if clickedGUI: + # 选中GUI元素 + self.world.selection.updateSelection(clickedGUI) + self.world.gui_manager.selectGUIInTree(clickedGUI) + print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}") + elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool: + # 在点击位置创建新GUI元素 + self.world.gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool) + pickerNP.removeNode() + return + + # 根据当前工具处理点击事件 + if self.world.currentTool == "选择": + print("✓ 使用选择工具处理点击") + try: + self._handleSelectionClick(hitNode) + print("✓ 选择处理完成") + except Exception as e: + print(f"❌ 选择处理出现异常: {str(e)}") + import traceback + traceback.print_exc() + else: + print(f"当前工具不是'选择',无法处理: {self.world.currentTool}") + else: + print("没有检测到碰撞") + + # 如果不在GUI编辑模式,清除选择 + if not self.world.guiEditMode: + self.world.selection.updateSelection(None) + + # 在GUI编辑模式下,即使没有碰撞也可以在空白区域创建GUI + if (self.world.guiEditMode and + hasattr(self.world, 'currentGUITool') and + self.world.currentGUITool): + # 使用默认的地面高度创建GUI + default_height = 0.0 + world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换 + world_pos.setZ(default_height) + self.world.gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool) + + # 确保总是清理碰撞检测节点 + try: + pickerNP.removeNode() + print("✓ 碰撞检测节点已清理") + except Exception as e: + print(f"清理碰撞检测节点失败: {str(e)}") + + 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 _handleSelectionClick(self, hitNode): + """处理选择工具的点击事件""" + print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") + + # 查找对应的实际模型节点 + selectedModel = None + + # 如果点击的是碰撞节点,找到它的父模型 + if isinstance(hitNode.node(), CollisionNode): + print(f"点击的是碰撞节点: {hitNode.getName()}") + # 碰撞节点的父节点应该是模型 + parent = hitNode.getParent() + if parent in self.world.models: + selectedModel = parent + print(f"找到对应的模型: {selectedModel.getName()}") + else: + print(f"碰撞节点的父节点不是模型: {parent.getName()}") + else: + # 查找可选择的节点(模型或其子节点) + current = hitNode + while current != self.world.render: + # 检查是否是模型 + if current in self.world.models: + selectedModel = current + print(f"找到模型节点: {selectedModel.getName()}") + break + + # 检查是否是模型的子节点 + for model in self.world.models: + if current.getParent() == model or current.isAncestorOf(model): + selectedModel = model + print(f"找到父模型: {selectedModel.getName()}") + break + + if selectedModel: + break + + current = current.getParent() + + if selectedModel: + #print(f"✓ 最终选中模型: {selectedModel.getName()}") + self.world.selection.handleMouseClick(selectedModel) + + # 更新选择状态并显示选择框和坐标轴 + self.world.selection.updateSelection(selectedModel) + + # 在树形控件中查找并选中对应的项 + if self.world.interface_manager.treeWidget: + #print("查找树形控件中的对应项...") + root = self.world.interface_manager.treeWidget.invisibleRootItem() + foundItem = None + + for i in range(root.childCount()): + sceneItem = root.child(i) + if sceneItem.text(0) == "场景": + #print(f"在场景节点下查找...") + foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem) + if foundItem: + print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}") + try: + self.world.interface_manager.treeWidget.itemClicked.disconnect() + except TypeError: + pass + + self.world.interface_manager.treeWidget.setCurrentItem(foundItem) + + self.world.interface_manager.treeWidget.itemClicked.connect( + self.world.interface_manager.onTreeItemClicked) + else: + print("× 在树形控件中没有找到对应项") + break + + if not foundItem: + print("× 没有找到场景节点或对应的树形项") + else: + print("× 树形控件不存在") + else: + print("× 没有找到可选择的模型节点") + self.world.selection.updateSelection(None) + + def mouseReleaseEventLeft(self, evt): + """处理鼠标左键释放事件""" + # 处理坐标轴拖拽结束 + if self.world.selection.isDraggingGizmo: + self.world.selection.stopGizmoDrag() + return + + def wheelForward(self, data=None): + """处理滚轮向前滚动(前进)""" + # 调用CoreWorld的父类方法 + super(type(self.world), self.world).wheelForward(data) + + # 更新属性面板 + if (self.world.interface_manager.treeWidget and + self.world.interface_manager.treeWidget.currentItem() and + self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): + self.world.property_panel.updatePropertyPanel( + self.world.interface_manager.treeWidget.currentItem()) + + def wheelBackward(self, data=None): + """处理滚轮向后滚动(后退)""" + # 调用CoreWorld的父类方法 + super(type(self.world), self.world).wheelBackward(data) + + # 更新属性面板 + if (self.world.interface_manager.treeWidget and + self.world.interface_manager.treeWidget.currentItem() and + self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): + self.world.property_panel.updatePropertyPanel( + self.world.interface_manager.treeWidget.currentItem()) + + def mousePressEventMiddle(self, evt): + """处理鼠标中键按下事件""" + # 已移除原有的Z轴拖拽功能 + pass + + def mouseReleaseEventMiddle(self, evt): + """处理鼠标中键释放事件""" + # 已移除原有的Z轴拖拽功能 + pass + + def mouseMoveEvent(self, evt): + """处理鼠标移动事件""" + if not evt: + return + + # 处理坐标轴拖拽 + if self.world.selection.isDraggingGizmo: + 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) + + # 更新坐标轴拖拽 + self.world.selection.updateGizmoDrag(x, y) + return + + # 更新坐标轴高亮(鼠标悬停效果) + if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo: + x = evt.get('x', 0) + y = evt.get('y', 0) + # 减少高亮调试输出,只在需要时输出 + # 已静默处理,避免控制台刷屏 + self.world.selection.updateGizmoHighlight(x, y) + + # 调用CoreWorld的父类方法处理基础的相机旋转 super(type(self.world), self.world).mouseMoveEvent(evt) \ No newline at end of file diff --git a/core/imgui_manager.py b/core/imgui_manager.py new file mode 100644 index 00000000..eacc8ab1 --- /dev/null +++ b/core/imgui_manager.py @@ -0,0 +1,166 @@ +""" +pyimgui管理器 - 在Panda3D引擎上集成ImGui界面 +""" +import imgui +import OpenGL.GL as gl +from panda3d.core import RenderState, ShaderAttrib, Texture, CardMaker, NodePath +from panda3d.core import TransparencyAttrib, ColorBlendAttrib +from direct.task.Task import Task +import numpy as np + +class ImGuiManager: + """ImGui管理器,负责在Panda3D引擎上渲染ImGui界面""" + + def __init__(self, world): + self.world = world + self.io = None + self.font_texture = None + self._setup_imgui() + self._create_imgui_task() + + # UI状态 + self.show_demo_window = False + self.show_test_button = True + self.button_clicked = False + + def _setup_imgui(self): + """初始化ImGui设置""" + # 初始化ImGui上下文 + imgui.create_context() + self.io = imgui.get_io() + + # 设置显示大小 + win_props = self.world.win.getProperties() + self.io.display_size = (win_props.get_x_size(), win_props.get_y_size()) + + # 设置渲染回调 + self.io.render_callback = self._render_imgui + + # 创建字体纹理 + self._create_font_texture() + + print("✓ ImGui初始化完成") + + def _create_font_texture(self): + """创建ImGui字体纹理""" + # 获取字体数据 + font_pixels = self.io.fonts.get_tex_data_as_rgba32() + width, height = self.io.fonts.get_tex_data_as_rgba32()[1] + + # 创建Panda3D纹理 + self.font_texture = Texture() + self.font_texture.setup_2d_texture(width, height, Texture.T_unsigned_byte, Texture.F_rgba8) + self.font_texture.set_ram_image(font_pixels) + + # 设置纹理ID + self.io.fonts.tex_id = self.font_texture + + # 清除字体数据 + self.io.fonts.clear_tex_data() + + def _render_imgui(self, draw_data): + """渲染ImGui绘制数据""" + if not draw_data: + return + + # 设置OpenGL状态 + gl.glEnable(gl.GL_BLEND) + gl.glBlendEquation(gl.GL_FUNC_ADD) + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) + gl.glDisable(gl.GL_CULL_FACE) + gl.glDisable(gl.GL_DEPTH_TEST) + gl.glEnable(gl.GL_SCISSOR_TEST) + + # 设置视口和投影矩阵 + display_w, display_h = self.io.display_size + gl.glViewport(0, 0, int(display_w), int(display_h)) + + # 获取投影矩阵 + projection_matrix = self._get_projection_matrix() + gl.glMatrixMode(gl.GL_PROJECTION) + gl.glLoadMatrixf(projection_matrix) + + # 渲染每个命令列表 + for commands in draw_data.commands_lists: + # 渲染命令 + self._render_commands(commands) + + # 恢复OpenGL状态 + gl.glDisable(gl.GL_SCISSOR_TEST) + gl.glEnable(gl.GL_DEPTH_TEST) + + def _get_projection_matrix(self): + """获取正交投影矩阵""" + L, R, T, B = 0, self.io.display_size[0], self.io.display_size[1], 0 + projection_matrix = np.array([ + [2.0/(R-L), 0.0, 0.0, 0.0], + [0.0, 2.0/(T-B), 0.0, 0.0], + [0.0, 0.0, -1.0, 0.0], + [(R+L)/(L-R), (T+B)/(B-T), 0.0, 1.0] + ], dtype=np.float32) + return projection_matrix + + def _render_commands(self, commands): + """渲染ImGui命令列表""" + # 这里需要实现具体的渲染逻辑 + # 由于Panda3D的渲染管线与ImGui不完全兼容,我们需要创建一个自定义的渲染方法 + pass + + def _create_imgui_task(self): + """创建ImGui更新任务""" + taskMgr.add(self._update_imgui, "update_imgui") + + def _update_imgui(self, task): + """更新ImGui界面""" + # 开始新帧 + imgui.new_frame() + + # 绘制UI + self._draw_ui() + + # 渲染ImGui + imgui.render() + + return Task.cont + + def _draw_ui(self): + """绘制UI元素""" + # 绘制测试按钮 + if self.show_test_button: + imgui.begin("测试窗口", True) + if imgui.button("点击我!"): + self.button_clicked = True + print("按钮被点击了!") + + if self.button_clicked: + imgui.text("按钮已被点击!") + + imgui.end() + + # 绘制演示窗口(可选) + if self.show_demo_window: + self.show_demo_window = imgui.show_demo_window(self.show_demo_window) + + def toggle_demo_window(self): + """切换演示窗口显示""" + self.show_demo_window = not self.show_demo_window + + def toggle_test_button(self): + """切换测试按钮显示""" + self.show_test_button = not self.show_test_button + + def process_mouse_input(self, button_event): + """处理鼠标输入""" + # 这里需要将Panda3D的鼠标事件转换为ImGui可以理解的事件 + pass + + def process_keyboard_input(self, key_event): + """处理键盘输入""" + # 这里需要将Panda3D的键盘事件转换为ImGui可以理解的事件 + pass + + def cleanup(self): + """清理资源""" + if self.font_texture: + self.font_texture = None + imgui.destroy_context() \ No newline at end of file diff --git a/core/imgui_style_manager.py b/core/imgui_style_manager.py new file mode 100644 index 00000000..42c10391 --- /dev/null +++ b/core/imgui_style_manager.py @@ -0,0 +1,286 @@ +""" +ImGui样式管理器 +用于统一管理ImGui UI样式,与主程序Qt UI保持一致 +""" + +import imgui +from imgui_bundle import imgui_ctx +import os +import platform +from pathlib import Path +from panda3d.core import Filename + + +class ImGuiStyleManager: + """ImGui样式管理器 - 负责UI样式的统一管理""" + + def __init__(self, imgui_backend): + """ + 初始化样式管理器 + + Args: + imgui_backend: ImGui后端对象 + """ + self.imgui_backend = imgui_backend + self.io = imgui_backend.io + self.style = None # 延迟初始化,在apply_style中设置 + + # 颜色定义 - 与Qt UI保持一致 + self.colors = { + # 主要颜色 + 'primary': (0.188, 0.404, 0.753, 1.0), # #3067C0 - 主色调 + 'primary_dark': (0.149, 0.337, 0.627, 1.0), # #2556A0 - 按钮按下 + 'background': (0.0, 0.0, 0.0, 1.0), # #000000 - 主背景 + 'secondary_bg': (0.096, 0.096, 0.106, 1.0), # #19191B - 次背景 + 'panel_bg': (0.18, 0.188, 0.208, 1.0), # #2E3035 - 面板背景 + 'text': (0.922, 0.922, 0.922, 1.0), # #EBEBEB - 主文字 + 'text_secondary': (0.922, 0.922, 0.922, 0.6), # #EBEBEB - 次文字 + 'border': (0.243, 0.243, 0.259, 1.0), # #3E3E42 - 主要边框 + 'border_secondary': (0.173, 0.184, 0.212, 1.0), # #2C2F36 - 次要边框 + 'button_bg': (0.349, 0.384, 0.463, 0.5), # 半透明按钮背景 + 'input_bg': (0.349, 0.392, 0.443, 0.2), # 输入框背景 + 'input_border': (0.298, 0.361, 0.431, 0.6), # 输入框边框 + 'success': (0.176, 1.0, 0.769, 1.0), # #2dffc4 - 成功状态 + 'warning': (0.953, 0.616, 0.471, 1.0), # #f39d78 - 警告状态 + 'info': (0.157, 0.620, 1.0, 1.0), # #289eff - 信息状态 + } + + # 尺寸定义 + self.sizes = { + 'font_size': 12.0, # 与Qt一致的字体大小 + 'window_padding': (8.0, 8.0), + 'window_rounding': 2.0, + 'item_spacing': (6.0, 6.0), + 'item_inner_spacing': (4.0, 4.0), + 'frame_padding': (4.0, 3.0), + 'frame_rounding': 2.0, + 'indent_spacing': 20.0, + 'scrollbar_size': 14.0, + } + + # 字体设置在demo.py中直接处理,这里不再初始化 + + def _setup_fonts(self): + """设置字体,包括中文字体支持""" + try: + # 尝试加载中文字体 + font_path = self._get_chinese_font_path() + if font_path: + # 使用p3dimgui的字体加载方法 + self.imgui_backend.load_font(font_path, self.sizes['font_size']) + print(f"✓ ImGui中文字体加载成功: {font_path}") + else: + print("⚠ 无法加载中文字体,使用默认字体") + + except Exception as e: + print(f"⚠ ImGui字体设置失败: {e}") + # 备用方案:尝试使用默认字体 + try: + # 使用p3dimgui的默认字体 + self.imgui_backend.load_font(None, self.sizes['font_size']) + except: + pass + + def _get_chinese_font_path(self): + """获取中文字体路径""" + system = platform.system().lower() + project_root = Path(__file__).resolve().parent.parent + + # 候选字体路径 + if system == "windows": + win_dir = os.environ.get("WINDIR") or r"C:\Windows" + font_candidates = [ + project_root / "RenderPipelineFile" / "data" / "font" / "msyh.ttc", + Path(win_dir) / "Fonts" / "msyh.ttc", + Path(win_dir) / "Fonts" / "msyh.ttf", + Path(win_dir) / "Fonts" / "simhei.ttf", + ] + elif system == "darwin": + font_candidates = [ + Path("/System/Library/Fonts/PingFang.ttc"), + Path("/System/Library/Fonts/STHeiti.ttc"), + ] + else: # Linux + font_candidates = [ + Path("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"), + Path("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"), + ] + + # 尝试找到存在的字体文件 + for font_path in font_candidates: + if font_path.exists(): + return str(font_path) + + return None + + def apply_style(self): + """应用与Qt UI一致的样式""" + # 先应用深色主题作为基础 + imgui.style_colors_dark() + + # 获取样式对象 + self.style = imgui.get_style() + style = self.style + + # 应用自定义颜色 + style.colors[imgui.COLOR_WINDOW_BG] = self.colors['background'] + style.colors[imgui.COLOR_CHILD_BG] = self.colors['secondary_bg'] + style.colors[imgui.COLOR_POPUP_BG] = self.colors['panel_bg'] + style.colors[imgui.COLOR_BORDER] = self.colors['border'] + style.colors[imgui.COLOR_BORDER_SHADOW] = self.colors['border_secondary'] + + # 文本颜色 + style.colors[imgui.COLOR_TEXT] = self.colors['text'] + style.colors[imgui.COLOR_TEXT_DISABLED] = self.colors['text_secondary'] + style.colors[imgui.COLOR_TEXT_SELECTED_BG] = self.colors['primary'] + + # 菜单栏颜色 + style.colors[imgui.COLOR_MENU_BAR_BG] = self.colors['background'] + + # 按钮颜色 + style.colors[imgui.COLOR_BUTTON] = self.colors['button_bg'] + style.colors[imgui.COLOR_BUTTON_HOVERED] = self.colors['primary'] + style.colors[imgui.COLOR_BUTTON_ACTIVE] = self.colors['primary_dark'] + + # 复选框和单选按钮 + style.colors[imgui.COLOR_CHECK_MARK] = self.colors['primary'] + style.colors[imgui.COLOR_RADIO_BUTTON_HOVERED] = self.colors['primary'] + style.colors[imgui.COLOR_RADIO_BUTTON_ACTIVE] = self.colors['primary_dark'] + + # 滑块 + style.colors[imgui.COLOR_SLIDER_GRAB] = self.colors['primary'] + style.colors[imgui.COLOR_SLIDER_GRAB_ACTIVE] = self.colors['primary_dark'] + + # 进度条 + style.colors[imgui.COLOR_PROGRESS_BAR_FG] = self.colors['primary'] + + # 标题栏 + style.colors[imgui.COLOR_TITLE_BG] = self.colors['panel_bg'] + style.colors[imgui.COLOR_TITLE_BG_ACTIVE] = self.colors['primary'] + style.colors[imgui.COLOR_TITLE_BG_COLLAPSED] = self.colors['border_secondary'] + + # 输入框 + style.colors[imgui.COLOR_FRAME_BG] = self.colors['input_bg'] + style.colors[imgui.COLOR_FRAME_BG_HOVERED] = self.colors['input_border'] + style.colors[imgui.COLOR_FRAME_BG_ACTIVE] = ( + self.colors['primary'][0], + self.colors['primary'][1], + self.colors['primary'][2], + 0.1 # 低透明度 + ) + + # 标签页 + style.colors[imgui.COLOR_TAB] = self.colors['button_bg'] + style.colors[imgui.COLOR_TAB_HOVERED] = self.colors['primary'] + style.colors[imgui.COLOR_TAB_ACTIVE] = self.colors['primary'] + style.colors[imgui.COLOR_TAB_UNFOCUSED] = self.colors['button_bg'] + style.colors[imgui.COLOR_TAB_UNFOCUSED_ACTIVE] = self.colors['primary_dark'] + + # 选择高亮 + style.colors[imgui.COLOR_HEADER] = self.colors['primary'] + style.colors[imgui.COLOR_HEADER_HOVERED] = self.colors['primary'] + style.colors[imgui.COLOR_HEADER_ACTIVE] = self.colors['primary_dark'] + + # 分隔符 + style.colors[imgui.COLOR_SEPARATOR] = self.colors['border_secondary'] + style.colors[imgui.COLOR_SEPARATOR_HOVERED] = self.colors['border'] + style.colors[imgui.COLOR_SEPARATOR_ACTIVE] = self.colors['primary'] + + # 调整尺寸和间距 + style.window_padding = self.sizes['window_padding'] + style.window_rounding = self.sizes['window_rounding'] + style.window_min_size = (200, 100) + style.child_rounding = self.sizes['frame_rounding'] + style.frame_padding = self.sizes['frame_padding'] + style.frame_rounding = self.sizes['frame_rounding'] + style.item_spacing = self.sizes['item_spacing'] + style.item_inner_spacing = self.sizes['item_inner_spacing'] + style.indent_spacing = self.sizes['indent_spacing'] + style.scrollbar_size = self.sizes['scrollbar_size'] + style.scrollbar_rounding = self.sizes['frame_rounding'] + style.grab_min_size = 10.0 + style.grab_rounding = self.sizes['frame_rounding'] + + # 禁用一些ImGui的默认效果,使其更像Qt + style.window_border_size = 1.0 + style.child_border_size = 1.0 + style.popup_border_size = 1.0 + style.frame_border_size = 1.0 + + print("✓ ImGui样式已应用,与Qt UI保持一致") + + def get_window_flags(self, window_type="default"): + """获取不同类型窗口的标志""" + base_flags = 0 + + if window_type == "main_menu": + return imgui.WINDOW_MENU_BAR + elif window_type == "dockable": + return (base_flags | + imgui.WINDOW_NO_TITLE_BAR | + imgui.WINDOW_NO_RESIZE | + imgui.WINDOW_NO_MOVE | + imgui.WINDOW_NO_COLLAPSE) + elif window_type == "toolbar": + return (base_flags | + imgui.WINDOW_NO_TITLE_BAR | + imgui.WINDOW_NO_RESIZE | + imgui.WINDOW_NO_MOVE | + imgui.WINDOW_NO_COLLAPSE | + imgui.WINDOW_NO_SCROLLBAR) + elif window_type == "panel": + return (base_flags | + imgui.WINDOW_NO_COLLAPSE) + + return base_flags + + def begin_styled_window(self, name, open=True, flags=None, window_type="default"): + """开始一个带样式的窗口""" + if flags is None: + flags = self.get_window_flags(window_type) + + return imgui_ctx.begin(name, open, flags) + + def styled_button(self, label, size=(0, 0)): + """绘制带样式的按钮""" + return imgui.button(label, size) + + def styled_input_text(self, label, value, buffer_size=256): + """绘制带样式的文本输入框""" + changed, new_value = imgui.input_text(label, value, buffer_size) + return changed, new_value + + def styled_slider_float(self, label, value, min_val, max_val, format="%.3f"): + """绘制带样式的浮点滑块""" + changed, new_value = imgui.slider_float(label, value, min_val, max_val, format) + return changed, new_value + + def load_icon(self, icon_name): + """加载图标纹理""" + try: + # 构建图标路径 + project_root = Path(__file__).resolve().parent.parent + icon_path = project_root / "icons" / f"{icon_name}.png" + + if icon_path.exists(): + return self.imgui_backend.loadTexture(str(icon_path)) + else: + print(f"⚠ 图标文件不存在: {icon_path}") + return None + except Exception as e: + print(f"⚠ 加载图标失败: {e}") + return None + + def image_button(self, texture_id, size=(32, 32), bg_col=(0, 0, 0, 0), tint_col=(1, 1, 1, 1)): + """绘制图像按钮""" + return imgui.image_button(texture_id, size, bg_col, tint_col) + + def get_icon_text_button(self, icon_texture, text, size=(0, 0)): + """绘制带图标的文本按钮""" + if icon_texture: + # 先绘制图标 + imgui.image(icon_texture, (16, 16)) + imgui.same_line() + + # 再绘制文本按钮 + return imgui.button(text, size) \ No newline at end of file diff --git a/core/simple_imgui_manager.py b/core/simple_imgui_manager.py new file mode 100644 index 00000000..55a68415 --- /dev/null +++ b/core/simple_imgui_manager.py @@ -0,0 +1,204 @@ +""" +pyimgui管理器 - 在Panda3D引擎上集成ImGui界面 +使用更简单的方法,通过创建覆盖层来实现 +""" +import imgui +import numpy as np +from panda3d.core import RenderState, ShaderAttrib, Texture, CardMaker, NodePath +from panda3d.core import TransparencyAttrib, ColorBlendAttrib, PNMImage +from panda3d.core import GraphicsOutput, RenderBuffer, TextureStage, TexGenAttrib +from direct.task.Task import Task +from direct.gui.OnscreenImage import OnscreenImage +from direct.gui.OnscreenText import OnscreenText +import PIL.Image +import PIL.ImageDraw +import io + +class SimpleImGuiManager: + """简化的ImGui管理器,使用2D覆盖层在Panda3D引擎上显示ImGui界面""" + + def __init__(self, world): + self.world = world + self.io = None + self._setup_imgui() + self._create_imgui_task() + + # UI状态 + self.show_demo_window = False + self.show_test_button = True + self.button_clicked = False + self.counter = 0 + + # 渲染相关 + self.imgui_texture = None + self.imgui_image = None + self.texture_buffer = None + + # 创建渲染纹理 + self._create_render_texture() + + def _setup_imgui(self): + """初始化ImGui设置""" + # 初始化ImGui上下文 + imgui.create_context() + self.io = imgui.get_io() + + # 设置显示大小 + win_props = self.world.win.getProperties() + self.io.display_size = (win_props.get_x_size(), win_props.get_y_size()) + + # 设置字体大小 + font_size = 16 + self.io.font_global_scale = font_size / 13.0 + + print("✓ ImGui初始化完成") + + def _create_render_texture(self): + """创建用于渲染ImGui的纹理""" + win_props = self.world.win.getProperties() + width = win_props.get_x_size() + height = win_props.get_y_size() + + # 创建纹理 + self.imgui_texture = Texture() + self.imgui_texture.setup_2d_texture(width, height, Texture.T_unsigned_byte, Texture.F_rgba8) + + # 创建屏幕图像 + self.imgui_image = OnscreenImage( + image=self.imgui_texture, + pos=(0, 0, 0), + scale=(1, 1, 1) + ) + self.imgui_image.setTransparency(TransparencyAttrib.MAlpha) + + def _create_imgui_task(self): + """创建ImGui更新任务""" + taskMgr.add(self._update_imgui, "update_imgui") + + def _update_imgui(self, task): + """更新ImGui界面""" + # 开始新帧 + imgui.new_frame() + + # 绘制UI + self._draw_ui() + + # 渲染ImGui到纹理 + self._render_to_texture() + + return Task.cont + + def _draw_ui(self): + """绘制UI元素""" + # 绘制测试窗口 + if self.show_test_button: + imgui.begin("测试窗口", True) + + if imgui.button("点击我!"): + self.button_clicked = True + self.counter += 1 + print(f"按钮被点击了! 计数: {self.counter}") + + if self.button_clicked: + imgui.text(f"按钮已被点击 {self.counter} 次!") + + imgui.text("这是一个在Panda3D引擎上显示的ImGui按钮") + imgui.text(f"帧率: {globalClock.getAverageFrameRate():.1f} FPS") + + imgui.end() + + # 绘制演示窗口(可选) + if self.show_demo_window: + self.show_demo_window = imgui.show_demo_window(self.show_demo_window) + + def _render_to_texture(self): + """将ImGui渲染到纹理""" + # 渲染ImGui + imgui.render() + + # 获取绘制数据 + draw_data = imgui.get_draw_data() + + if draw_data: + # 创建PIL图像 + width, height = int(self.io.display_size[0]), int(self.io.display_size[1]) + image = PIL.Image.new('RGBA', (width, height), (0, 0, 0, 0)) + draw = PIL.ImageDraw.Draw(image) + + # 简单的文本渲染(这里只是示例,实际需要完整的ImGui渲染) + if self.show_test_button: + # 绘制简单的按钮模拟 + button_x, button_y = 50, 50 + button_width, button_height = 100, 30 + draw.rectangle([button_x, button_y, button_x + button_width, button_y + button_height], + fill=(100, 100, 200, 255), outline=(255, 255, 255, 255)) + draw.text((button_x + 10, button_y + 5), "点击我!", fill=(255, 255, 255, 255)) + + # 显示点击计数 + if self.counter > 0: + draw.text((button_x, button_y + button_height + 10), + f"点击次数: {self.counter}", fill=(255, 255, 255, 255)) + + # 将PIL图像转换为Panda3D纹理 + img_buffer = io.BytesIO() + image.save(img_buffer, format='RGBA') + img_buffer.seek(0) + + pnm_image = PNMImage() + pnm_image.read(StringStream(img_buffer.read())) + self.imgui_texture.load(pnm_image) + + def toggle_demo_window(self): + """切换演示窗口显示""" + self.show_demo_window = not self.show_demo_window + + def toggle_test_button(self): + """切换测试按钮显示""" + self.show_test_button = not self.show_test_button + + def process_mouse_click(self, x, y): + """处理鼠标点击""" + # 检查是否点击了按钮区域 + if self.show_test_button: + button_x, button_y = 50, 50 + button_width, button_height = 100, 30 + + # 转换坐标系(Panda3D坐标系原点在屏幕中心) + screen_x = (x + self.io.display_size[0] / 2) + screen_y = (self.io.display_size[1] / 2 - y) + + if (button_x <= screen_x <= button_x + button_width and + button_y <= screen_y <= button_y + button_height): + self.button_clicked = True + self.counter += 1 + print(f"按钮被点击了! 计数: {self.counter}") + + def cleanup(self): + """清理资源""" + if self.imgui_image: + self.imgui_image.removeNode() + if self.imgui_texture: + self.imgui_texture = None + imgui.destroy_context() + +# 辅助类用于内存中的字符串流 +class StringStream: + """简单的内存字符串流,用于PNMImage读取""" + def __init__(self, data): + self.data = data + self.position = 0 + + def read(self, size=-1): + if size == -1: + result = self.data[self.position:] + self.position = len(self.data) + else: + result = self.data[self.position:self.position + size] + self.position += size + return result + + def seek(self, position): + self.position = position + + def tell(self): + return self.position \ No newline at end of file diff --git a/demo.py b/demo.py new file mode 100644 index 00000000..441c1d45 --- /dev/null +++ b/demo.py @@ -0,0 +1,679 @@ +from panda3d.core import loadPrcFileData, WindowProperties, Point3 +from math import pi, sin, cos + +from direct.showbase.ShowBase import ShowBase +from direct.task import Task +from direct.actor.Actor import Actor +from direct.interval.IntervalGlobal import Sequence + +import p3dimgui + +from imgui_bundle import imgui, imgui_ctx + +import sys +import warnings + +# 导入MyWorld类和必要的模块 +from core.world import CoreWorld +from core.selection import SelectionSystem +from core.event_handler import EventHandler +from core.tool_manager import ToolManager +from core.script_system import ScriptManager +from core.patrol_system import PatrolSystem +from core.Command_System import CommandManager +from gui.gui_manager import GUIManager +from core.terrain_manager import TerrainManager +from scene.scene_manager import SceneManager +from project.project_manager import ProjectManager +from core.InfoPanelManager import InfoPanelManager +from core.collision_manager import CollisionManager +try: + # 尝试导入视频管理器,避免循环导入 + import importlib.util + spec = importlib.util.spec_from_file_location("video_integration", "demo/video_integration.py") + video_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(video_module) + VideoManager = video_module.VideoManager +except: + # 如果video_integration模块不存在,则跳过 + VideoManager = None + print("⚠ 视频管理器模块未找到,视频功能将不可用") + +warnings.filterwarnings("ignore", category=DeprecationWarning) + +class MyWorld(CoreWorld): + def __init__(self): + super().__init__() + + # 初始化选择和变换系统 + self.selection = SelectionSystem(self) + + # 绑定F键用于聚焦选中节点 + self.accept("f", self.onFocusKeyPressed) + self.accept("F", self.onFocusKeyPressed) # 大写F + + #初始化巡检系统 + self.patrol_system = PatrolSystem(self) + self.accept("p",self.onPatrolKeyPressed) + self.accept("P",self.onPatrolKeyPressed) + + # 初始化事件处理系统 + self.event_handler = EventHandler(self) + + # 初始化工具管理系统 + self.tool_manager = ToolManager(self) + + # 初始化脚本管理系统 + self.script_manager = ScriptManager(self) + + # 初始化GUI管理系统 + self.gui_manager = GUIManager(self) + + # 初始化视频管理 + if VideoManager is not None: + self.video_manager = VideoManager(self) + else: + self.video_manager = None + + # 初始化场景管理系统 + self.scene_manager = SceneManager(self) + + # 初始化项目管理系统 + self.project_manager = ProjectManager(self) + + self.info_panel_manager = InfoPanelManager(self) + + self.command_manager = CommandManager() + + # 初始化碰撞管理器 + self.collision_manager = CollisionManager(self) + + # 初始化VR管理器 + try: + from core.vr import VRManager + self.vr_manager = VRManager(self) + print("✓ VR管理器初始化完成") + except Exception as e: + print(f"⚠ VR管理器初始化失败: {e}") + self.vr_manager = None + + # 调试选项 + self.debug_collision = True # 是否显示碰撞体 + + # 默认启用模型间碰撞检测(可选) + self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5) + + # 启动脚本系统 + self.script_manager.start_system() + + self.terrain_manager = TerrainManager(self) + + self.terrain_edit_radius = 3.0 + self.terrain_edit_strength=0.3 + self.terrain_edit_operation = "add" + + # Install Dear ImGui + p3dimgui.init() + + # 初始化样式管理器 + from core.imgui_style_manager import ImGuiStyleManager + self.style_manager = ImGuiStyleManager(self.imgui) + + # 尝试直接设置中文字体 + try: + import platform + from pathlib import Path + + # 获取中文字体路径 + system = platform.system().lower() + if system == "linux": + font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc" + elif system == "windows": + font_path = "C:/Windows/Fonts/msyh.ttc" + elif system == "darwin": + font_path = "/System/Library/Fonts/PingFang.ttc" + else: + font_path = None + + if font_path and Path(font_path).exists(): + # 先清除默认字体 + self.imgui.io.fonts.clear() + + # 创建中文字符范围(基本中文字符) + # 这个范围包含了常用中文字符的Unicode范围 + chinese_ranges = [] + # 基本拉丁字母 + for i in range(0x0020, 0x00FF): + chinese_ranges.append(i) + # 中文字符范围 + for i in range(0x4E00, 0x9FFF): # CJK统一汉字 + chinese_ranges.append(i) + for i in range(0x3400, 0x4DBF): # CJK扩展A + chinese_ranges.append(i) + for i in range(0x20000, 0x2A6DF): # CJK扩展B + chinese_ranges.append(i) + for i in range(0x2A700, 0x2B73F): # CJK扩展C + chinese_ranges.append(i) + for i in range(0x2B740, 0x2B81F): # CJK扩展D + chinese_ranges.append(i) + for i in range(0x2B820, 0x2CEAF): # CJK扩展E + chinese_ranges.append(i) + for i in range(0x2CEB0, 0x2EBEF): # CJK扩展F + chinese_ranges.append(i) + for i in range(0x3000, 0x303F): # CJK符号和标点 + chinese_ranges.append(i) + for i in range(0xFF00, 0xFFEF): # 全角字符 + chinese_ranges.append(i) + + # 添加中文字体(不指定字符范围,让ImGui自动处理) + font = self.imgui.io.fonts.add_font_from_file_ttf(font_path, 14.0) + print(f"✓ 直接设置中文字体成功: {font_path}") + print(f" 使用自动字符范围") + else: + print("⚠ 中文字体文件不存在") + except Exception as e: + print(f"⚠ 直接设置中文字体失败: {e}") + # 备用方案:使用默认字体 + try: + self.imgui.io.fonts.add_font_default() + print("✓ 使用默认字体") + except: + pass + + # Disable the camera trackball controls. + self.disableMouse() + + self.showDemoWindow = True + + # UI状态管理 + self.showSceneTree = True + self.showPropertyPanel = True + self.showConsole = True + self.showScriptPanel = True + self.showToolbar = True + + self.accept('imgui-new-frame', self.__newFrame) + self.accept('`', self.__toggleImgui) + + self.testTexture = None + self.icons = {} # 初始化图标字典 + + print("✓ MyWorld 初始化完成") + + # ==================== 兼容性属性 ==================== + + # 保留models属性以兼容现有代码 + @property + def models(self): + """模型列表的兼容性属性""" + return self.scene_manager.models + + @models.setter + def models(self, value): + """模型列表的兼容性设置器""" + self.scene_manager.models = value + + # 保留gui_elements属性以兼容现有代码 + @property + def gui_elements(self): + """GUI元素列表的兼容性属性""" + return self.gui_manager.gui_elements + + @gui_elements.setter + def gui_elements(self, value): + """GUI元素列表的兼容性设置器""" + self.gui_manager.gui_elements = value + + # 保留guiEditMode属性以兼容现有代码 + @property + def guiEditMode(self): + """GUI编辑模式的兼容性属性""" + return self.gui_manager.guiEditMode + + @guiEditMode.setter + def guiEditMode(self, value): + """GUI编辑模式的兼容性设置器""" + self.gui_manager.guiEditMode = value + + # 保留currentTool属性以兼容现有代码 + @property + def currentTool(self): + """当前工具的兼容性属性""" + return self.tool_manager.currentTool + + @currentTool.setter + def currentTool(self, value): + """当前工具的兼容性设置器""" + self.tool_manager.currentTool = value + + # 保留terrains属性以兼容现有代码 + @property + def terrains(self): + """地形列表的兼容性属性""" + return self.terrain_manager.terrains + + @terrains.setter + def terrains(self,value): + """地形列表的兼容性设置器""" + self.terrain_manager.terrains = value + + def onFocusKeyPressed(self): + """处理 F 键按下事件""" + try: + if hasattr(self, 'selection') and self.selection.selectedNode: + self.selection.focusCameraOnSelectedNodeAdvanced() + else: + print("当前没有选中任何节点") + except Exception as e: + print(f"处理 F 键事件失败: {e}") + + def onPatrolKeyPressed(self): + """处理 P 键按下事件 - 控制巡检系统""" + try: + print("检测到 P 键按下") + if not self.patrol_system.is_patrolling: + if not self.patrol_system.patrol_points: + self.createDefaultPatrolRoute() + if self.patrol_system.start_patrol(): + print("✓ 巡检已开始") + else: + print("✗ 巡检启动失败") + else: + if self.patrol_system.stop_patrol(): + print("✓ 巡检已停止") + else: + print("✗ 巡检停止失败") + except Exception as e: + print(f"处理 P 键事件失败: {e}") + + def createDefaultPatrolRoute(self): + """创建默认巡检路线""" + try: + self.patrol_system.clear_patrol_points() + self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5) + print("✓ 默认自动朝向巡检路线已创建") + self.patrol_system.list_patrol_points() + except Exception as e: + print(f"创建默认自动朝向巡检路线失败: {e}") + + def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5): + """启用模型间碰撞检测""" + return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold) + + def __toggleImgui(self): + if not self.imgui.isKeyboardCaptured(): + self.imgui.toggle() + + def __newFrame(self): + # Dear ImGui commands can be placed here. + + # 在第一帧应用样式 + if imgui.get_frame_count() == 0: + self.style_manager.apply_style() + # 加载图标 + self.icons = { + 'select': self.style_manager.load_icon('select_tool'), + 'move': self.style_manager.load_icon('move_tool'), + 'rotate': self.style_manager.load_icon('rotate_tool'), + 'scale': self.style_manager.load_icon('scale_tool'), + 'success': self.style_manager.load_icon('success_icon'), + 'warning': self.style_manager.load_icon('warning_icon'), + } + + # 确保中文字体被正确应用 + try: + # 强制刷新字体 + self.imgui.io.fonts.clear() + # 重新添加中文字体 + import platform + from pathlib import Path + + system = platform.system().lower() + if system == "linux": + font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc" + elif system == "windows": + font_path = "C:/Windows/Fonts/msyh.ttc" + elif system == "darwin": + font_path = "/System/Library/Fonts/PingFang.ttc" + + if font_path and Path(font_path).exists(): + # 添加中文字体,明确指定中文字符集 + try: + # 获取中文字符集 + chinese_ranges = imgui.get_io().fonts.get_glyph_ranges_chinese_full() + self.imgui.io.fonts.add_font_from_file_ttf(font_path, 14.0, None, chinese_ranges) + print("✓ 第一帧重新设置中文字体(包含中文字符集)") + except: + # 如果获取字符集失败,使用简单方式 + self.imgui.io.fonts.add_font_from_file_ttf(font_path, 14.0) + print("✓ 第一帧重新设置中文字体(简单方式)") + except Exception as e: + print(f"⚠ 第一帧设置中文字体失败: {e}") + + # 绘制菜单栏 + self._draw_menu_bar() + + # 绘制工具栏 + if self.showToolbar: + self._draw_toolbar() + + # 绘制场景树面板 + if self.showSceneTree: + self._draw_scene_tree() + + # 绘制属性面板 + if self.showPropertyPanel: + self._draw_property_panel() + + # 绘制控制台面板 + if self.showConsole: + self._draw_console() + + # 绘制脚本面板 + if self.showScriptPanel: + self._draw_script_panel() + + # 绘制纹理测试窗口 + if self.testTexture: + with self.style_manager.begin_styled_window("Texture Test", True, + imgui.WINDOW_ALWAYS_AUTO_RESIZE) as (_, windowOpen): + if not windowOpen: + self.imgui.removeTexture(self.testTexture) + self.testTexture = None + return + imgui.image(self.testTexture, (256, 256)) + + # 绘制ImGui演示窗口 + if self.showDemoWindow: + imgui.show_demo_window() + + + + def _draw_menu_bar(self): + """绘制菜单栏""" + with imgui_ctx.begin_main_menu_bar() as main_menu: + if main_menu: + # 文件菜单 + with imgui_ctx.begin_menu("文件") as file_menu: + if file_menu: + imgui.menu_item("新建项目", "Ctrl+N", False, True) + imgui.menu_item("打开项目", "Ctrl+O", False, True) + imgui.separator() + imgui.menu_item("保存", "Ctrl+S", False, True) + imgui.menu_item("另存为", "", False, True) + imgui.separator() + imgui.menu_item("退出", "Alt+F4", False, True) + + # 编辑菜单 + with imgui_ctx.begin_menu("编辑") as edit_menu: + if edit_menu: + imgui.menu_item("撤销", "Ctrl+Z", False, True) + imgui.menu_item("重做", "Ctrl+Y", False, True) + imgui.separator() + imgui.menu_item("复制", "Ctrl+C", False, True) + imgui.menu_item("粘贴", "Ctrl+V", False, True) + imgui.menu_item("删除", "Del", False, True) + + # 视图菜单 + with imgui_ctx.begin_menu("视图") as view_menu: + if view_menu: + _, self.showToolbar = imgui.menu_item("工具栏", "", self.showToolbar, True) + _, self.showSceneTree = imgui.menu_item("场景树", "", self.showSceneTree, True) + _, self.showPropertyPanel = imgui.menu_item("属性面板", "", self.showPropertyPanel, True) + _, self.showConsole = imgui.menu_item("控制台", "", self.showConsole, True) + _, self.showScriptPanel = imgui.menu_item("脚本管理", "", self.showScriptPanel, True) + + # 工具菜单 + with imgui_ctx.begin_menu("工具") as tools_menu: + if tools_menu: + imgui.menu_item("导入模型", "", False, True) + imgui.menu_item("地形编辑器", "", False, True) + imgui.menu_item("材质编辑器", "", False, True) + imgui.menu_item("脚本编辑器", "", False, True) + + # 窗口菜单 + with imgui_ctx.begin_menu("窗口") as window_menu: + if window_menu: + _, self.showDemoWindow = imgui.menu_item("ImGui演示", "", self.showDemoWindow, True) + if self.testTexture: + imgui.menu_item("关闭纹理测试", "", False, True) + else: + imgui.menu_item("显示纹理测试", "", False, True) + + # 帮助菜单 + with imgui_ctx.begin_menu("帮助") as help_menu: + if help_menu: + imgui.menu_item("关于", "", False, True) + imgui.menu_item("文档", "", False, True) + + # 右侧显示FPS + imgui.set_cursor_pos_x(imgui.get_window_size().x - 140) + imgui.text("%.2f FPS (%.2f ms)" % (imgui.get_io().framerate, 1000.0 / imgui.get_io().framerate)) + + def _draw_toolbar(self): + """绘制工具栏""" + # 设置工具栏位置 + imgui.set_next_window_pos((10, 30), imgui.Cond_.first_use_ever.value) + imgui.set_next_window_size((300, 40), imgui.Cond_.first_use_ever.value) + + with self.style_manager.begin_styled_window("工具栏", self.showToolbar, + self.style_manager.get_window_flags("toolbar")): + self.showToolbar = True # 确保窗口保持打开 + + # 工具按钮组 + if self.icons.get('select'): + if self.style_manager.image_button(self.icons['select'], (24, 24)): + print("选择工具") + if imgui.is_item_hovered(): + imgui.set_tooltip("选择工具 (Q)") + imgui.same_line() + else: + if imgui.button("选择"): + print("选择工具") + imgui.same_line() + + if self.icons.get('move'): + if self.style_manager.image_button(self.icons['move'], (24, 24)): + print("移动工具") + if imgui.is_item_hovered(): + imgui.set_tooltip("移动工具 (W)") + imgui.same_line() + else: + if imgui.button("移动"): + print("移动工具") + imgui.same_line() + + if self.icons.get('rotate'): + if self.style_manager.image_button(self.icons['rotate'], (24, 24)): + print("旋转工具") + if imgui.is_item_hovered(): + imgui.set_tooltip("旋转工具 (E)") + imgui.same_line() + else: + if imgui.button("旋转"): + print("旋转工具") + imgui.same_line() + + if self.icons.get('scale'): + if self.style_manager.image_button(self.icons['scale'], (24, 24)): + print("缩放工具") + if imgui.is_item_hovered(): + imgui.set_tooltip("缩放工具 (R)") + else: + if imgui.button("缩放"): + print("缩放工具") + + imgui.same_line() + imgui.separator() + imgui.same_line() + + # 其他工具按钮 + if imgui.button("导入"): + print("导入模型") + imgui.same_line() + if imgui.button("保存"): + print("保存场景") + imgui.same_line() + if imgui.button("播放"): + print("播放动画") + + def _draw_scene_tree(self): + """绘制场景树面板""" + # 设置场景树位置 + imgui.set_next_window_pos((10, 80), imgui.Cond_.first_use_ever.value) + imgui.set_next_window_size((250, 300), imgui.Cond_.first_use_ever.value) + + with self.style_manager.begin_styled_window("场景树", self.showSceneTree, + self.style_manager.get_window_flags("panel")): + self.showSceneTree = True # 确保窗口保持打开 + + imgui.text("场景层级") + imgui.separator() + + # 模拟场景树结构 + if imgui.tree_node("渲染"): + if imgui.tree_node("环境光"): + imgui.text("环境光 #1") + imgui.tree_pop() + if imgui.tree_node("定向光"): + imgui.text("定向光 #1") + imgui.tree_pop() + if imgui.tree_node("地板"): + imgui.text("地板节点") + imgui.tree_pop() + imgui.tree_pop() + + if imgui.tree_node("相机"): + imgui.text("主相机") + imgui.tree_pop() + + if imgui.tree_node("模型"): + imgui.text("(空)") + imgui.tree_pop() + + def _draw_property_panel(self): + """绘制属性面板""" + # 设置属性面板位置 + imgui.set_next_window_pos((270, 80), imgui.Cond_.first_use_ever.value) + imgui.set_next_window_size((250, 300), imgui.Cond_.first_use_ever.value) + + with self.style_manager.begin_styled_window("属性面板", self.showPropertyPanel, + self.style_manager.get_window_flags("panel")): + self.showPropertyPanel = True # 确保窗口保持打开 + + imgui.text("对象属性") + imgui.separator() + + # 模拟属性设置 + changed, pos_x = imgui.drag_float("位置 X", 0.0, 0.1) + changed, pos_y = imgui.drag_float("位置 Y", 0.0, 0.1) + changed, pos_z = imgui.drag_float("位置 Z", 0.0, 0.1) + + imgui.separator() + + changed, rot_x = imgui.drag_float("旋转 X", 0.0, 1.0) + changed, rot_y = imgui.drag_float("旋转 Y", 0.0, 1.0) + changed, rot_z = imgui.drag_float("旋转 Z", 0.0, 1.0) + + imgui.separator() + + changed, scale = imgui.drag_float("缩放", 1.0, 0.1) + + imgui.separator() + + if imgui.button("重置变换"): + print("重置变换") + + def _draw_console(self): + """绘制控制台面板""" + # 设置控制台位置 + imgui.set_next_window_pos((10, 390), imgui.Cond_.first_use_ever.value) + imgui.set_next_window_size((770, 150), imgui.Cond_.first_use_ever.value) + + with self.style_manager.begin_styled_window("控制台", self.showConsole, + self.style_manager.get_window_flags("panel")): + self.showConsole = True # 确保窗口保持打开 + + imgui.text("控制台输出") + imgui.separator() + + # 模拟控制台输出 + if self.icons.get('success'): + imgui.image(self.icons['success'], (12, 12)) + imgui.same_line() + imgui.text_colored((0.176, 1.0, 0.769, 1.0), "[INFO]") # 成功颜色 + imgui.same_line() + imgui.text("引擎初始化完成") + + imgui.text_colored((0.157, 0.620, 1.0, 1.0), "[INFO]") # 信息颜色 + imgui.same_line() + imgui.text("渲染管线已加载") + + if self.icons.get('warning'): + imgui.image(self.icons['warning'], (12, 12)) + imgui.same_line() + imgui.text_colored((0.953, 0.616, 0.471, 1.0), "[WARN]") # 警告颜色 + imgui.same_line() + imgui.text("视频管理器模块未找到") + + if self.icons.get('success'): + imgui.image(self.icons['success'], (12, 12)) + imgui.same_line() + imgui.text_colored((0.176, 1.0, 0.769, 1.0), "[INFO]") # 成功颜色 + imgui.same_line() + imgui.text("VR管理器初始化完成") + + # 输入框 + imgui.separator() + changed, command = imgui.input_text(">", "", 256) + if changed and command: + print(f"执行命令: {command}") + + def _draw_script_panel(self): + """绘制脚本管理面板""" + # 设置脚本面板位置 + imgui.set_next_window_pos((530, 80), imgui.Cond_.first_use_ever.value) + imgui.set_next_window_size((250, 300), imgui.Cond_.first_use_ever.value) + + with self.style_manager.begin_styled_window("脚本管理", self.showScriptPanel, + self.style_manager.get_window_flags("panel")): + self.showScriptPanel = True # 确保窗口保持打开 + + imgui.text("脚本列表") + imgui.separator() + + # 模拟脚本列表 + selected, _ = imgui.selectable("main.py", False) + if selected: + print("选择脚本: main.py") + selected, _ = imgui.selectable("player_controller.py", False) + if selected: + print("选择脚本: player_controller.py") + selected, _ = imgui.selectable("ui_manager.py", False) + if selected: + print("选择脚本: ui_manager.py") + + imgui.separator() + + # 脚本操作按钮 + if imgui.button("新建脚本"): + print("新建脚本") + imgui.same_line() + if imgui.button("编辑"): + print("编辑脚本") + imgui.same_line() + if imgui.button("重载"): + print("重载脚本") + + imgui.separator() + + imgui.text("脚本输出:") + imgui.text("脚本引擎已启动") + imgui.text("热重载监控已启动") + +demo = MyWorld() +demo.run() diff --git a/imgui.ini b/imgui.ini new file mode 100644 index 00000000..3f69a088 --- /dev/null +++ b/imgui.ini @@ -0,0 +1,55 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Window][Explore: render] +Pos=60,60 +Size=410,761 +Collapsed=0 + +[Window][Node Placer "camera"] +Pos=486,83 +Size=472,216 +Collapsed=0 + +[Window][Time Slider "pandaPace"] +Pos=-69,89 +Size=745,76 +Collapsed=0 + +[Window][Dear ImGui Demo] +Pos=989,19 +Size=550,680 +Collapsed=1 + +[Window][工具栏] +Pos=10,30 +Size=300,40 +Collapsed=0 + +[Window][场景树] +Pos=-3,66 +Size=270,497 +Collapsed=0 + +[Window][属性面板] +Pos=1132,337 +Size=252,416 +Collapsed=0 + +[Window][控制台] +Pos=0,562 +Size=1132,191 +Collapsed=0 + +[Window][脚本管理] +Pos=1132,39 +Size=250,300 +Collapsed=0 + +[Window][中文显示测试] +Pos=60,60 +Size=135,263 +Collapsed=0 + diff --git a/test_chinese_font.py b/test_chinese_font.py new file mode 100644 index 00000000..6ed783b6 --- /dev/null +++ b/test_chinese_font.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +测试中文字体显示的调试脚本 +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '.')) + +from panda3d.core import loadPrcFileData +from direct.showbase.ShowBase import ShowBase +import p3dimgui +from imgui_bundle import imgui +import platform +from pathlib import Path + +class ChineseFontTest(ShowBase): + def __init__(self): + # 设置窗口 + loadPrcFileData('', 'win-size 800 600') + ShowBase.__init__(self) + + # 初始化ImGui + p3dimgui.init() + + # 测试字体加载 + self.test_font_loading() + + # 接受ImGui新帧事件 + self.accept('imgui-new-frame', self._new_frame) + self.accept('`', self._toggle_imgui) + + self.show_test_window = True + + def test_font_loading(self): + """测试字体加载""" + print("=== 开始字体测试 ===") + + # 检查系统 + system = platform.system().lower() + print(f"系统: {system}") + + # 获取字体路径 + font_paths = [] + if system == "linux": + font_paths = [ + "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", + "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + ] + elif system == "windows": + font_paths = [ + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simhei.ttf", + ] + elif system == "darwin": + font_paths = [ + "/System/Library/Fonts/PingFang.ttc", + "/System/Library/Fonts/STHeiti.ttc", + ] + + # 测试每个字体 + for font_path in font_paths: + if Path(font_path).exists(): + print(f"✓ 字体文件存在: {font_path}") + try: + # 尝试加载字体 + font = self.imgui.io.fonts.add_font_from_file_ttf(font_path, 16.0) + print(f" ✓ 字体加载成功") + + # 检查字体信息 + font_info = self.imgui.io.fonts.fonts + print(f" 字体数量: {len(font_info)}") + + # 尝试获取字符集 + try: + # 方法1:尝试获取中文字符集 + chinese_ranges = imgui.get_io().fonts.get_glyph_ranges_chinese_full() + print(f" ✓ 中文字符集获取成功,范围: {len(chinese_ranges)} 个字符") + except: + try: + # 方法2:尝试获取中文字符集(简化版) + chinese_ranges = imgui.get_io().fonts.get_glyph_ranges_chinese() + print(f" ✓ 中文字符集获取成功(简化版),范围: {len(chinese_ranges)} 个字符") + except: + try: + # 方法3:尝试获取默认字符集 + default_ranges = imgui.get_io().fonts.get_glyph_ranges_default() + print(f" ✓ 默认字符集获取成功,范围: {len(default_ranges)} 个字符") + print(f" ⚠ 使用默认字符集,可能不支持中文") + except Exception as e: + print(f" ⚠ 字符集获取失败: {e}") + print(f" ⚠ 将使用默认字符集") + + break + except Exception as e: + print(f" ✗ 字体加载失败: {e}") + else: + print(f"✗ 字体文件不存在: {font_path}") + + print("=== 字体测试完成 ===") + + def _toggle_imgui(self): + if not self.imgui.isKeyboardCaptured(): + self.imgui.toggle() + + def _new_frame(self): + # 绘制测试窗口 + if self.show_test_window: + imgui.show_demo_window() + + # 中文测试窗口 + expanded, opened = imgui.begin("中文显示测试", True) + if expanded: + imgui.text("基础中文测试:") + imgui.text("你好,世界!") + imgui.text("这是一个中文测试") + imgui.text("引擎初始化完成") + imgui.text("选择和变换系统") + imgui.text("脚本管理系统") + + imgui.separator() + imgui.text("特殊字符测试:") + imgui.text("标点符号:,。!?;:""''()【】") + + imgui.separator() + imgui.text("数字测试:") + imgui.text("一二三四五六七八九十") + + imgui.separator() + + if imgui.button("测试按钮"): + print("中文按钮被点击") + + imgui.same_line() + if imgui.button("另一个按钮"): + print("另一个中文按钮") + + imgui.separator() + + changed, text = imgui.input_text("输入中文", "在这里输入中文") + if changed: + print(f"输入的中文: {text}") + + imgui.end() + +if __name__ == "__main__": + test = ChineseFontTest() + test.run() \ No newline at end of file diff --git a/testgui.py b/testgui.py new file mode 100644 index 00000000..c7735dbf --- /dev/null +++ b/testgui.py @@ -0,0 +1,21 @@ +from direct.showbase.ShowBase import ShowBase + +from imgui_bundle import imgui + +import p3dimgui + +class MyApp(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # Install Dear ImGui + p3dimgui.init() + + self.accept('imgui-new-frame', self.draw) + + def draw(self): + # Show the demo window. + imgui.show_demo_window() + +app = MyApp() +app.run() \ No newline at end of file