diff --git a/.idea/misc.xml b/.idea/misc.xml index 505c56b5..a86c38d2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/RenderPipelineFile/config/plugins.yaml b/RenderPipelineFile/config/plugins.yaml index 4b0f5d64..42c03f4c 100644 --- a/RenderPipelineFile/config/plugins.yaml +++ b/RenderPipelineFile/config/plugins.yaml @@ -9,14 +9,14 @@ enabled: - color_correction - env_probes - forward_shading - - motion_blur + # - motion_blur # disabled for editor performance - pssm - scattering - skin_shading - sky_ao - smaa - - ssr - - clouds + # - ssr # disabled for editor performance + #- clouds #- dof #- fxaa #- volumetrics @@ -120,7 +120,7 @@ overrides: logarithmic_factor: 3.0 sun_distance: 100.0 split_count: 4 - resolution: 1024 + resolution: 512 border_bias: 0.058 use_pcf: True filter_sequence: halton_2D_32 @@ -133,7 +133,7 @@ overrides: pcss_penumbra_size: 2.38 pcss_min_penumbra_size: 7.0 use_distant_shadows: True - dist_shadow_resolution: 4096 + dist_shadow_resolution: 1024 dist_shadow_clipsize: 400.0 dist_shadow_sundist: 300.0 scene_shadow_resolution: 512 @@ -157,7 +157,7 @@ overrides: sky_ao: sample_radius: 17.17 max_radius: 500.0 - resolution: 1024 + resolution: 512 sample_sequence: poisson_2D_32 ao_multiplier: 0.83 ao_bias: 0.0 diff --git a/core/CustomMouseController.py b/core/CustomMouseController.py index ac4132e9..b03da669 100644 --- a/core/CustomMouseController.py +++ b/core/CustomMouseController.py @@ -1,4 +1,5 @@ from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import KeyboardButton class CustomMouseController: @@ -23,18 +24,30 @@ class CustomMouseController: self.showbase.accept("mouse3-up", self.setKey, ["mouse3", False]) # 右键释放 self.showbase.accept("w", self.setKey, ["cam-forward", True]) + self.showbase.accept("W", self.setKey, ["cam-forward", True]) self.showbase.accept("a", self.setKey, ["cam-left", True]) + self.showbase.accept("A", self.setKey, ["cam-left", True]) self.showbase.accept("s", self.setKey, ["cam-backward", True]) + self.showbase.accept("S", self.setKey, ["cam-backward", True]) self.showbase.accept("d", self.setKey, ["cam-right", True]) + self.showbase.accept("D", self.setKey, ["cam-right", True]) self.showbase.accept("e", self.setKey, ["cam-up", True]) + self.showbase.accept("E", self.setKey, ["cam-up", True]) self.showbase.accept("q", self.setKey, ["cam-down", True]) + self.showbase.accept("Q", self.setKey, ["cam-down", True]) self.showbase.accept("w-up", self.setKey, ["cam-forward", False]) + self.showbase.accept("W-up", self.setKey, ["cam-forward", False]) self.showbase.accept("a-up", self.setKey, ["cam-left", False]) + self.showbase.accept("A-up", self.setKey, ["cam-left", False]) self.showbase.accept("s-up", self.setKey, ["cam-backward", False]) + self.showbase.accept("S-up", self.setKey, ["cam-backward", False]) self.showbase.accept("d-up", self.setKey, ["cam-right", False]) + self.showbase.accept("D-up", self.setKey, ["cam-right", False]) self.showbase.accept("e-up", self.setKey, ["cam-up", False]) + self.showbase.accept("E-up", self.setKey, ["cam-up", False]) self.showbase.accept("q-up", self.setKey, ["cam-down", False]) + self.showbase.accept("Q-up", self.setKey, ["cam-down", False]) self.last_mouse_x = 0 self.last_mouse_y = 0 @@ -61,17 +74,25 @@ class CustomMouseController: # 检查ImGui是否捕获了键盘输入 if self._should_handle_keyboard(): - if self.keyMap["cam-left"]: + watcher = self.showbase.mouseWatcherNode + left_down = self.keyMap["cam-left"] or watcher.is_button_down(KeyboardButton.ascii_key("a")) + right_down = self.keyMap["cam-right"] or watcher.is_button_down(KeyboardButton.ascii_key("d")) + backward_down = self.keyMap["cam-backward"] or watcher.is_button_down(KeyboardButton.ascii_key("s")) + forward_down = self.keyMap["cam-forward"] or watcher.is_button_down(KeyboardButton.ascii_key("w")) + up_down = self.keyMap["cam-up"] or watcher.is_button_down(KeyboardButton.ascii_key("e")) + down_down = self.keyMap["cam-down"] or watcher.is_button_down(KeyboardButton.ascii_key("q")) + + if left_down: self.showbase.camera.setX(self.showbase.camera, -self.move_speed * dt) - if self.keyMap["cam-right"]: + if right_down: self.showbase.camera.setX(self.showbase.camera, +self.move_speed * dt) - if self.keyMap["cam-backward"]: + if backward_down: self.showbase.camera.setY(self.showbase.camera, -self.move_speed * dt) - if self.keyMap["cam-forward"]: + if forward_down: self.showbase.camera.setY(self.showbase.camera, +self.move_speed * dt) - if self.keyMap["cam-up"]: + if up_down: self.showbase.camera.setZ(self.showbase.camera, +self.move_speed * dt) - if self.keyMap["cam-down"]: + if down_down: self.showbase.camera.setZ(self.showbase.camera, -self.move_speed * dt) if self.keyMap["mouse3"]: # 只使用右键控制视角旋转 try: diff --git a/core/event_handler.py b/core/event_handler.py index a2425c97..9d4fdc64 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -254,7 +254,7 @@ class EventHandler: return # 根据当前工具处理点击事件 - if self.world.currentTool == "选择": + if self.world.currentTool in ("选择", "移动", "旋转", "缩放"): print("✓ 使用选择工具处理点击") try: self._handleSelectionClick(hitNode) @@ -589,4 +589,4 @@ class EventHandler: self.world.selection.updateGizmoHighlight(x, y) # 调用CoreWorld的父类方法处理基础的相机旋转 - super(type(self.world), self.world).mouseMoveEvent(evt) \ No newline at end of file + super(type(self.world), self.world).mouseMoveEvent(evt) diff --git a/core/imgui_style_manager.py b/core/imgui_style_manager.py index e9aee657..39e3c04b 100644 --- a/core/imgui_style_manager.py +++ b/core/imgui_style_manager.py @@ -402,7 +402,10 @@ class ImGuiStyleManager: if icon_path.exists(): # 使用base.imgui.loadTexture方法 if hasattr(base, 'imgui') and hasattr(base.imgui, 'loadTexture'): - return base.imgui.loadTexture(str(icon_path)) + # 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...) + # 注意: p3dimgui.loadTexture 仅支持 str 或 Texture,不支持 Filename 对象 + fn = Filename.fromOsSpecific(str(icon_path)) + return base.imgui.loadTexture(fn.getFullpath()) else: print(f"⚠ ImGui后端未初始化") return None diff --git a/core/selection.py b/core/selection.py index 53ccbb7b..a8565272 100644 --- a/core/selection.py +++ b/core/selection.py @@ -118,6 +118,30 @@ class SelectionSystem: def _resetCursor(self): self._setCursor("default") + def _has_new_transform_gizmo(self): + tg = getattr(self.world, "newTransform", None) + return tg is not None + + def sync_transform_gizmo_mode(self): + """Sync TransformGizmo mode with current tool.""" + if not self._has_new_transform_gizmo(): + return + try: + from TransformGizmo.events import TransformGizmoMode + tool_mgr = getattr(self.world, "tool_manager", None) + if not tool_mgr: + return + if tool_mgr.isRotateTool(): + self.world.newTransform.set_mode(TransformGizmoMode.ROTATE) + elif tool_mgr.isScaleTool(): + self.world.newTransform.set_mode(TransformGizmoMode.SCALE) + elif tool_mgr.isMoveTool() or tool_mgr.isSelectionTool(): + self.world.newTransform.set_mode(TransformGizmoMode.MOVE) + else: + self.world.newTransform.set_mode(TransformGizmoMode.NONE) + except Exception as e: + print(f"sync transform gizmo mode failed: {e}") + # ==================== 选择框系统 ==================== def createSelectionBox(self, nodePath): @@ -135,8 +159,8 @@ class SelectionSystem: self.selectionBox = self.world.render.attachNewNode("selectionBox") self.selectionBoxTarget = nodePath - taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") - self.updateSelectionBoxGeometry() + #taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") + #self.updateSelectionBoxGeometry() except Exception as e: print(f" ✗ 创建选择框失败: {str(e)}") @@ -340,6 +364,17 @@ class SelectionSystem: def createGizmo(self, nodePath): """为选中的节点创建坐标轴工具 - 保留箭头版本""" + if self._has_new_transform_gizmo(): + self.gizmo = None + self.gizmoTarget = nodePath + if not nodePath: + return + self.sync_transform_gizmo_mode() + try: + self.world.newTransform.attach(nodePath) + except Exception as e: + print(f"attach TransformGizmo failed: {e}") + return try: #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") @@ -818,15 +853,31 @@ class SelectionSystem: pass def clearGizmo(self): - """清除坐标轴""" + """Clear transform gizmo.""" + if self._has_new_transform_gizmo(): + try: + self.world.newTransform.detach() + except Exception as e: + print(f"detach TransformGizmo failed: {e}") + self.gizmo = None + self.gizmoTarget = None + self.gizmoXAxis = None + self.gizmoYAxis = None + self.gizmoZAxis = None + self.isDraggingGizmo = False + self.dragGizmoAxis = None + self.dragStartMousePos = None + self.gizmoTargetStartPos = None + self.gizmoStartPos = None + self._resetCursor() + return + if self.gizmo: self.gizmo.removeNode() self.gizmo = None - # 停止坐标轴更新任务 taskMgr.remove("updateGizmo") - # 清除坐标轴相关引用 self.gizmoTarget = None self.gizmoXAxis = None self.gizmoYAxis = None @@ -838,39 +889,8 @@ class SelectionSystem: self.gizmoStartPos = None self._resetCursor() - # def setGizmoAxisColor(self, axis, color): # """设置坐标轴颜色 - RenderPipeline 兼容版本""" - # try: - # from panda3d.core import AntialiasAttrib,TransparencyAttrib - # - # axis_nodes = { - # "x": self.gizmoXAxis, - # "y": self.gizmoYAxis, - # "z": self.gizmoZAxis - # } - # - # if axis in axis_nodes and axis_nodes[axis]: - # axis_node = axis_nodes[axis] - # - # axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3]) - # axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3]) - # axis_node.setShaderOff(10000) - # axis_node.setLightOff() - # axis_node.setMaterialOff() - # axis_node.setTextureOff() - # axis_node.setFogOff() - # - # except Exception as e: - # print(f"设置坐标轴颜色失败: {str(e)}") - # # 回退到简单的颜色设置 - # try: - # if axis in axis_nodes and axis_nodes[axis]: - # axis_nodes[axis].setColor(*color) - # except: - # pass - - def setGizmoRotAxisColor(self, axis, color): """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" try: diff --git a/core/tool_manager.py b/core/tool_manager.py index 87ffe17c..d86f4773 100644 --- a/core/tool_manager.py +++ b/core/tool_manager.py @@ -37,6 +37,12 @@ class ToolManager: # 坐标轴现在始终跟随选中状态,不再依赖工具类型 + try: + if hasattr(self.world, "selection") and self.world.selection: + self.world.selection.sync_transform_gizmo_mode() + except Exception as e: + print(f"sync gizmo mode on tool change failed: {e}") + def getCurrentTool(self): """获取当前工具""" return self.currentTool diff --git a/core/world.py b/core/world.py index 77077142..2daf6cfc 100644 --- a/core/world.py +++ b/core/world.py @@ -20,6 +20,7 @@ render_pipeline_path = os.path.join(project_root, "RenderPipelineFile") sys.path.insert(0, render_pipeline_path) from RenderPipelineFile.rpcore import RenderPipeline +from ssbo_component.ssbo_editor import SSBOEditor # 从渲染管线工具模块导入全局函数 from core.render_pipeline_utils import get_render_pipeline, set_render_pipeline @@ -47,12 +48,19 @@ class CoreWorld(ShowBase): loadPrcFileData("", f"win-size {width} {height}") loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小 + # Performance preset to match the standalone smooth runner + loadPrcFileData("", "threading-model /Draw") + loadPrcFileData("", "pstats-gpu-timing #f") + loadPrcFileData("", "gl-debug #f") + loadPrcFileData("", "gl-debug-object-labels #f") + # VR性能优化配置 loadPrcFileData("", "prefer-single-buffer true") loadPrcFileData("", "gl-force-flush false") loadPrcFileData("", "sync-video false") loadPrcFileData("", "support-stencil false") loadPrcFileData("", "clock-mode non-real-time") + loadPrcFileData("", "support-threads false") if is_fullscreen: loadPrcFileData("", "fullscreen #t") @@ -60,6 +68,9 @@ class CoreWorld(ShowBase): # 创建渲染管线 self.render_pipeline = RenderPipeline() self.render_pipeline.pre_showbase_init() + + # 强制开启多线程支持 (Video播放必需) + loadPrcFileData("force_threads", "support-threads #f") # 初始化 ShowBase ShowBase.__init__(self) diff --git a/imgui.ini b/imgui.ini index 1c34eff5..6556eb15 100644 --- a/imgui.ini +++ b/imgui.ini @@ -1,5 +1,5 @@ [Window][Debug##Default] -Pos=28,731 +Pos=28,465 Size=400,400 Collapsed=0 @@ -24,34 +24,34 @@ Size=832,45 Collapsed=0 [Window][工具栏] -Pos=287,20 -Size=1234,32 +Pos=325,20 +Size=1228,32 Collapsed=0 -DockId=0x00000007,0 +DockId=0x0000000D,0 [Window][场景树] Pos=0,20 -Size=285,776 +Size=323,634 Collapsed=0 -DockId=0x00000001,0 +DockId=0x00000007,0 [Window][属性面板] -Pos=1523,20 -Size=327,498 +Pos=1555,20 +Size=365,989 Collapsed=0 -DockId=0x00000005,0 +DockId=0x00000003,0 [Window][控制台] -Pos=880,798 -Size=641,218 +Pos=0,656 +Size=323,353 Collapsed=0 -DockId=0x0000000C,0 +DockId=0x00000008,0 [Window][脚本管理] -Pos=1523,520 -Size=327,496 +Pos=1540,20 +Size=380,390 Collapsed=0 -DockId=0x00000006,0 +DockId=0x00000003,1 [Window][中文显示测试] Pos=60,60 @@ -60,7 +60,7 @@ Collapsed=0 [Window][WindowOverViewport_11111111] Pos=0,20 -Size=1850,996 +Size=1920,989 Collapsed=0 [Window][测试窗口1] @@ -79,30 +79,30 @@ Size=93,65 Collapsed=0 [Window][新建项目] -Pos=725,358 +Pos=760,354 Size=400,300 Collapsed=0 [Window][选择路径] -Pos=625,258 +Pos=660,254 Size=600,500 Collapsed=0 [Window][打开项目] -Pos=675,308 +Pos=710,304 Size=500,400 Collapsed=0 [Window][导入模型] -Pos=625,258 +Pos=660,254 Size=600,500 Collapsed=0 [Window][资源管理器] -Pos=0,798 -Size=878,218 +Pos=325,827 +Size=1228,182 Collapsed=0 -DockId=0x0000000B,0 +DockId=0x00000006,0 [Window][创建3D文本] Pos=60,60 @@ -135,27 +135,68 @@ Size=89,250 Collapsed=0 [Window][颜色选择器] -Pos=775,308 +Pos=810,304 Size=300,400 Collapsed=0 [Window][选择diffuse纹理文件##texture_dialog] -Pos=625,308 +Pos=660,304 +Size=600,400 +Collapsed=0 + +[Window][创建GUI图片] +Pos=60,60 +Size=101,226 +Collapsed=0 + +[Window][LUI编辑器] +Pos=1540,412 +Size=380,597 +Collapsed=0 +DockId=0x00000004,0 + +[Window][LUI测试控制面板] +Pos=6,10 +Size=300,200 +Collapsed=1 + +[Window][选择高度图文件] +Pos=60,60 +Size=596,498 +Collapsed=0 + +[Window][创建平面地形] +Pos=61,60 +Size=238,176 +Collapsed=0 + +[Window][选择normal纹理文件##texture_dialog] +Pos=660,304 +Size=600,400 +Collapsed=0 + +[Window][选择metallic纹理文件##texture_dialog] +Pos=660,304 +Size=600,400 +Collapsed=0 + +[Window][选择roughness纹理文件##texture_dialog] +Pos=660,304 Size=600,400 Collapsed=0 [Docking][Data] -DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X - DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=1521,996 Split=Y - DockNode ID=0x00000009 Parent=0x00000003 SizeRef=1380,776 Split=X - DockNode ID=0x00000001 Parent=0x00000009 SizeRef=285,730 HiddenTabBar=1 Selected=0xE0015051 - DockNode ID=0x00000002 Parent=0x00000009 SizeRef=1234,730 Split=Y - DockNode ID=0x00000007 Parent=0x00000002 SizeRef=1380,32 HiddenTabBar=1 Selected=0x43A39006 - DockNode ID=0x00000008 Parent=0x00000002 SizeRef=1380,742 CentralNode=1 Selected=0x5E5F7166 - DockNode ID=0x0000000A Parent=0x00000003 SizeRef=1380,218 Split=X Selected=0x5428E753 - DockNode ID=0x0000000B Parent=0x0000000A SizeRef=878,111 HiddenTabBar=1 Selected=0x3A2E05C3 - DockNode ID=0x0000000C Parent=0x0000000A SizeRef=641,111 HiddenTabBar=1 Selected=0x5428E753 - DockNode ID=0x00000004 Parent=0x08BD597D SizeRef=327,996 Split=Y Selected=0x5DB6FF37 - DockNode ID=0x00000005 Parent=0x00000004 SizeRef=304,498 HiddenTabBar=1 Selected=0x5DB6FF37 - DockNode ID=0x00000006 Parent=0x00000004 SizeRef=304,496 HiddenTabBar=1 Selected=0x3188AB8D +DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X + DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1553,989 Split=X + DockNode ID=0x00000009 Parent=0x00000001 SizeRef=323,989 Split=Y Selected=0xE0015051 + DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051 + DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753 + DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1228,989 Split=Y + DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006 + DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y + DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,771 CentralNode=1 + DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,182 Selected=0x3A2E05C3 + DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=365,989 Split=Y Selected=0x3188AB8D + DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37 + DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7 diff --git a/main.py b/main.py index 0c9a7c26..e15559bd 100644 --- a/main.py +++ b/main.py @@ -34,6 +34,18 @@ from core.InfoPanelManager import InfoPanelManager from core.collision_manager import CollisionManager from core.CustomMouseController import CustomMouseController from core.resource_manager import ResourceManager +from ui.lui_manager import LUIManager +from ui.panels.editor_panels import EditorPanels +from ui.panels.script_panels import ScriptPanels +from ui.panels.dialog_panels import DialogPanels +from ui.panels.interaction_panels import InteractionPanels +from ui.panels.create_actions import CreateActions +from ui.panels.object_factory import ObjectFactory +from ui.panels.animation_tools import AnimationTools +from ui.panels.property_helpers import PropertyHelpers +from ui.panels.app_actions import AppActions +from ssbo_component.ssbo_editor import SSBOEditor +from TransformGizmo.transform_gizmo import TransformGizmo # 拖拽监控类 class DragDropMonitor: @@ -106,7 +118,7 @@ class MyWorld(CoreWorld): # 设置窗口为最大化模式 props = WindowProperties() - props.set_maximized(True) + #props.set_maximized(True) self.win.request_properties(props) print("✓ 窗口已设置为最大化模式") @@ -117,15 +129,11 @@ class MyWorld(CoreWorld): 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) - - # 绑定鼠标事件用于3D场景选择 - self.accept("mouse1", self.onMouseClick) + self.use_ssbo_mouse_picking = True + if not self.use_ssbo_mouse_picking: + self.accept("mouse1", self.onMouseClick) + # Keep release/move bindings even in SSBO mode so gizmo drag can work. self.accept("mouse1-up", self.onMouseRelease) - # 尝试多种鼠标移动事件绑定方式 self.accept("mouse-move", self.onMouseMove) self.accept("drag", self.onMouseMove) @@ -140,6 +148,12 @@ class MyWorld(CoreWorld): # 初始化GUI管理系统 self.gui_manager = GUIManager(self) + + # 初始化LUI管理系统 + self.lui_manager = LUIManager(self) + + # 新的坐标系 + self.newTransform = TransformGizmo(self) # 初始化视频管理 if VideoManager is not None: @@ -194,7 +208,10 @@ class MyWorld(CoreWorld): self.debug_collision = True # 是否显示碰撞体 # 默认启用模型间碰撞检测(可选) - self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5) + if self.use_ssbo_mouse_picking: + self.enableModelCollisionDetection(enable=False, frequency=0.1, threshold=0.5) + else: + self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5) # 碰撞检测UI相关变量 self._selected_collision_shape = "球形 (Sphere)" # 默认选择的碰撞形状 @@ -209,15 +226,41 @@ class MyWorld(CoreWorld): self.terrain_edit_operation = "add" # Install Dear ImGui - p3dimgui.init() + p3dimgui.init( + wantPlaceManager=False, + wantExplorerManager=False, + wantTimeSliderManager=False, + ) + # Let ssbo_component reuse the existing imgui backend instance. + self.imgui_backend = self.imgui + # Initialize SSBO editor and let it own mouse1 picking. + # Do not auto-load a default model here; models are loaded from import flow. + self.ssbo_editor = None + if self.use_ssbo_mouse_picking: + self.ssbo_editor = SSBOEditor( + base_app=self, + render_pipeline=self.render_pipeline, + model_path=None, + font_path=None, + ) + self.ssbo_editor.bind_transform_gizmo(self.newTransform) + print("SSBOEditor mouse picking enabled (waiting for imported model)") - # 启用ImGui Docking功能 imgui.get_io().config_flags |= imgui.ConfigFlags_.docking_enable print("✓ ImGui Docking功能已启用") # 初始化样式管理器 from core.imgui_style_manager import ImGuiStyleManager self.style_manager = ImGuiStyleManager(self.imgui, self) + self.editor_panels = EditorPanels(self) + self.script_panels = ScriptPanels(self) + self.dialog_panels = DialogPanels(self) + self.interaction_panels = InteractionPanels(self) + self.create_actions = CreateActions(self) + self.object_factory = ObjectFactory(self) + self.animation_tools = AnimationTools(self) + self.property_helpers = PropertyHelpers(self) + self.app_actions = AppActions(self) # 简化的初始化字体设置(只使用中文字体) try: @@ -226,8 +269,6 @@ class MyWorld(CoreWorld): # 尝试加载中文字体 import platform - from pathlib import Path - system = platform.system().lower() if system == "linux": font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" @@ -269,7 +310,7 @@ class MyWorld(CoreWorld): self.showSceneTree = True self.showPropertyPanel = True self.showConsole = True - self.showScriptPanel = True + self.showScriptPanel = not self.use_ssbo_mouse_picking self.showToolbar = True self.showResourceManager = True @@ -342,6 +383,7 @@ class MyWorld(CoreWorld): self.is_dragging = False self.show_drag_overlay = False self.drag_drop_monitor = None + self.showLUIEditor = not self.use_ssbo_mouse_picking # 导入功能状态 self.show_import_dialog = False @@ -410,6 +452,7 @@ class MyWorld(CoreWorld): self.accept('control-c', self._on_copy) self.accept('control-v', self._on_paste) self.accept('delete', self._on_delete_pressed) + self.accept('escape', self._on_escape_pressed) # 滚轮事件 self.accept('wheel_up', self._on_wheel_up) @@ -598,38 +641,6 @@ class MyWorld(CoreWorld): except Exception as e: print(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): """启用模型间碰撞检测""" @@ -766,6 +777,10 @@ class MyWorld(CoreWorld): # 绘制拖拽界面 self._draw_drag_drop_interface() + # 绘制LUI编辑器 + if self.showLUIEditor and hasattr(self, 'lui_manager'): + self.lui_manager.draw_editor() + # 更新变换监控 dt = imgui.get_io().delta_time self.update_transform_monitoring(dt) @@ -802,1126 +817,65 @@ class MyWorld(CoreWorld): if self.showToolbar: self._draw_toolbar() - - 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: - if imgui.menu_item("新建项目", "Ctrl+N", False, True)[1]: - self._on_new_project() - if imgui.menu_item("打开项目", "Ctrl+O", False, True)[1]: - self._on_open_project() - imgui.separator() - if imgui.menu_item("保存", "Ctrl+S", False, True)[1]: - self._on_save_project() - if imgui.menu_item("另存为", "", False, True)[1]: - self._on_save_as_project() - imgui.separator() - if imgui.menu_item("退出", "Alt+F4", False, True)[1]: - self._on_exit() - - # 编辑菜单 - with imgui_ctx.begin_menu("编辑") as edit_menu: - if edit_menu: - if imgui.menu_item("撤销", "Ctrl+Z", False, True)[1]: - self._on_undo() - if imgui.menu_item("重做", "Ctrl+Y", False, True)[1]: - self._on_redo() - imgui.separator() - if imgui.menu_item("剪切", "Ctrl+X", False, True)[1]: - self._on_cut() - if imgui.menu_item("复制", "Ctrl+C", False, True)[1]: - self._on_copy() - if imgui.menu_item("粘贴", "Ctrl+V", False, True)[1]: - self._on_paste() - imgui.separator() - if imgui.menu_item("删除", "Del", False, True)[1]: - self._on_delete() - - # 创建菜单 - with imgui_ctx.begin_menu("创建") as create_menu: - if create_menu: - if imgui.menu_item("导入模型", "", False, True)[1]: - self._on_import_model() - - imgui.separator() - - if imgui.menu_item("空对象", "", False, True)[1]: - self._on_create_empty_object() - - # 3D对象子菜单 - with imgui_ctx.begin_menu("3D对象") as three_d_menu: - if three_d_menu: - if imgui.menu_item("立方体", "", False, True)[1]: - self._on_create_cube() - if imgui.menu_item("球体", "", False, True)[1]: - self._on_create_sphere() - if imgui.menu_item("圆柱体", "", False, True)[1]: - self._on_create_cylinder() - if imgui.menu_item("平面", "", False, True)[1]: - self._on_create_plane() - - # 3D GUI子菜单 - with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu: - if three_d_gui_menu: - if imgui.menu_item("3D文本", "", False, True)[1]: - self._on_create_3d_text() - if imgui.menu_item("3D图片", "", False, True)[1]: - self._on_create_3d_image() - - # GUI子菜单 - with imgui_ctx.begin_menu("GUI") as gui_menu: - if gui_menu: - if imgui.menu_item("创建按钮", "", False, True)[1]: - self._on_create_gui_button() - if imgui.menu_item("创建标签", "", False, True)[1]: - self._on_create_gui_label() - if imgui.menu_item("创建输入框", "", False, True)[1]: - self._on_create_gui_entry() - if imgui.menu_item("创建图片", "", False, True)[1]: - self._on_create_gui_image() - imgui.separator() - if imgui.menu_item("创建视频屏幕", "", False, True)[1]: - self._on_create_video_screen() - if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]: - self._on_create_2d_video_screen() - if imgui.menu_item("创建球形视频", "", False, True)[1]: - self._on_create_spherical_video() - if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]: - self._on_create_virtual_screen() - - # 光源子菜单 - with imgui_ctx.begin_menu("光源") as light_menu: - if light_menu: - if imgui.menu_item("聚光灯", "", False, True)[1]: - self._on_create_spot_light() - if imgui.menu_item("点光源", "", False, True)[1]: - self._on_create_point_light() - - # 地形子菜单 - with imgui_ctx.begin_menu("地形") as terrain_menu: - if terrain_menu: - if imgui.menu_item("创建平面地形", "", False, True)[1]: - self._on_create_flat_terrain() - if imgui.menu_item("从高度图创建地形", "", False, True)[1]: - self._on_create_heightmap_terrain() - - # 脚本子菜单 - with imgui_ctx.begin_menu("脚本") as script_menu: - if script_menu: - if imgui.menu_item("创建脚本...", "", False, True)[1]: - self._on_create_script() - if imgui.menu_item("加载脚本文件...", "", False, True)[1]: - self._on_load_script() - imgui.separator() - if imgui.menu_item("重载所有脚本", "", False, True)[1]: - self._on_reload_all_scripts() - _, self.hotReloadEnabled = imgui.menu_item("启用热重载", "", self.hotReloadEnabled, True) - if imgui.menu_item("脚本管理器", "", False, True)[1]: - self._on_open_scripts_manager() - - # 信息面板子菜单 - with imgui_ctx.begin_menu("信息面板") as info_panel_menu: - if info_panel_menu: - if imgui.menu_item("创建2D示例面板", "", False, True)[1]: - self._on_create_2d_sample_panel() - if imgui.menu_item("创建3D实例面板", "", False, True)[1]: - self._on_create_3d_sample_panel() - if imgui.menu_item("Web面板", "", False, True)[1]: - self._on_create_web_panel() - - # 视图菜单 - 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.showResourceManager = imgui.menu_item("资源管理器", "", self.showResourceManager, 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: - # 工具切换选项 - if imgui.menu_item("选择工具", "", False, True)[1]: - self.tool_manager.setCurrentTool("选择") - if imgui.menu_item("移动工具", "", False, True)[1]: - self.tool_manager.setCurrentTool("移动") - if imgui.menu_item("旋转工具", "", False, True)[1]: - self.tool_manager.setCurrentTool("旋转") - if imgui.menu_item("缩放工具", "", False, True)[1]: - self.tool_manager.setCurrentTool("缩放") - - imgui.separator() - - # 编辑工具 - if imgui.menu_item("光照编辑", "", False, True)[1]: - self.tool_manager.setCurrentTool("光照编辑") - if imgui.menu_item("图形编辑", "", False, True)[1]: - self.tool_manager.setCurrentTool("图形编辑") - - imgui.separator() - - # VR子菜单 - with imgui_ctx.begin_menu("VR") as vr_menu: - if vr_menu: - if imgui.menu_item("进入VR模式", "", False, True)[1]: - self._toggle_vr_mode() - if imgui.menu_item("退出VR模式", "", False, True)[1]: - self._exit_vr_mode() - - imgui.separator() - - if imgui.menu_item("VR状态", "", False, True)[1]: - self._show_vr_status() - if imgui.menu_item("VR设置", "", False, True)[1]: - self._show_vr_settings() - - imgui.separator() - - # VR调试子菜单 - with imgui_ctx.begin_menu("VR调试") as vr_debug_menu: - if vr_debug_menu: - _, self.vr_debug_enabled = imgui.menu_item("启用调试输出", "", self.vr_debug_enabled, True) - - if imgui.menu_item("立即显示性能报告", "", False, True)[1]: - self._show_vr_performance_report() - - imgui.separator() - - # 输出模式 - with imgui_ctx.begin_menu("输出模式") as output_menu: - if output_menu: - if imgui.menu_item("简短模式", "", not self.vr_detailed_mode, True)[1]: - self.vr_detailed_mode = False - if imgui.menu_item("详细模式", "", self.vr_detailed_mode, True)[1]: - self.vr_detailed_mode = True - - imgui.separator() - - _, self.vr_performance_monitor = imgui.menu_item("启用性能监控", "", self.vr_performance_monitor, 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)) - + self.editor_panels.draw_menu_bar() + def _draw_toolbar(self): - """绘制工具栏""" - # 工具栏可以保持无标题栏,但允许移动和调整大小 - flags = self.style_manager.get_window_flags("toolbar") - - with self.style_manager.begin_styled_window("工具栏", self.showToolbar, flags): - self.showToolbar = True # 确保窗口保持打开 - - # 选择工具按钮 - select_active = self.tool_manager.isSelectionTool() - if self.icons.get('select'): - tint_col = (1.0, 1.0, 0.0, 1.0) if select_active else (1.0, 1.0, 1.0, 1.0) - if self.style_manager.image_button(self.icons['select'], (24, 24), tint_col=tint_col): - self.tool_manager.setCurrentTool("选择") - if imgui.is_item_hovered(): - imgui.set_tooltip("选择工具 (Q)") - imgui.same_line() - else: - if imgui.button("选择##select_tool"): - self.tool_manager.setCurrentTool("选择") - if select_active: - draw_list = imgui.get_window_draw_list() - button_min = imgui.get_item_rect_min() - button_max = imgui.get_item_rect_max() - draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3))) - imgui.same_line() - - # 移动工具按钮 - move_active = self.tool_manager.isMoveTool() - if self.icons.get('move'): - # 使用不同颜色表示活动状态 - tint_col = (1.0, 1.0, 0.0, 1.0) if move_active else (1.0, 1.0, 1.0, 1.0) # 活动时显示黄色 - if self.style_manager.image_button(self.icons['move'], (24, 24), tint_col=tint_col): - self.tool_manager.setCurrentTool("移动") - if imgui.is_item_hovered(): - imgui.set_tooltip("移动工具 (W)") - imgui.same_line() - else: - if imgui.button("移动##move_tool"): - self.tool_manager.setCurrentTool("移动") - if move_active: - # 为活动按钮添加背景色 - draw_list = imgui.get_window_draw_list() - button_min = imgui.get_item_rect_min() - button_max = imgui.get_item_rect_max() - draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3))) - imgui.same_line() - - # 旋转工具按钮 - rotate_active = self.tool_manager.isRotateTool() - if self.icons.get('rotate'): - tint_col = (1.0, 1.0, 0.0, 1.0) if rotate_active else (1.0, 1.0, 1.0, 1.0) - if self.style_manager.image_button(self.icons['rotate'], (24, 24), tint_col=tint_col): - self.tool_manager.setCurrentTool("旋转") - if imgui.is_item_hovered(): - imgui.set_tooltip("旋转工具 (E)") - imgui.same_line() - else: - if imgui.button("旋转##rotate_tool"): - self.tool_manager.setCurrentTool("旋转") - if rotate_active: - draw_list = imgui.get_window_draw_list() - button_min = imgui.get_item_rect_min() - button_max = imgui.get_item_rect_max() - draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3))) - imgui.same_line() - - # 缩放工具按钮 - scale_active = self.tool_manager.isScaleTool() - if self.icons.get('scale'): - tint_col = (1.0, 1.0, 0.0, 1.0) if scale_active else (1.0, 1.0, 1.0, 1.0) - if self.style_manager.image_button(self.icons['scale'], (24, 24), tint_col=tint_col): - self.tool_manager.setCurrentTool("缩放") - if imgui.is_item_hovered(): - imgui.set_tooltip("缩放工具 (R)") - else: - if imgui.button("缩放##scale_tool"): - self.tool_manager.setCurrentTool("缩放") - if scale_active: - draw_list = imgui.get_window_draw_list() - button_min = imgui.get_item_rect_min() - button_max = imgui.get_item_rect_max() - draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3))) - - imgui.same_line() - imgui.separator() - imgui.same_line() - - # 工具按钮已移除(导入、保存、播放) - - def _draw_scene_tree(self): - """绘制场景树面板""" - # 使用更少的限制性标志,允许docking - flags = (imgui.WindowFlags_.no_collapse) - - with self.style_manager.begin_styled_window("场景树", self.showSceneTree, flags): - self.showSceneTree = True # 确保窗口保持打开 - - imgui.text("场景层级") - imgui.separator() - - # 构建动态场景树 - self._build_scene_tree() - - def _build_scene_tree(self): - """构建动态场景树""" - # 渲染节点 - if imgui.tree_node("渲染"): - # 环境光 - if hasattr(self, 'ambient_light') and self.ambient_light: - self._draw_scene_node(self.ambient_light, "环境光", "light") - - # 聚光灯 - if hasattr(self, 'scene_manager') and self.scene_manager: - if hasattr(self.scene_manager, 'Spotlight') and self.scene_manager.Spotlight: - for i, spotlight in enumerate(self.scene_manager.Spotlight): - self._draw_scene_node(spotlight, f"聚光灯_{i+1}", "light") - if hasattr(self.scene_manager, 'Pointlight') and self.scene_manager.Pointlight: - for i, pointlight in enumerate(self.scene_manager.Pointlight): - self._draw_scene_node(pointlight, f"点光源_{i+1}", "light") - - # 地板 - if hasattr(self, 'ground') and self.ground: - self._draw_scene_node(self.ground, "地板", "geometry") - - imgui.tree_pop() - - # 相机节点 - if imgui.tree_node("相机"): - if hasattr(self, 'camera') and self.camera: - self._draw_scene_node(self.camera, "主相机", "camera") - imgui.tree_pop() - - # 3D模型节点 - if imgui.tree_node("模型"): - if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'): - if self.scene_manager.models: - for i, model in enumerate(self.scene_manager.models): - if model and not model.isEmpty(): - self._draw_scene_node(model, model.getName() or f"模型_{i+1}", "model") - else: - imgui.text("(空)") - else: - imgui.text("(空)") - imgui.tree_pop() - - # GUI元素节点 - if imgui.tree_node("GUI元素"): - if hasattr(self, 'gui_manager') and self.gui_manager and hasattr(self.gui_manager, 'gui_elements'): - if self.gui_manager.gui_elements: - for gui_element in self.gui_manager.gui_elements: - if gui_element and hasattr(gui_element, 'node'): - gui_type = getattr(gui_element, 'gui_type', 'GUI_UNKNOWN') - display_name = getattr(gui_element, 'name', gui_type) - self._draw_scene_node(gui_element.node, display_name, "gui", gui_type) - else: - imgui.text("(空)") - else: - imgui.text("(空)") - imgui.tree_pop() - - def _draw_scene_node(self, node, name, node_type, gui_subtype=None): - """绘制单个场景节点""" - if not node or node.isEmpty(): - return - - # 检查是否被选中 - is_selected = (hasattr(self, 'selection') and self.selection and - hasattr(self.selection, 'selectedNode') and - self.selection.selectedNode == node) - - # 节点可见性 - is_visible = node.is_hidden() == False - - # 设置选择颜色 - if is_selected: - imgui.push_style_color(imgui.Col_.text, (0.2, 0.6, 1.0, 1.0)) - - try: - # 显示节点 - node_open = imgui.tree_node(name) - - # 处理节点选择 - if imgui.is_item_clicked(): - if hasattr(self, 'selection') and self.selection: - self.selection.updateSelection(node) - - # 右键菜单 - if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): - self._show_node_context_menu(node, name, node_type) - - # 显示节点属性 - imgui.same_line() - if is_visible: - imgui.text_colored((0.5, 1.0, 0.5, 1.0), "可见") - else: - imgui.text_colored((0.5, 0.5, 0.5, 1.0), "隐藏") - - if node_open: - # 如果有子节点,递归显示 - if node.getNumChildren() > 0: - for i in range(node.getNumChildren()): - child = node.getChild(i) - if child and not child.isEmpty(): - child_name = child.getName() or f"子节点_{i+1}" - self._draw_scene_node(child, child_name, node_type) - imgui.tree_pop() - except Exception as e: - print(f"绘制场景节点时出错: {e}") - finally: - # 确保颜色状态被恢复 - if is_selected: - imgui.pop_style_color() - - def _show_node_context_menu(self, node, name, node_type): - """显示节点右键菜单""" - self._context_menu_node = True - self._context_menu_target = node - - def _draw_resource_manager(self): - """绘制资源管理器面板""" - # 使用面板类型的窗口标志,支持docking - flags = self.style_manager.get_window_flags("panel") - - with self.style_manager.begin_styled_window("资源管理器", self.showResourceManager, flags): - self.showResourceManager = True # 确保窗口保持打开 - - # 获取资源管理器实例 - rm = self.resource_manager - - # 工具栏 - imgui.text("文件浏览器") - imgui.separator() - - # 导航按钮 - if imgui.button("◀"): - rm.navigate_back() - imgui.same_line() - if imgui.button("▶"): - rm.navigate_forward() - imgui.same_line() - if imgui.button("▲"): - rm.navigate_up() - imgui.same_line() - if imgui.button("主页"): - rm.navigate_to(rm.project_root / "Resources") - imgui.same_line() - if imgui.button("刷新"): - rm.force_refresh() - - # 自动刷新开关 - imgui.same_line() - changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled) - if changed: - rm.set_auto_refresh(rm.auto_refresh_enabled) - - imgui.same_line() - imgui.text(" ") - imgui.same_line() - - # 路径输入框 - changed, new_path = imgui.input_text("路径", str(rm.current_path), 256) - if changed: - try: - rm.navigate_to(Path(new_path)) - except: - pass - - # 搜索框 - changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256) - - imgui.separator() - - # 检查自动刷新 - if rm.refresh_if_needed(): - # 目录内容发生变化,可以在这里添加通知逻辑 - pass - - # 获取目录内容 - dirs, files = rm.get_directory_contents(rm.current_path) - - # 显示目录 - for dir_path in dirs: - if not rm.should_show_file(dir_path): - continue - - # 目录图标和名称 - icon_name = rm.get_file_icon(dir_path.name, is_folder=True) - node_open = False - - # 检查是否被选中 - is_selected = dir_path in rm.selected_files - - # 使用TreeNode来显示目录 - if is_selected: - imgui.push_style_color(imgui.Col_.header, (100/255, 150/255, 200/255, 1.0)) - - # 尝试加载PNG图标 - icon_texture = None - try: - # 直接使用图标名称,load_icon会自动添加.png - icon_texture = self.style_manager.load_icon(f"file_types/{icon_name}") - except: - pass - - if icon_texture: - # 使用PNG图标 - imgui.image(icon_texture, (16, 16)) - imgui.same_line() - node_open = imgui.tree_node(f"{dir_path.name}") - else: - # 回退到文本标识符 - node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}") - - if is_selected: - imgui.pop_style_color() - - # 处理选择 - if imgui.is_item_clicked(): - if imgui.get_io().key_ctrl: - # 多选模式 - if is_selected: - rm.selected_files.discard(dir_path) - else: - rm.selected_files.add(dir_path) - else: - # 单选模式 - rm.selected_files.clear() - rm.selected_files.add(dir_path) - rm.focused_file = dir_path - - # 双击导航 - if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - rm.navigate_to(dir_path) - - # 右键菜单 - if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): - rm.show_context_menu = True - rm.context_menu_file = dir_path - rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) - - # 如果节点展开,显示子内容 - if node_open: - # 获取子目录内容 - subdirs, subfiles = rm.get_directory_contents(dir_path) - - # 显示子目录 - for subdir in subdirs: - if not rm.should_show_file(subdir): - continue - - # 初始化变量 - subicon_name = "folder" - sub_is_selected = False - - # 获取子目录图标名称 - subicon_name = rm.get_file_icon(subdir.name, is_folder=True) - sub_is_selected = subdir in rm.selected_files - - # 尝试加载PNG图标 - subicon_texture = None - try: - subicon_texture = self.style_manager.load_icon(f"file_types/{subicon_name}") - except: - pass - - if subicon_texture: - # 使用PNG图标 - imgui.image(subicon_texture, (16, 16)) - imgui.same_line() - sub_node_open = imgui.tree_node(f" {subdir.name}") - else: - # 回退到文本标识符 - sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}") - - if sub_is_selected: - imgui.pop_style_color() - - # 处理子目录的选择 - if imgui.is_item_clicked(): - if imgui.get_io().key_ctrl: - if sub_is_selected: - rm.selected_files.discard(subdir) - else: - rm.selected_files.add(subdir) - else: - rm.selected_files.clear() - rm.selected_files.add(subdir) - rm.focused_file = subdir - - # 双击子目录导航 - if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - rm.navigate_to(subdir) - - # 右键菜单 - if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): - rm.show_context_menu = True - rm.context_menu_file = subdir - rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) - - if sub_node_open: - imgui.tree_pop() - - # 显示子文件 - for subfile in subfiles: - if not rm.should_show_file(subfile): - continue - - subicon_name = rm.get_file_icon(subfile.name) - sub_is_selected = subfile in rm.selected_files - - # 尝试加载PNG图标 - subicon_texture = None - try: - subicon_texture = self.style_manager.load_icon(f"file_types/{subicon_name}") - except: - pass - - if subicon_texture: - # 使用PNG图标 - imgui.image(subicon_texture, (16, 16)) - imgui.same_line() - selected = imgui.selectable(f" {subfile.name}", sub_is_selected) - else: - # 回退到文本标识符 - selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}", sub_is_selected) - - # 处理子文件的选择 - if selected: - if imgui.get_io().key_ctrl: - if sub_is_selected: - rm.selected_files.discard(subfile) - else: - rm.selected_files.add(subfile) - else: - rm.selected_files.clear() - rm.selected_files.add(subfile) - rm.focused_file = subfile - - # 双击子文件操作 - if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - if subfile.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: - self.scene_manager.importModel(str(subfile)) - self.add_info_message(f"正在导入模型: {subfile.name}") - else: - rm.open_file(subfile) - - # 右键菜单 - if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): - rm.show_context_menu = True - rm.context_menu_file = subfile - rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) - - # 只有在节点展开时才调用tree_pop - if node_open: - imgui.tree_pop() - - - - # 处理拖拽开始 - if imgui.is_item_active() and imgui.is_item_hovered(): - if imgui.is_mouse_dragging(0): - # 开始拖拽 - drag_files = list(rm.selected_files) if rm.selected_files else [file_path] - rm.start_drag(drag_files) - self.is_dragging = True - self.show_drag_overlay = True - - # 双击打开文件 - if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - # 检查是否是支持的3D模型格式 - if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: - # 导入3D模型 - self.add_info_message(f"正在导入模型: {file_path.name}") - self.scene_manager.importModel(str(file_path)) - else: - # 使用系统默认程序打开 - rm.open_file(file_path) - - # 右键菜单 - if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): - rm.show_context_menu = True - rm.context_menu_file = file_path - rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) - - # 右键菜单 - if rm.show_context_menu and rm.context_menu_file: - imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1])) - with imgui_ctx.begin_popup("context_menu", imgui.WindowFlags_.no_title_bar | - imgui.WindowFlags_.no_resize | imgui.WindowFlags_.always_auto_resize) as popup: - if popup: - if rm.context_menu_file.is_dir(): - if imgui.menu_item("打开"): - rm.navigate_to(rm.context_menu_file) - imgui.separator() - if imgui.menu_item("重命名"): - print(f"重命名文件夹: {rm.context_menu_file.name}") - if imgui.menu_item("删除"): - print(f"删除文件夹: {rm.context_menu_file.name}") - else: - if imgui.menu_item("打开"): - rm.open_file(rm.context_menu_file) - imgui.separator() - if imgui.menu_item("导入到场景"): - if rm.context_menu_file.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: - self.add_info_message(f"正在导入模型: {rm.context_menu_file.name}") - self.scene_manager.importModel(str(rm.context_menu_file)) - if imgui.menu_item("重命名"): - print(f"重命名文件: {rm.context_menu_file.name}") - if imgui.menu_item("删除"): - print(f"删除文件: {rm.context_menu_file.name}") - - imgui.separator() - if imgui.menu_item("复制路径"): - imgui.set_clipboard_text(str(rm.context_menu_file)) - self.add_info_message("路径已复制到剪贴板") - if imgui.menu_item("在文件管理器中显示"): - import platform - import subprocess - if platform.system() == "Windows": - subprocess.run(["explorer", "/select,", str(rm.context_menu_file)]) - elif platform.system() == "Darwin": - subprocess.run(["open", "-R", str(rm.context_menu_file)]) - else: - subprocess.run(["xdg-open", str(rm.context_menu_file.parent)]) - - # 如果点击其他地方,关闭菜单 - if imgui.is_mouse_clicked(0) or imgui.is_mouse_clicked(1): - if not imgui.is_window_hovered(): - rm.show_context_menu = False - rm.context_menu_file = None - - def _draw_property_panel(self): - """绘制属性面板""" - # 使用面板类型的窗口标志,支持docking - flags = self.style_manager.get_window_flags("panel") - - with self.style_manager.begin_styled_window("属性面板", self.showPropertyPanel, flags): - self.showPropertyPanel = True # 确保窗口保持打开 - - # 获取当前选中的节点 - selected_node = None - if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): - selected_node = self.selection.selectedNode - - if selected_node and not selected_node.isEmpty(): - self._draw_node_properties(selected_node) - else: - # 无选中对象时显示提示(模仿Qt版本的空状态样式) - imgui.spacing() - imgui.spacing() - - # 居中显示提示信息 - window_width = imgui.get_window_width() - text_width = 200 # 估算文本宽度 - text_pos_x = (window_width - text_width) / 2 - - imgui.set_cursor_pos_x(text_pos_x) - imgui.text_colored((0.5, 0.5, 0.5, 1.0), "🔍 未选择任何对象") - - imgui.set_cursor_pos_x(text_pos_x - 20) - imgui.text("请从场景树中选择一个对象") - - imgui.set_cursor_pos_x(text_pos_x + 10) - imgui.text("以查看其属性") - - imgui.spacing() - imgui.spacing() - - # 添加一些分隔线和装饰 - imgui.separator() - - # 显示快速提示 - imgui.text("💡 快速提示:") - imgui.bullet_text("单击场景树中的对象进行选择") - imgui.bullet_text("使用 F 键快速聚焦到选中对象") - imgui.bullet_text("使用 Delete 键删除选中对象") - - def _draw_node_properties(self, node): - """绘制节点属性""" - if not node or node.isEmpty(): - # 停止变换监控 - self.stop_transform_monitoring() - return - - # 检查是否需要重新启动变换监控 - if self._monitored_node != node: - self.stop_transform_monitoring() - self.start_transform_monitoring(node) - - # 获取节点基本信息 - node_name = node.getName() or "未命名节点" - node_type = self._get_node_type_from_node(node) - - # 添加一些间距,模仿Qt版本的布局 - imgui.spacing() - - # 物体名称组(使用Qt版本的样式) - if imgui.collapsing_header("物体名称"): - # 第一行:可见性复选框和名称输入 - user_visible = node.getPythonTag("user_visible") - if user_visible is None: - user_visible = True - node.setPythonTag("user_visible", True) - - # 可见性复选框(模仿Qt版本的样式) - changed, is_visible = imgui.checkbox("##visibility", user_visible) - if changed: - node.setPythonTag("user_visible", is_visible) - if is_visible: - node.show() - else: - node.hide() - - imgui.same_line() - imgui.text("可见") - imgui.same_line() - imgui.spacing() - imgui.same_line() - - # 名称输入框(模仿Qt版本的样式) - imgui.text("名称:") - imgui.same_line() - changed, new_name = imgui.input_text("##name", node_name, 256) - if changed and hasattr(self, 'selection'): - # 更新场景树中的名称 - self._update_node_name(node, new_name) - - # 添加分隔线 - imgui.separator() - - # 状态徽章(模仿Qt版本的徽章样式) - self._draw_status_badges(node) - - imgui.spacing() - - # 变换属性组 - if imgui.collapsing_header("变换 Transform"): - self._draw_transform_properties(node) - - # 根据节点类型显示特定属性组 - if node_type == "GUI元素": - if imgui.collapsing_header("GUI信息"): - self._draw_gui_properties(node) - elif node_type == "光源": - if imgui.collapsing_header("光源属性"): - self._draw_light_properties(node) - elif node_type == "模型": - if imgui.collapsing_header("模型属性"): - self._draw_model_properties(node) - - # 动画控制组(只对模型显示) - if imgui.collapsing_header("动画控制"): - self._draw_animation_properties(node) - - # 外观属性组(通用) - if imgui.collapsing_header("外观属性"): - self._draw_appearance_properties(node) - - # 碰撞检测组 - if imgui.collapsing_header("碰撞检测"): - self._draw_collision_properties(node) - - # 操作按钮组 - if imgui.collapsing_header("操作"): - self._draw_property_actions(node) - - def _getActor(self, origin_model): - """ - 获取或创建模型的Actor,用于动画控制 - 复用Qt版本经过验证的实现方式 - """ - # 检查缓存 - if origin_model in self._actor_cache: - return self._actor_cache[origin_model] - - # 尝试直接从内存创建 - if origin_model.hasTag("can_create_actor_from_memory"): - try: - test_actor = Actor(origin_model) - anims = test_actor.getAnimNames() - self._actor_cache[origin_model] = test_actor - print(f"[Actor加载] 内存创建检测到动画: {anims}") - if anims: - return test_actor - else: - test_actor.cleanup() - test_actor.removeNode() - except Exception as e: - print(f"从内存模型创建Actor失败: {e}") - - # 如果不能直接从内存创建,再尝试通过文件路径加载 - filepath = origin_model.getTag("model_path") - if not filepath: - return None + self.editor_panels.draw_toolbar() - print(f"[Actor加载] 尝试加载: {filepath}") + def _draw_scene_tree(self, *args, **kwargs): + return self.editor_panels._draw_scene_tree(*args, **kwargs) - # 处理跨平台路径问题 - import os - # 检查路径是否有效,如果无效则尝试修复 - if not os.path.exists(filepath): - original_filepath = filepath - # 尝试多种修复策略 - fixed = False + def _build_scene_tree(self, *args, **kwargs): + return self.editor_panels._build_scene_tree(*args, **kwargs) - import platform - # 策略1: 处理Linux风格路径在Windows上的问题 - if filepath.startswith('/') and platform.system() == "Windows": - print("[路径转换] 尝试处理Linux风格路径:", filepath) - path_parts = filepath.split('/') - print(platform.system()) - if len(path_parts) > 1: - drive_letter = path_parts[1].upper() + ':\\' # 添加反斜杠确保正确路径格式 - remaining_path = '\\'.join(path_parts[2:]) if len(path_parts) > 2 else '' - potential_path = os.path.join(drive_letter, remaining_path) - print(f"[路径转换] 构造的潜在路径: {potential_path}") - if os.path.exists(potential_path): - filepath = potential_path - fixed = True - print(f"[路径转换] 成功: {original_filepath} -> {filepath}") - else: - print(f"[路径转换] 文件不存在: {potential_path}") - - - # 策略2: 处理路径分隔符问题 - if not fixed: - # 尝试规范化路径 - normalized_path = os.path.normpath(filepath) - print(f"[路径规范化] 尝试规范化路径: {filepath} -> {normalized_path}") - if os.path.exists(normalized_path): - filepath = normalized_path - fixed = True - print(f"[路径规范化] 成功: {filepath}") - else: - print(f"[路径规范化] 文件不存在: {normalized_path}") - - # 策略3: 在Resources目录中查找 - if not fixed: - # 尝试在Resources目录中查找文件 - resources_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Resources") - filename = os.path.basename(filepath) - potential_path = os.path.join(resources_path, filename) - print(f"[Resources查找] 尝试在Resources目录查找: {potential_path}") - if os.path.exists(potential_path): - filepath = potential_path - fixed = True - print(f"[Resources查找] 成功: {filepath}") - else: - print(f"[Resources查找] 文件不存在: {potential_path}") - - if fixed: - print(f"路径修复: {original_filepath} -> {filepath}") - # 更新模型标签 - origin_model.setTag("model_path", filepath) - else: - print(f"[警告] 模型文件不存在: {filepath}") - return None + def _draw_scene_node(self, *args, **kwargs): + return self.editor_panels._draw_scene_node(*args, **kwargs) - # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 - if filepath.lower().endswith('.fbx'): - pass - #return self._createFBXActor(origin_model, filepath) + def _show_node_context_menu(self, *args, **kwargs): + return self.editor_panels._show_node_context_menu(*args, **kwargs) - # 其他格式使用标准 Actor 加载 - try: - import gltf - from panda3d.core import Filename - - # 将Panda3D路径转换为操作系统特定路径 - panda_filename = Filename(filepath) - os_specific_path = panda_filename.to_os_specific() - print(f"[路径转换] {filepath} -> {os_specific_path}") - - print(f"[GLTF加载] 尝试加载: {os_specific_path}") - - # 使用明确的设置确保动画被加载 - gltf_settings = gltf.GltfSettings(skip_animations=False) - model_root = gltf.load_model(os_specific_path, gltf_settings) - model_node = NodePath(model_root) - test_actor = Actor(model_node) - anims = test_actor.getAnimNames() - test_actor.reparentTo(self.render) - self._actor_cache[origin_model] = test_actor - print(f"[Actor加载] 标准加载检测到动画: {anims}") - if not anims: - test_actor.cleanup() - test_actor.removeNode() - return None - return test_actor - except Exception as e: - print(f"创建Actor失败: {e}") - return None - - def _getModelFormat(self, origin_model): - """获取模型格式信息""" - filepath = origin_model.getTag("model_path") - original_path = origin_model.getTag("original_path") - converted_from = origin_model.getTag("converted_from") + def _draw_resource_manager(self, *args, **kwargs): + return self.editor_panels._draw_resource_manager(*args, **kwargs) - if filepath: - ext = filepath.lower().split('.')[-1] - format_name = ext.upper() + def _draw_property_panel(self, *args, **kwargs): + return self.editor_panels._draw_property_panel(*args, **kwargs) - # 如果是转换后的文件,显示转换信息 - if converted_from and original_path: - original_ext = converted_from.upper() - format_name = f"{format_name} (从{original_ext}转换)" + def _draw_node_properties(self, *args, **kwargs): + return self.editor_panels._draw_node_properties(*args, **kwargs) - return format_name - return "未知" + def _getActor(self, *args, **kwargs): + return self.animation_tools._getActor(*args, **kwargs) - def _processAnimationNames(self, origin_model, anim_names): - """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]""" - format_info = self._getModelFormat(origin_model) - processed = [] + def _getModelFormat(self, *args, **kwargs): + return self.animation_tools._getModelFormat(*args, **kwargs) - print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + def _processAnimationNames(self, *args, **kwargs): + return self.animation_tools._processAnimationNames(*args, **kwargs) - for name in anim_names: - display_name = name - original_name = name + def _isLikelyBoneGroup(self, *args, **kwargs): + return self.animation_tools._isLikelyBoneGroup(*args, **kwargs) - if format_info == "GLB": - # GLB 格式通常有真实的动画名称 - if "|" in name: - # 处理类似 'Armature|mixamo.com|Layer0' 的名称 - parts = name.split("|") - if "mixamo" in name.lower(): - # Mixamo 动画 - display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name - elif len(parts) > 2: - # 其他复杂命名 - display_name = f"{parts[0]}_{parts[-1]}" - else: - display_name = parts[-1] + def _analyzeAnimationQuality(self, *args, **kwargs): + return self.animation_tools._analyzeAnimationQuality(*args, **kwargs) - elif format_info == "FBX": - # FBX 格式可能需要特殊处理 - if self._isLikelyBoneGroup(name): - # 检查是否是骨骼组而非动画 - print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组") - display_name = f"⚠️ {name} (可能非动画)" - else: - display_name = name + def _playAnimation(self, *args, **kwargs): + return self.animation_tools._playAnimation(*args, **kwargs) - elif format_info in ["EGG", "BAM"]: - # 原生格式通常命名规范 - display_name = name + def _pauseAnimation(self, *args, **kwargs): + return self.animation_tools._pauseAnimation(*args, **kwargs) - processed.append((display_name, original_name)) - print(f"[动画分析] {original_name} → {display_name}") + def _stopAnimation(self, *args, **kwargs): + return self.animation_tools._stopAnimation(*args, **kwargs) - return processed + def _loopAnimation(self, *args, **kwargs): + return self.animation_tools._loopAnimation(*args, **kwargs) - def _isLikelyBoneGroup(self, name): - """判断动画名称是否更像骨骼组而不是动画序列""" - bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig'] - name_lower = name.lower() + def _setAnimationSpeed(self, *args, **kwargs): + return self.animation_tools._setAnimationSpeed(*args, **kwargs) - # 如果包含这些关键词,可能是骨骼组 - for indicator in bone_indicators: - if indicator in name_lower: - return True - - # 如果名称太简单(少于3个字符),可能不是动画 - if len(name) < 3: - return True - - return False - - def _analyzeAnimationQuality(self, actor, anim_names, format_info): - """分析动画质量和类型(优化版本,减少详细分析以提高性能)""" - try: - valid_anims = 0 - - # 简化分析:只检查动画是否存在,不详细分析帧数 - for anim_name in anim_names: - try: - control = actor.getAnimControl(anim_name) - if control and control.getNumFrames() > 1: - valid_anims += 1 - except Exception: - # 忽略单个动画的分析错误,继续处理其他动画 - continue - - if valid_anims == 0: - return "⚠️ 无有效动画" - elif valid_anims < len(anim_names): - return f"⚠️ {valid_anims}/{len(anim_names)} 个有效" - else: - return f"✓ {valid_anims} 个动画" - - except Exception as e: - # 简化错误处理 - return "分析异常" - + def _clear_animation_cache(self, *args, **kwargs): + return self.animation_tools._clear_animation_cache(*args, **kwargs) def _get_node_type_from_node(self, node): """从节点判断其类型""" # 检查是否为GUI元素 @@ -1945,5209 +899,449 @@ class MyWorld(CoreWorld): # 默认为几何体 return "几何体" - def _draw_status_badges(self, node): - """绘制状态徽章(模仿Qt版本的徽章样式)""" - imgui.text("状态标签: ") - - # 可见性状态徽章 - is_visible = not node.is_hidden() - visibility_color = (0.176, 1.0, 0.769, 1.0) if is_visible else (0.953, 0.616, 0.471, 1.0) - visibility_text = "可见" if is_visible else "隐藏" - - imgui.same_line() - imgui.text_colored(visibility_color, f"[{visibility_text}]") - - # 节点类型徽章 - node_type = self._get_node_type_from_node(node) - type_colors = { - "GUI元素": (0.188, 0.404, 0.753, 1.0), # 主题蓝色 - "光源": (1.0, 0.8, 0.2, 1.0), # 黄色 - "模型": (0.6, 0.8, 1.0, 1.0), # 浅蓝色 - "相机": (0.8, 0.8, 0.2, 1.0), # 橙色 - "几何体": (0.5, 0.5, 0.5, 1.0), # 灰色 - } - - if node_type in type_colors: - imgui.same_line() - imgui.text_colored(type_colors[node_type], f"[{node_type}]") - - # 功能性徽章 - badges = [] - - # 碰撞体徽章 - has_collision = hasattr(node, 'getChild') and any('Collision' in child.getName() for child in node.getChildren() if child.getName()) - if has_collision: - badges.append(("碰撞", (0.2, 0.4, 0.8, 1.0))) # 蓝色 - - # 脚本徽章 - has_script = hasattr(node, 'getPythonTag') and node.getPythonTag('script') - if has_script: - badges.append(("脚本", (0.8, 0.4, 0.8, 1.0))) # 紫色 - - # 动画徽章(优化检测逻辑,避免重复创建Actor) - has_animation = False - if node_type == "模型": # 只对模型类型进行动画检测 - # 首先检查是否已经缓存了检测结果 - cached_result = node.getPythonTag('animation') - if cached_result is not None: - has_animation = cached_result - else: - # 只有在未缓存时才进行检测 - try: - # 使用轻量级检测:先检查文件扩展名 - model_path = node.getTag("model_path") - if model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx')): - # 对于可能包含动画的格式,才进行Actor检测 - actor = self._getActor(node) - if actor and actor.getAnimNames(): - has_animation = True - # 缓存检测结果 - node.setPythonTag('animation', has_animation) - print(f"[动画检测] {node.getName()}: {'有动画' if has_animation else '无动画'}") - else: - # 对于不太可能有动画的格式,直接标记为无动画 - node.setPythonTag('animation', False) - except Exception as e: - print(f"动画检测失败: {e}") - node.setPythonTag('animation', False) - else: - # 对于非模型类型,检查已有的动画标签 - has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation') - - if has_animation: - badges.append(("动画", (0.4, 0.8, 0.4, 1.0))) # 绿色 - - # 材质徽章 - has_material = hasattr(node, 'getMaterial') and node.getMaterial() - if has_material: - badges.append(("材质", (0.8, 0.6, 0.2, 1.0))) # 金色 - - # 绘制功能性徽章 - for badge_text, badge_color in badges: - imgui.same_line() - imgui.text_colored(badge_color, f"[{badge_text}]") - - # 如果没有特殊徽章,显示默认状态 - if not badges: - imgui.same_line() - imgui.text_colored((0.5, 0.5, 0.5, 1.0), "[标准对象]") - - def _draw_transform_properties(self, node): - """绘制变换属性""" - # 位置组 - if imgui.collapsing_header("位置 Position"): - # 相对位置 - imgui.text("相对位置") - pos = node.getPos() - - # X坐标 - changed, new_x = imgui.input_float("X##pos_x", pos.x, 0.1, 1.0, "%.3f") - if changed: node.setX(new_x) - - # Y坐标 - changed, new_y = imgui.input_float("Y##pos_y", pos.y, 0.1, 1.0, "%.3f") - if changed: node.setY(new_y) - - # Z坐标 - changed, new_z = imgui.input_float("Z##pos_z", pos.z, 0.1, 1.0, "%.3f") - if changed: node.setZ(new_z) - - # 世界位置 - imgui.text("世界位置") - world_pos = node.getPos(self.render) - - imgui.text(f"世界 X: {world_pos.x:.3f}") - imgui.text(f"世界 Y: {world_pos.y:.3f}") - imgui.text(f"世界 Z: {world_pos.z:.3f}") - - # 位置操作按钮 - if imgui.button("重置位置##reset_pos"): - node.setPos(0, 0, 0) - imgui.same_line() - if imgui.button("复制位置##copy_pos"): - self._clipboard_pos = (pos.x, pos.y, pos.z) - imgui.same_line() - if imgui.button("粘贴位置##paste_pos") and hasattr(self, '_clipboard_pos'): - node.setPos(self._clipboard_pos[0], self._clipboard_pos[1], self._clipboard_pos[2]) - - # 旋转组 - if imgui.collapsing_header("旋转 Rotation"): - hpr = node.getHpr() - - # HPR旋转 - imgui.text("HPR 旋转 (度)") - changed, new_h = imgui.input_float("H##rot_h", hpr.x, 1.0, 10.0, "%.1f") - if changed: node.setH(new_h) - - changed, new_p = imgui.input_float("P##rot_p", hpr.y, 1.0, 10.0, "%.1f") - if changed: node.setP(new_p) - - changed, new_r = imgui.input_float("R##rot_r", hpr.z, 1.0, 10.0, "%.1f") - if changed: node.setR(new_r) - - # 旋转操作按钮 - if imgui.button("重置旋转##reset_rot"): - node.setHpr(0, 0, 0) - imgui.same_line() - if imgui.button("随机旋转##random_rot"): - import random - node.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360)) - - # 缩放组 - if imgui.collapsing_header("缩放 Scale"): - scale = node.getScale() - - # XYZ缩放 - imgui.text("XYZ 缩放") - changed, new_sx = imgui.input_float("X##scale_x", scale.x, 0.1, 1.0, "%.3f") - if changed: node.setSx(new_sx) - - changed, new_sy = imgui.input_float("Y##scale_y", scale.y, 0.1, 1.0, "%.3f") - if changed: node.setSy(new_sy) - - changed, new_sz = imgui.input_float("Z##scale_z", scale.z, 0.1, 1.0, "%.3f") - if changed: node.setSz(new_sz) - - # 统一缩放 - if imgui.button("统一缩放##uniform_scale"): - uniform_scale = (scale.x + scale.y + scale.z) / 3.0 - node.setScale(uniform_scale, uniform_scale, uniform_scale) - imgui.same_line() - if imgui.button("重置缩放##reset_scale"): - node.setScale(1, 1, 1) - imgui.same_line() - if imgui.button("翻倍##double_scale"): - node.setScale(scale.x * 2, scale.y * 2, scale.z * 2) - - def _draw_gui_properties(self, node): - """绘制GUI元素属性""" - # 获取GUI元素 - gui_element = None - if hasattr(node, 'getPythonTag'): - gui_element = node.getPythonTag('gui_element') - - if not gui_element: - imgui.text("无GUI元素数据") - return - - # GUI类型信息 - gui_type = getattr(gui_element, 'gui_type', 'UNKNOWN') - imgui.text(f"GUI类型: {gui_type}") - - # 基本属性 - if imgui.collapsing_header("基本属性"): - # 文本内容 (适用于按钮、标签等) - if hasattr(gui_element, 'text'): - changed, new_text = imgui.input_text("文本内容", gui_element.text, 256) - if changed and hasattr(self, 'gui_manager'): - self.gui_manager.editGUIElement(gui_element, 'text', new_text) - - # GUI ID - gui_id = getattr(gui_element, 'id', '') - changed, new_id = imgui.input_text("GUI ID", gui_id, 64) - if changed and hasattr(self, 'gui_manager'): - gui_element.id = new_id - - # 变换属性 - if imgui.collapsing_header("变换属性"): - # 位置 - pos = gui_element.getPos() - imgui.text("位置") - - if gui_type in ["button", "label", "entry", "2d_image"]: - # 2D GUI组件使用屏幕坐标 - imgui.text("屏幕坐标") - logical_x = pos.getX() / 0.1 - logical_z = pos.getZ() / 0.1 - - changed, new_x = imgui.input_float("X##gui_pos_x", logical_x, 1.0, 10.0, "%.1f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setX(new_x * 0.1) - - changed, new_z = imgui.input_float("Y##gui_pos_y", logical_z, 1.0, 10.0, "%.1f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setZ(new_z * 0.1) - else: - # 3D GUI组件使用世界坐标 - imgui.text("世界坐标") - changed, new_x = imgui.input_float("X##gui_world_x", pos.getX(), 0.1, 1.0, "%.3f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setX(new_x) - - changed, new_y = imgui.input_float("Y##gui_world_y", pos.getY(), 0.1, 1.0, "%.3f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setY(new_y) - - changed, new_z = imgui.input_float("Z##gui_world_z", pos.getZ(), 0.1, 1.0, "%.3f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setZ(new_z) - - # 缩放 - scale = gui_element.getScale() - imgui.text("缩放") - changed, new_sx = imgui.input_float("X##gui_scale_x", scale.getX(), 0.1, 1.0, "%.3f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setSx(new_sx) - - changed, new_sy = imgui.input_float("Y##gui_scale_y", scale.getY(), 0.1, 1.0, "%.3f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setSy(new_sy) - - changed, new_sz = imgui.input_float("Z##gui_scale_z", scale.getZ(), 0.1, 1.0, "%.3f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setSz(new_sz) - - # 旋转 - hpr = gui_element.getHpr() - imgui.text("旋转") - changed, new_h = imgui.input_float("H##gui_rot_h", hpr.getX(), 1.0, 10.0, "%.1f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setH(new_h) - - changed, new_p = imgui.input_float("P##gui_rot_p", hpr.getY(), 1.0, 10.0, "%.1f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setP(new_p) - - changed, new_r = imgui.input_float("R##gui_rot_r", hpr.getZ(), 1.0, 10.0, "%.1f") - if changed and hasattr(self, 'gui_manager'): - gui_element.setR(new_r) - - # 外观属性 - if imgui.collapsing_header("外观属性"): - # 大小 - if hasattr(gui_element, 'size'): - size = gui_element.size - imgui.text("大小") - changed, new_w = imgui.input_float("宽度", size[0], 1.0, 10.0, "%.1f") - if changed and hasattr(self, 'gui_manager'): - new_size = (new_w, size[1]) - self.gui_manager.editGUIElement(gui_element, 'size', new_size) - - changed, new_h = imgui.input_float("高度", size[1], 1.0, 10.0, "%.1f") - if changed and hasattr(self, 'gui_manager'): - new_size = (size[0], new_h) - self.gui_manager.editGUIElement(gui_element, 'size', new_size) - - # 颜色 - if hasattr(gui_element, 'getColor'): - try: - color = gui_element.getColor() - # 确保颜色是有效的 - if not color or (hasattr(color, '__len__') and len(color) < 3): - color = (1.0, 1.0, 1.0, 1.0) # 默认白色 - except: - color = (1.0, 1.0, 1.0, 1.0) # 默认白色 - - imgui.text("颜色") - # 获取颜色值 - if hasattr(color, 'getX'): - # 如果是Panda3D的Vec4对象 - r, g, b = color.getX(), color.getY(), color.getZ() - else: - # 如果是元组或其他格式 - r, g, b = color[0], color[1], color[2] - - changed, new_r = imgui.slider_float("R##gui_color_r", r, 0.0, 1.0) - if changed: gui_element.setColor(new_r, g, b, 1.0) - - changed, new_g = imgui.slider_float("G##gui_color_g", g, 0.0, 1.0) - if changed: gui_element.setColor(r, new_g, b, 1.0) - - changed, new_b = imgui.slider_float("B##gui_color_b", b, 0.0, 1.0) - if changed: gui_element.setColor(r, g, new_b, 1.0) - # 透明度 - imgui.text("透明度") - current_alpha = getattr(gui_element, 'alpha', 1.0) - changed, new_alpha = imgui.slider_float("Alpha", current_alpha, 0.0, 1.0) - if changed: - gui_element.alpha = new_alpha - if hasattr(gui_element, 'setTransparency'): - # 将0.0-1.0范围转换为Panda3D的透明度格式 - panda_transparency = int((1.0 - new_alpha) * 255) - gui_element.setTransparency(panda_transparency) - - # 渲染顺序 - imgui.text("渲染顺序") - current_sort = getattr(gui_element, 'sort', 0) - changed, new_sort = imgui.input_int("Sort Order", current_sort) - if changed: - gui_element.sort = new_sort - if hasattr(gui_element, 'setBin'): - gui_element.setBin('fixed', new_sort) - - # 字体设置(适用于文本类型的GUI元素) - if gui_type in ["button", "label", "entry"]: - imgui.text("字体设置") - - # 字体选择 - current_font = getattr(gui_element, 'font_path', '') - if imgui.button(f"字体: {Path(current_font).name if current_font else '默认'}##font_select"): - self.show_font_selector( - gui_element, - 'font_path', - current_font, - lambda font_path: self._apply_gui_font(gui_element, font_path) - ) - - # 字体大小 - current_size = getattr(gui_element, 'font_size', 12) - changed, new_size = imgui.slider_float("字体大小", current_size, 8.0, 72.0) - if changed: - gui_element.font_size = new_size - self._apply_gui_font_size(gui_element, new_size) - - # 字体样式 - imgui.text("字体样式") - is_bold = getattr(gui_element, 'font_bold', False) - changed, new_bold = imgui.checkbox("粗体", is_bold) - if changed: - gui_element.font_bold = new_bold - self._apply_gui_font_style(gui_element) - - imgui.same_line() - is_italic = getattr(gui_element, 'font_italic', False) - changed, new_italic = imgui.checkbox("斜体", is_italic) - if changed: - gui_element.font_italic = new_italic - self._apply_gui_font_style(gui_element) - - def _apply_gui_font(self, gui_element, font_path): - """应用GUI元素的字体""" - try: - if hasattr(gui_element, 'setFont') and font_path: - gui_element.setFont(font_path) - gui_element.font_path = font_path - except Exception as e: - print(f"应用GUI字体失败: {e}") - - def _apply_gui_font_size(self, gui_element, font_size): - """应用GUI元素的字体大小""" - try: - if hasattr(gui_element, 'setFontSize'): - gui_element.setFontSize(font_size) - gui_element.font_size = font_size - except Exception as e: - print(f"应用GUI字体大小失败: {e}") - - def _apply_gui_font_style(self, gui_element): - """应用GUI元素的字体样式""" - try: - if hasattr(gui_element, 'setFontStyle'): - style = 0 - if getattr(gui_element, 'font_bold', False): - style |= 1 # 粗体 - if getattr(gui_element, 'font_italic', False): - style |= 2 # 斜体 - gui_element.setFontStyle(style) - except Exception as e: - print(f"应用GUI字体样式失败: {e}") - - # 特定类型的属性 - if gui_type == "button": - if imgui.collapsing_header("按钮属性"): - # 按钮状态 - is_pressed = getattr(gui_element, 'pressed', False) - changed, new_pressed = imgui.checkbox("按下状态", is_pressed) - if changed: - gui_element.pressed = new_pressed - - # 按钮回调 - callback_name = getattr(gui_element, 'callback_name', '') - changed, new_callback = imgui.input_text("回调函数", callback_name, 64) - if changed: - gui_element.callback_name = new_callback - - elif gui_type == "entry": - if imgui.collapsing_header("输入框属性"): - # 输入框内容 - entry_text = getattr(gui_element, 'entry_text', '') - changed, new_text = imgui.input_text("输入内容", entry_text, 256) - if changed: - gui_element.entry_text = new_text - if hasattr(gui_element, 'set'): - gui_element.set(new_text) - - # 最大长度 - max_length = getattr(gui_element, 'max_length', 256) - changed, new_max = imgui.input_int("最大长度", max_length) - if changed: - gui_element.max_length = max(max_length, 1) - - # 密码模式 - is_password = getattr(gui_element, 'is_password', False) - changed, new_password = imgui.checkbox("密码模式", is_password) - if changed: - gui_element.is_password = new_password - if hasattr(gui_element, 'obscure'): - gui_element.obscure(new_password) - - elif gui_type in ["2d_image", "3d_image"]: - if imgui.collapsing_header("图像属性"): - # 图像路径 - image_path = getattr(gui_element, 'image_path', '') - changed, new_path = imgui.input_text("图像路径", image_path, 256) - if changed and hasattr(self, 'gui_manager'): - gui_element.image_path = new_path - # TODO: 重新加载图像 - - # 图像缩放模式 - scale_mode = getattr(gui_element, 'scale_mode', 'stretch') - if imgui.begin_combo("缩放模式", scale_mode): - if imgui.selectable("拉伸##stretch"): - gui_element.scale_mode = 'stretch' - if imgui.selectable("适应##fit"): - gui_element.scale_mode = 'fit' - if imgui.selectable("填充##fill"): - gui_element.scale_mode = 'fill' - imgui.end_combo() - - def _draw_light_properties(self, node): - """绘制光源属性""" - imgui.text("光源属性") - - # 光源颜色 - if hasattr(node, 'getColor'): - try: - color = node.getColor() - # 确保颜色是有效的 - if not color or len(color) < 3: - color = (1.0, 1.0, 1.0, 1.0) # 默认白色 - except: - color = (1.0, 1.0, 1.0, 1.0) # 默认白色 - - changed, new_r = imgui.drag_float("颜色 R", color[0], 0.01, 0.0, 1.0) - if changed: node.setColor(new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0) - - changed, new_g = imgui.drag_float("颜色 G", color[1], 0.01, 0.0, 1.0) - if changed: node.setColor(color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0) - - changed, new_b = imgui.drag_float("颜色 B", color[2], 0.01, 0.0, 1.0) - if changed: node.setColor(color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0) - - # 光源强度 - imgui.text("光源强度: (暂不支持编辑)") - - def _draw_model_properties(self, node): - """绘制模型属性""" - # 获取模型信息 - model_path = node.getTag("model_path") if node.hasTag("model_path") else "未知" - - imgui.text("模型路径:") - imgui.same_line() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_path) - - # 模型基本信息 - imgui.text("模型名称:") - imgui.same_line() - model_name = node.getName() or "未命名模型" - imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_name) - - # 模型位置信息 - imgui.text("位置:") - imgui.same_line() - pos = node.getPos() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{pos.x:.2f} Y:{pos.y:.2f} Z:{pos.z:.2f}") - - # 模型缩放信息 - imgui.text("缩放:") - imgui.same_line() - scale = node.getScale() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{scale.x:.2f} Y:{scale.y:.2f} Z:{scale.z:.2f}") - - # 模型旋转信息 - imgui.text("旋转:") - imgui.same_line() - hpr = node.getHpr() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"H:{hpr.x:.1f}° P:{hpr.y:.1f}° R:{hpr.z:.1f}°") - - def _draw_animation_properties(self, node): - """绘制动画控制属性面板(优化版本,使用缓存避免重复计算)""" - # 检查是否已经缓存了动画信息 - cached_anim_info = node.getPythonTag("cached_anim_info") - cached_processed_names = node.getPythonTag("cached_processed_names") - - # 只有在没有缓存时才进行完整的动画检测和处理 - if cached_anim_info is None or cached_processed_names is None: - # 获取Actor - actor = self._getActor(node) - if not actor: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画") - return - - # 获取和分析动画名称 - anim_names = actor.getAnimNames() - processed_names = self._processAnimationNames(node, anim_names) - - if not processed_names: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列") - # 缓存空结果 - node.setPythonTag("cached_processed_names", []) - node.setPythonTag("cached_anim_info", "无动画") - return - - # 计算并缓存动画信息 - format_info = self._getModelFormat(node) - animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info) - info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}" - if animation_info: - info_text += f" | {animation_info}" - - # 缓存结果 - node.setPythonTag("cached_anim_info", info_text) - node.setPythonTag("cached_processed_names", processed_names) - - else: - # 使用缓存的数据 - info_text = cached_anim_info - processed_names = cached_processed_names - - # 如果缓存的空结果,直接返回 - if not processed_names: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列") - return - - # 显示动画信息(使用缓存的数据) - imgui.text("信息:") - imgui.same_line() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), info_text) - - imgui.spacing() - - # 动画选择下拉框 - imgui.text("动画名称:") - imgui.same_line() - - # 获取当前选中的动画 - current_anim = node.getPythonTag("selected_animation") - if current_anim is None: - current_anim = processed_names[0][1] if processed_names else "" - node.setPythonTag("selected_animation", current_anim) - - # 查找当前动画的索引 - current_index = 0 - for i, (display_name, original_name) in enumerate(processed_names): - if original_name == current_anim: - current_index = i - break - - # 创建下拉框选项 - animation_options = [display_name for display_name, _ in processed_names] - changed, new_index = imgui.combo("##animation_combo", current_index, animation_options) - - if changed and new_index < len(processed_names): - selected_display, selected_original = processed_names[new_index] - node.setPythonTag("selected_animation", selected_original) - print(f"选择动画: {selected_display} (原始名称: {selected_original})") - - imgui.spacing() - - # 控制按钮组 - imgui.text("控制:") - - # 播放按钮 - if imgui.button("播放##play_animation"): - self._playAnimation(node) - imgui.same_line() - - # 暂停按钮 - if imgui.button("暂停##pause_animation"): - self._pauseAnimation(node) - imgui.same_line() - - # 停止按钮 - if imgui.button("停止##stop_animation"): - self._stopAnimation(node) - imgui.same_line() - - # 循环按钮 - if imgui.button("循环##loop_animation"): - self._loopAnimation(node) - - imgui.spacing() - - # 播放速度控制 - imgui.text("播放速度:") - imgui.same_line() - - # 获取当前速度 - current_speed = node.getPythonTag("anim_speed") - if current_speed is None: - current_speed = 1.0 - node.setPythonTag("anim_speed", current_speed) - - # 速度滑块 - changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f") - if changed: - node.setPythonTag("anim_speed", new_speed) - self._setAnimationSpeed(node, new_speed) - - imgui.same_line() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速") - - def _playAnimation(self, origin_model): - """播放动画""" - actor = self._getActor(origin_model) - if not actor: - return + def _draw_status_badges(self, *args, **kwargs): + return self.editor_panels._draw_status_badges(*args, **kwargs) - # 保存原始世界坐标 - original_world_pos = origin_model.getPos(self.render) - original_world_hpr = origin_model.getHpr(self.render) - original_world_scale = origin_model.getScale(self.render) - - # 设置Actor位置和姿态 - actor.setPos(origin_model.getPos()) - actor.setHpr(origin_model.getHpr()) - actor.setScale(origin_model.getScale()) + def _draw_transform_properties(self, *args, **kwargs): + return self.editor_panels._draw_transform_properties(*args, **kwargs) - # 隐藏原始模型,显示Actor - origin_model.hide() - actor.show() + def _draw_gui_properties(self, *args, **kwargs): + return self.editor_panels._draw_gui_properties(*args, **kwargs) - # 创建任务来维持世界坐标不变 - def maintainWorldPosition(task): - try: - if not actor.isEmpty(): - actor.setPos(self.render, original_world_pos) - actor.setHpr(self.render, original_world_hpr) - actor.setScale(self.render, original_world_scale) - return task.cont - else: - return task.done - except: - return task.done + def _draw_light_properties(self, *args, **kwargs): + return self.editor_panels._draw_light_properties(*args, **kwargs) - # 添加维持位置的任务 - taskMgr.add(maintainWorldPosition, f"maintain_anim_pos_{id(actor)}") + def _draw_model_properties(self, *args, **kwargs): + return self.editor_panels._draw_model_properties(*args, **kwargs) - # 获取当前选中的动画 - current_anim = origin_model.getPythonTag("selected_animation") - if current_anim: - actor.play(current_anim) - print(f"『动画播放』:{current_anim}") - else: - # 兜底:使用第一个可用动画 - anim_names = actor.getAnimNames() - if anim_names: - actor.play(anim_names[0]) - print(f"『动画播放』:{anim_names[0]}") + def _draw_animation_properties(self, *args, **kwargs): + return self.editor_panels._draw_animation_properties(*args, **kwargs) - def _pauseAnimation(self, origin_model): - """暂停动画""" - actor = self._getActor(origin_model) - if not actor: - return - - # 设置Actor位置和姿态 - actor.setPos(origin_model.getPos()) - actor.setHpr(origin_model.getHpr()) - actor.setScale(origin_model.getScale()) + def _draw_collision_properties(self, *args, **kwargs): + return self.editor_panels._draw_collision_properties(*args, **kwargs) - # 隐藏原始模型,显示Actor - origin_model.hide() - actor.show() - - # 停止动画(保持当前姿势) - actor.stop() - print("『动画』暂停") + def _draw_shape_specific_parameters(self, *args, **kwargs): + return self.editor_panels._draw_shape_specific_parameters(*args, **kwargs) - def _stopAnimation(self, origin_model): - """停止动画""" - actor = self._getActor(origin_model) - if not actor: - return - - # 停止动画 - actor.stop() + def _draw_sphere_parameters(self, *args, **kwargs): + return self.editor_panels._draw_sphere_parameters(*args, **kwargs) - # 获取当前选中的动画 - current_anim = origin_model.getPythonTag("selected_animation") - if current_anim and actor.getAnimControl(current_anim): - actor.getAnimControl(current_anim).pose(0) - - # 隐藏Actor,显示原始模型 - actor.hide() - origin_model.show() - - # 移除维持位置的任务 - taskMgr.remove(f"maintain_anim_pos_{id(actor)}") - - print("『动画』停止切换至原始模型") + def _draw_box_parameters(self, *args, **kwargs): + return self.editor_panels._draw_box_parameters(*args, **kwargs) - def _loopAnimation(self, origin_model): - """循环播放动画""" - actor = self._getActor(origin_model) - if not actor: - return + def _draw_capsule_parameters(self, *args, **kwargs): + return self.editor_panels._draw_capsule_parameters(*args, **kwargs) - # 保存原始世界坐标 - original_world_pos = origin_model.getPos(self.render) - original_world_hpr = origin_model.getHpr(self.render) - original_world_scale = origin_model.getScale(self.render) - - # 设置Actor位置和姿态 - actor.setPos(origin_model.getPos()) - actor.setHpr(origin_model.getHpr()) - actor.setScale(origin_model.getScale()) + def _draw_plane_parameters(self, *args, **kwargs): + return self.editor_panels._draw_plane_parameters(*args, **kwargs) - # 隐藏原始模型,显示Actor - origin_model.hide() - actor.show() + def _draw_property_actions(self, *args, **kwargs): + return self.editor_panels._draw_property_actions(*args, **kwargs) - # 创建任务来维持世界坐标不变 - def maintainWorldPosition(task): - try: - if not actor.isEmpty(): - actor.setPos(self.render, original_world_pos) - actor.setHpr(self.render, original_world_hpr) - actor.setScale(self.render, original_world_scale) - return task.cont - else: - return task.done - except: - return task.done + def _draw_appearance_properties(self, *args, **kwargs): + return self.editor_panels._draw_appearance_properties(*args, **kwargs) - # 添加维持位置的任务 - taskMgr.add(maintainWorldPosition, f"maintain_anim_pos_{id(actor)}") + def _draw_material_properties(self, *args, **kwargs): + return self.editor_panels._draw_material_properties(*args, **kwargs) - # 获取当前选中的动画 - current_anim = origin_model.getPythonTag("selected_animation") - if current_anim: - actor.loop(current_anim) - print(f"[动画] 循环: {current_anim}") - else: - # 兜底:使用第一个可用动画 - anim_names = actor.getAnimNames() - if anim_names: - actor.loop(anim_names[0]) - print(f"[动画] 循环: {anim_names[0]}") + def _draw_shading_model_panel(self, *args, **kwargs): + return self.editor_panels._draw_shading_model_panel(*args, **kwargs) - def _setAnimationSpeed(self, origin_model, speed): - """设置动画播放速度""" - actor = self._getActor(origin_model) - if not actor: - return - - # 获取当前选中的动画 - current_anim = origin_model.getPythonTag("selected_animation") - if current_anim: - actor.setPlayRate(speed, current_anim) - print(f"[动画] 速度设为: {speed} ({current_anim})") - else: - # 兜底:尝试所有动画 - anim_names = actor.getAnimNames() - for anim_name in anim_names: - actor.setPlayRate(speed, anim_name) - print(f"[动画] 速度设为: {speed} (所有动画)") - - def _clear_animation_cache(self, node): - """清除节点的动画缓存,当模型发生变化时调用""" - node.setPythonTag("cached_anim_info", None) - node.setPythonTag("cached_processed_names", None) - node.setPythonTag("animation", None) # 同时清除动画检测结果 - - # 如果Actor在缓存中,也需要清理 - if node in self._actor_cache: - actor = self._actor_cache[node] - try: - # 清理相关任务 - taskMgr.remove(f"maintain_anim_pos_{id(actor)}") - # 清理Actor - if not actor.isEmpty(): - actor.cleanup() - actor.removeNode() - except Exception as e: - print(f"清理Actor缓存失败: {e}") - finally: - del self._actor_cache[node] - print(f"[缓存清理] 清除节点 {node.getName()} 的动画缓存") - - def _draw_collision_properties(self, node): - """绘制碰撞检测属性""" - if not node or node.isEmpty(): - return - - try: - # 检查节点是否已有碰撞 - has_collision = self._has_collision(node) - - # 碰撞状态徽章 - imgui.text("状态:") - imgui.same_line() - if has_collision: - imgui.text_colored((0.0, 0.8, 0.0, 1.0), "🟢 已启用") - else: - imgui.text_colored((0.8, 0.0, 0.0, 1.0), "🔴 未启用") - - imgui.separator() - - # 碰撞形状选择 - imgui.text("碰撞形状:") - imgui.same_line() - - # 碰撞形状选项 - collision_shapes = ["球形 (Sphere)", "盒型 (Box)", "胶囊体 (Capsule)", "平面 (Plane)", "自动选择 (Auto)"] - - # 获取当前形状 - current_shape = self._get_current_collision_shape(node) if has_collision else "球形 (Sphere)" - - # 形状选择下拉框 - current_index = collision_shapes.index(current_shape) if current_shape in collision_shapes else 0 - changed, selected_index = imgui.combo("##collision_shape", current_index, collision_shapes) - if changed: - # 始终更新选择的形状 - selected_shape = collision_shapes[selected_index] - self._selected_collision_shape = selected_shape - # 如果已经有碰撞体,询问用户是否要重新创建 - if has_collision: - print(f"形状已更改为 {selected_shape},点击'移除碰撞'后'添加碰撞'来应用新形状") - - imgui.separator() - - # 位置偏移控件 - imgui.text("位置偏移:") - - # 获取当前位置偏移 - pos_offset = self._get_collision_position_offset(node) - - # X位置 - changed, new_x = imgui.drag_float("X##collision_pos_x", pos_offset[0], 0.1, -100.0, 100.0, "%.2f") - if changed and has_collision: - self._update_collision_position(node, 'x', new_x) - - # Y位置 - changed, new_y = imgui.drag_float("Y##collision_pos_y", pos_offset[1], 0.1, -100.0, 100.0, "%.2f") - if changed and has_collision: - self._update_collision_position(node, 'y', new_y) - - # Z位置 - changed, new_z = imgui.drag_float("Z##collision_pos_z", pos_offset[2], 0.1, -100.0, 100.0, "%.2f") - if changed and has_collision: - self._update_collision_position(node, 'z', new_z) - - # 形状特定参数(始终显示,但根据状态启用/禁用) - shape_type = self._get_current_collision_shape_type(node) - self._draw_shape_specific_parameters(node, shape_type, has_collision) - - imgui.separator() - - # 操作按钮 - if has_collision: - # 显示/隐藏碰撞体按钮 - is_visible = self._is_collision_visible(node) - visibility_text = "隐藏碰撞体" if is_visible else "显示碰撞体" - if imgui.button(visibility_text): - self._toggle_collision_visibility(node) - - imgui.same_line() - - # 移除碰撞按钮 - if imgui.button("移除碰撞"): - self._remove_collision_from_node(node) - else: - # 添加碰撞按钮 - if imgui.button("添加碰撞"): - self._add_collision_to_node(node) - - imgui.separator() - - # 碰撞检测触发模式 - imgui.text("触发模式:") - - # 自动检测开关 - auto_enabled = self.collision_manager.model_collision_enabled if hasattr(self, 'collision_manager') else False - changed, new_auto = imgui.checkbox("自动检测", auto_enabled) - if changed and hasattr(self, 'collision_manager'): - self.collision_manager.enableModelCollisionDetection(new_auto, 0.1, 0.5) - - imgui.same_line() - - # 手动检测按钮 - if imgui.button("立即检测"): - if hasattr(self, 'collision_manager'): - self._manual_collision_detection() - - except Exception as e: - print(f"绘制碰撞属性失败: {e}") - import traceback - traceback.print_exc() - - def _has_collision(self, node): - """检查节点是否有碰撞体""" - try: - if not node or node.isEmpty(): - return False - - # 检查是否有碰撞节点 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - return True - - return False - except Exception as e: - print(f"检查碰撞状态失败: {e}") - return False - - def _get_current_collision_shape(self, node): - """获取当前碰撞形状""" - try: - if not self._has_collision(node): - return "球形 (Sphere)" - - # 查找碰撞节点并判断形状 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - # 根据碰撞节点名称判断形状 - if 'sphere' in name.lower(): - return "球形 (Sphere)" - elif 'box' in name.lower(): - return "盒型 (Box)" - elif 'capsule' in name.lower(): - return "胶囊体 (Capsule)" - elif 'plane' in name.lower(): - return "平面 (Plane)" - - return "球形 (Sphere)" # 默认 - except Exception as e: - print(f"获取碰撞形状失败: {e}") - return "球形 (Sphere)" - - def _get_current_collision_shape_type(self, node): - """获取当前碰撞形状类型(内部标识)""" - try: - shape_name = self._get_current_collision_shape(node) - if "Sphere" in shape_name: - return "sphere" - elif "Box" in shape_name: - return "box" - elif "Capsule" in shape_name: - return "capsule" - elif "Plane" in shape_name: - return "plane" - else: - return "sphere" - except Exception as e: - print(f"获取碰撞形状类型失败: {e}") - return "sphere" - - def _get_collision_position_offset(self, node): - """获取碰撞体位置偏移""" - try: - if not self._has_collision(node): - return (0.0, 0.0, 0.0) - - # 查找碰撞节点并获取位置 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - pos = child.getPos() - return (pos.x, pos.y, pos.z) - - return (0.0, 0.0, 0.0) - except Exception as e: - print(f"获取碰撞位置失败: {e}") - return (0.0, 0.0, 0.0) - - def _is_collision_visible(self, node): - """检查碰撞体是否可见""" - try: - if not self._has_collision(node): - return False - - # 查找碰撞节点并检查可见性 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - return child.isHidden() == False - - return False - except Exception as e: - print(f"检查碰撞可见性失败: {e}") - return False - - def _add_collision_to_node(self, node): - """为节点添加碰撞体""" - try: - if not node or node.isEmpty(): - print("无效的节点") - return - - if self._has_collision(node): - print("节点已有碰撞体") - return - - # 获取选择的形状 - shape_name = getattr(self, '_selected_collision_shape', '球形 (Sphere)') - - if hasattr(self, 'collision_manager'): - # 使用碰撞管理器添加碰撞体 - shape_type = self._get_shape_type_from_name(shape_name) - collision_node = self.collision_manager.setupAdvancedCollision( - node, - shape_type=shape_type, - mask_type='MODEL_COLLISION' - ) - - if collision_node: - print(f"成功为节点 {node.getName()} 添加 {shape_name} 碰撞体") - else: - print(f"添加碰撞体失败") - else: - print("碰撞管理器未初始化") - - except Exception as e: - print(f"添加碰撞体失败: {e}") - import traceback - traceback.print_exc() - - def _remove_collision_from_node(self, node): - """从节点移除碰撞体""" - try: - if not node or node.isEmpty(): - print("无效的节点") - return - - if not self._has_collision(node): - print("节点没有碰撞体") - return - - # 查找并移除碰撞节点 - children_to_remove = [] - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - children_to_remove.append(child) - - # 移除找到的碰撞节点 - for child in children_to_remove: - child.removeNode() - - if children_to_remove: - print(f"成功移除节点 {node.getName()} 的碰撞体") - else: - print(f"未找到碰撞体") - - except Exception as e: - print(f"移除碰撞体失败: {e}") - import traceback - traceback.print_exc() - - def _toggle_collision_visibility(self, node): - """切换碰撞体可见性""" - try: - if not node or node.isEmpty(): - return - - # 查找碰撞节点并切换可见性 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - if child.isHidden(): - child.show() - else: - child.hide() - break - - except Exception as e: - print(f"切换碰撞可见性失败: {e}") - - def _update_collision_position(self, node, axis, value): - """更新碰撞体位置""" - try: - if not node or node.isEmpty(): - return - - # 查找碰撞节点并更新位置 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - current_pos = child.getPos() - if axis == 'x': - child.setPos(value, current_pos.y, current_pos.z) - elif axis == 'y': - child.setPos(current_pos.x, value, current_pos.z) - elif axis == 'z': - child.setPos(current_pos.x, current_pos.y, value) - break - - except Exception as e: - print(f"更新碰撞位置失败: {e}") - - def _get_shape_type_from_name(self, shape_name): - """从形状名称获取形状类型""" - if "Sphere" in shape_name: - return "sphere" - elif "Box" in shape_name: - return "box" - elif "Capsule" in shape_name: - return "capsule" - elif "Plane" in shape_name: - return "plane" - else: - return "sphere" - - def _draw_shape_specific_parameters(self, node, shape_type, has_collision=True): - """绘制形状特定参数""" - try: - if shape_type == "sphere": - self._draw_sphere_parameters(node, has_collision) - elif shape_type == "box": - self._draw_box_parameters(node, has_collision) - elif shape_type == "capsule": - self._draw_capsule_parameters(node, has_collision) - elif shape_type == "plane": - self._draw_plane_parameters(node, has_collision) - except Exception as e: - print(f"绘制形状参数失败: {e}") - - def _draw_sphere_parameters(self, node, has_collision=True): - """绘制球形参数""" - try: - imgui.text("球形参数:") - imgui.same_line() - - # 获取当前半径 - radius = self._get_sphere_radius(node) - - # 半径调整 - changed, new_radius = imgui.drag_float("半径##sphere_radius", radius, 0.1, 0.1, 100.0, "%.2f") - if changed and has_collision: - self._update_sphere_radius(node, new_radius) - - except Exception as e: - print(f"绘制球形参数失败: {e}") - - def _draw_box_parameters(self, node, has_collision=True): - """绘制盒型参数""" - try: - imgui.text("盒型参数:") - - # 获取当前尺寸 - size = self._get_box_size(node) - - # 尺寸调整 - changed, new_x = imgui.drag_float("长度##box_length", size[0], 0.1, 0.1, 100.0, "%.2f") - if changed and has_collision: - self._update_box_size(node, 'x', new_x) - - changed, new_y = imgui.drag_float("宽度##box_width", size[1], 0.1, 0.1, 100.0, "%.2f") - if changed and has_collision: - self._update_box_size(node, 'y', new_y) - - changed, new_z = imgui.drag_float("高度##box_height", size[2], 0.1, 0.1, 100.0, "%.2f") - if changed and has_collision: - self._update_box_size(node, 'z', new_z) - - except Exception as e: - print(f"绘制盒型参数失败: {e}") - - def _draw_capsule_parameters(self, node, has_collision=True): - """绘制胶囊体参数""" - try: - imgui.text("胶囊体参数:") - - # 获取当前参数 - radius = self._get_capsule_radius(node) - height = self._get_capsule_height(node) - - # 半径调整 - changed, new_radius = imgui.drag_float("半径##capsule_radius", radius, 0.1, 0.1, 100.0, "%.2f") - if changed and has_collision: - self._update_capsule_radius(node, new_radius) - - # 高度调整 - changed, new_height = imgui.drag_float("高度##capsule_height", height, 0.1, 0.1, 100.0, "%.2f") - if changed and has_collision: - self._update_capsule_height(node, new_height) - - except Exception as e: - print(f"绘制胶囊体参数失败: {e}") - - def _draw_plane_parameters(self, node, has_collision=True): - """绘制平面参数""" - try: - imgui.text("平面参数:") - - # 获取当前法向量 - normal = self._get_plane_normal(node) - - # 法向量调整 - changed, new_x = imgui.drag_float("法向量 X##plane_normal_x", normal[0], 0.1, -1.0, 1.0, "%.2f") - if changed and has_collision: - self._update_plane_normal(node, 'x', new_x) - - changed, new_y = imgui.drag_float("法向量 Y##plane_normal_y", normal[1], 0.1, -1.0, 1.0, "%.2f") - if changed and has_collision: - self._update_plane_normal(node, 'y', new_y) - - changed, new_z = imgui.drag_float("法向量 Z##plane_normal_z", normal[2], 0.1, -1.0, 1.0, "%.2f") - if changed and has_collision: - self._update_plane_normal(node, 'z', new_z) - - except Exception as e: - print(f"绘制平面参数失败: {e}") - - def _get_sphere_radius(self, node): - """获取球形半径""" - try: - # 从碰撞节点获取半径信息 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: - solid = child.node().getSolid(0) - from panda3d.core import CollisionSphere - if isinstance(solid, CollisionSphere): - return solid.getRadius() - return 1.0 - except Exception as e: - print(f"获取球形半径失败: {e}") - return 1.0 - - def _update_sphere_radius(self, node, radius): - """更新球形半径""" - try: - # 重新创建碰撞体来更新参数 - if hasattr(self, 'collision_manager'): - # 先移除旧的碰撞体 - self._remove_collision_from_node(node) - # 重新创建带有新参数的碰撞体 - self.collision_manager.setupAdvancedCollision( - node, - shape_type='sphere', - mask_type='MODEL_COLLISION', - radius=radius - ) - print(f"更新球形半径为: {radius}") - except Exception as e: - print(f"更新球形半径失败: {e}") - - def _get_box_size(self, node): - """获取盒型尺寸""" - try: - # 从碰撞节点获取尺寸信息 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - # 尝试从碰撞体获取尺寸 - if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: - solid = child.node().getSolid(0) - from panda3d.core import CollisionBox - if isinstance(solid, CollisionBox): - min_p = solid.getMin() - max_p = solid.getMax() - return ( - max_p.x - min_p.x, - max_p.y - min_p.y, - max_p.z - min_p.z - ) - return (1.0, 1.0, 1.0) - except Exception as e: - print(f"获取盒型尺寸失败: {e}") - return (1.0, 1.0, 1.0) - - def _update_box_size(self, node, axis, value): - """更新盒型尺寸""" - try: - # 获取当前尺寸 - current_size = self._get_box_size(node) - new_size = list(current_size) - - # 更新指定轴的尺寸 - if axis == 'x': - new_size[0] = value - elif axis == 'y': - new_size[1] = value - elif axis == 'z': - new_size[2] = value - - # 重新创建碰撞体 - if hasattr(self, 'collision_manager'): - self._remove_collision_from_node(node) - self.collision_manager.setupAdvancedCollision( - node, - shape_type='box', - mask_type='MODEL_COLLISION', - width=new_size[0], - length=new_size[1], - height=new_size[2] - ) - print(f"更新盒型尺寸: {new_size}") - except Exception as e: - print(f"更新盒型尺寸失败: {e}") - - def _get_capsule_radius(self, node): - """获取胶囊体半径""" - try: - # 从碰撞节点获取半径信息 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: - solid = child.node().getSolid(0) - from panda3d.core import CollisionCapsule - if isinstance(solid, CollisionCapsule): - return solid.getRadius() - return 1.0 - except Exception as e: - print(f"获取胶囊体半径失败: {e}") - return 1.0 - - def _update_capsule_radius(self, node, radius): - """更新胶囊体半径""" - try: - # 获取当前高度 - height = self._get_capsule_height(node) - - # 重新创建碰撞体 - if hasattr(self, 'collision_manager'): - self._remove_collision_from_node(node) - self.collision_manager.setupAdvancedCollision( - node, - shape_type='capsule', - mask_type='MODEL_COLLISION', - radius=radius, - height=height - ) - print(f"更新胶囊体半径为: {radius}") - except Exception as e: - print(f"更新胶囊体半径失败: {e}") - - def _get_capsule_height(self, node): - """获取胶囊体高度""" - try: - # 从碰撞节点获取高度信息 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: - solid = child.node().getSolid(0) - from panda3d.core import CollisionCapsule - if isinstance(solid, CollisionCapsule): - point_a = solid.getPointA() - point_b = solid.getPointB() - return (point_b - point_a).length() + 2 * solid.getRadius() - return 2.0 - except Exception as e: - print(f"获取胶囊体高度失败: {e}") - return 2.0 - - def _update_capsule_height(self, node, height): - """更新胶囊体高度""" - try: - # 获取当前半径 - radius = self._get_capsule_radius(node) - - # 重新创建碰撞体 - if hasattr(self, 'collision_manager'): - self._remove_collision_from_node(node) - self.collision_manager.setupAdvancedCollision( - node, - shape_type='capsule', - mask_type='MODEL_COLLISION', - radius=radius, - height=height - ) - print(f"更新胶囊体高度为: {height}") - except Exception as e: - print(f"更新胶囊体高度失败: {e}") - - def _get_plane_normal(self, node): - """获取平面法向量""" - try: - # 从碰撞节点获取法向量信息 - for child in node.getChildren(): - if hasattr(child, 'getName') and child.getName(): - name = child.getName() - if 'collision' in name.lower() or 'Collision' in name: - if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: - solid = child.node().getSolid(0) - from panda3d.core import CollisionPlane - if isinstance(solid, CollisionPlane): - plane = solid.getPlane() - normal = plane.getNormal() - return (normal.x, normal.y, normal.z) - return (0.0, 0.0, 1.0) - except Exception as e: - print(f"获取平面法向量失败: {e}") - return (0.0, 0.0, 1.0) - - def _update_plane_normal(self, node, axis, value): - """更新平面法向量""" - try: - # 获取当前法向量 - current_normal = self._get_plane_normal(node) - new_normal = list(current_normal) - - # 更新指定轴的值 - if axis == 'x': - new_normal[0] = value - elif axis == 'y': - new_normal[1] = value - elif axis == 'z': - new_normal[2] = value - - # 标准化法向量 - from panda3d.core import Vec3 - normal_vec = Vec3(*new_normal) - normal_vec.normalize() - - # 重新创建碰撞体 - if hasattr(self, 'collision_manager'): - self._remove_collision_from_node(node) - self.collision_manager.setupAdvancedCollision( - node, - shape_type='plane', - mask_type='MODEL_COLLISION', - normal=normal_vec - ) - print(f"更新平面法向量为: ({normal_vec.x:.2f}, {normal_vec.y:.2f}, {normal_vec.z:.2f})") - except Exception as e: - print(f"更新平面法向量失败: {e}") - - def _manual_collision_detection(self): - """手动执行碰撞检测""" - try: - if hasattr(self, 'collision_manager'): - results = self.collision_manager.detectModelCollisions(log_results=True) - if results: - print(f"手动碰撞检测完成,发现 {len(results)} 个碰撞") - else: - print("手动碰撞检测完成,未发现碰撞") - except Exception as e: - print(f"手动碰撞检测失败: {e}") - def _draw_property_actions(self, node): - """绘制属性操作按钮""" - # 重置变换 - if imgui.button("重置变换"): - node.setPos(0, 0, 0) - node.setHpr(0, 0, 0) - node.setScale(1, 1, 1) - - imgui.same_line() - - # 切换可见性 - is_visible = not node.is_hidden() - visibility_text = "隐藏" if is_visible else "显示" - if imgui.button(visibility_text): - if is_visible: - node.hide() - else: - node.show() - - imgui.same_line() - - # 聚焦到对象 - if imgui.button("聚焦"): - if hasattr(self, 'selection') and self.selection: - self.selection.focusCameraOnSelectedNodeAdvanced() - - # 删除对象 - imgui.same_line() - if imgui.button("删除"): - if hasattr(self, 'selection') and self.selection: - self.selection.deleteSelectedNode() - - def _update_node_name(self, node, new_name): - """更新节点名称""" - if new_name and new_name != node.getName(): - node.setName(new_name) - # 更新场景树显示 - if hasattr(self, 'scene_tree'): - self.scene_tree.refresh() - - def _draw_appearance_properties(self, node): - """绘制外观属性""" - # 颜色属性 - if hasattr(node, 'getColor'): - imgui.text("颜色") - try: - color = node.getColor() - # 确保颜色是有效的 - if not color or len(color) < 3: - color = (1.0, 1.0, 1.0, 1.0) # 默认白色 - except: - color = (1.0, 1.0, 1.0, 1.0) # 默认白色 - - # 颜色滑块 - changed, new_r = imgui.slider_float("R##color_r", color[0], 0.0, 1.0) - if changed: - new_color = (new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0) - node.setColor(new_color) - color = new_color - - changed, new_g = imgui.slider_float("G##color_g", color[1], 0.0, 1.0) - if changed: - new_color = (color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0) - node.setColor(new_color) - color = new_color - - changed, new_b = imgui.slider_float("B##color_b", color[2], 0.0, 1.0) - if changed: - new_color = (color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0) - node.setColor(new_color) - color = new_color - - # 只有当颜色有alpha通道时才显示alpha滑块 - if len(color) > 3: - changed, new_a = imgui.slider_float("A##color_a", color[3], 0.0, 1.0) - if changed: - new_color = (color[0], color[1], color[2], new_a) - node.setColor(new_color) - color = new_color - - # 颜色预览和选择器 - imgui.text("颜色预览") - color_with_alpha = (color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0) - if imgui.color_button("颜色预览##preview", color_with_alpha, 0, (100, 30)): - # 点击颜色按钮打开颜色选择器 - self.show_color_picker(node, 'color', color_with_alpha) - - imgui.same_line() - if imgui.button("选择颜色##color_picker_btn"): - self.show_color_picker(node, 'color', (color.x, color.y, color.z, color.w)) - - # 透明度 - if hasattr(node, 'setTransparency') and hasattr(node, 'getTransparency'): - imgui.text("透明度") - current_transparency = node.getTransparency() - # 将当前的透明度值转换为0.0-1.0范围用于显示 - display_transparency = 1.0 - current_transparency if current_transparency <= 1 else 0.0 - changed, new_transparency = imgui.slider_float("透明度", display_transparency, 0.0, 1.0) - if changed: - # 将0.0-1.0范围转换回Panda3D的透明度格式 - panda_transparency = int((1.0 - new_transparency) * 255) - node.setTransparency(panda_transparency) - - # 材质属性 - self._draw_material_properties(node) - - # 渲染状态 - imgui.text("渲染状态") - if imgui.button("应用材质"): - self._apply_material_to_node(node) - - imgui.same_line() - if imgui.button("重置材质"): - self._reset_material(node) - - def _draw_material_properties(self, node): - """绘制材质属性""" - materials = node.find_all_materials() - - if not materials: - imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质") - return - - for i, material in enumerate(materials): - material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" - - if imgui.collapsing_header(f"材质: {material_name}"): - # 材质基础颜色 - base_color = self._get_material_base_color(material) - if base_color: - imgui.text("基础颜色") - changed, new_r = imgui.slider_float(f"R##mat_r_{i}", base_color[0], 0.0, 1.0) - if changed: - self._update_material_base_color(material, 'r', new_r) - base_color = (new_r, base_color[1], base_color[2], base_color[3]) - - changed, new_g = imgui.slider_float(f"G##mat_g_{i}", base_color[1], 0.0, 1.0) - if changed: - self._update_material_base_color(material, 'g', new_g) - base_color = (base_color[0], new_g, base_color[2], base_color[3]) - - changed, new_b = imgui.slider_float(f"B##mat_b_{i}", base_color[2], 0.0, 1.0) - if changed: - self._update_material_base_color(material, 'b', new_b) - base_color = (base_color[0], base_color[1], new_b, base_color[3]) - - changed, new_a = imgui.slider_float(f"A##mat_a_{i}", base_color[3], 0.0, 1.0) - if changed: - self._update_material_base_color(material, 'a', new_a) - base_color = (base_color[0], base_color[1], base_color[2], new_a) - - # PBR属性 - if hasattr(material, 'roughness') and material.roughness is not None: - imgui.text("PBR属性") - try: - roughness_value = float(material.roughness) - changed, new_roughness = imgui.slider_float(f"粗糙度##rough_{i}", roughness_value, 0.0, 1.0) - if changed: - self._update_material_roughness(material, new_roughness) - except: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "粗糙度: 不可用") - - if hasattr(material, 'metallic') and material.metallic is not None: - try: - metallic_value = float(material.metallic) - changed, new_metallic = imgui.slider_float(f"金属性##metal_{i}", metallic_value, 0.0, 1.0) - if changed: - self._update_material_metallic(material, new_metallic) - except: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "金属性: 不可用") - - if hasattr(material, 'refractive_index') and material.refractive_index is not None: - try: - ior_value = float(material.refractive_index) - changed, new_ior = imgui.slider_float(f"折射率##ior_{i}", ior_value, 1.0, 3.0) - if changed: - self._update_material_ior(material, new_ior) - except: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "折射率: 不可用") - - # 材质预设 - imgui.text("材质预设") - presets = ["默认", "金属", "塑料", "玻璃", "木材", "混凝土"] - current_preset = 0 # 默认选择 - - if imgui.begin_combo(f"预设##preset_{i}", presets[current_preset]): - for j, preset_name in enumerate(presets): - if imgui.selectable(preset_name, j == current_preset): - self._apply_material_preset(material, preset_name) - imgui.end_combo() - - # 纹理信息 - imgui.text("纹理贴图") - if imgui.button(f"选择漫反射贴图##diffuse_{i}"): - self._select_texture_for_material(node, material, "diffuse") - - imgui.same_line() - if imgui.button(f"选择法线贴图##normal_{i}"): - self._select_texture_for_material(node, material, "normal") - - imgui.same_line() - if imgui.button(f"选择粗糙度贴图##roughness_{i}"): - self._select_texture_for_material(node, material, "roughness") - - if imgui.button(f"选择金属性贴图##metallic_{i}"): - self._select_texture_for_material(node, material, "metallic") - - imgui.same_line() - if imgui.button(f"选择自发光贴图##emission_{i}"): - self._select_texture_for_material(node, material, "emission") - - imgui.same_line() - if imgui.button(f"清除所有贴图##clear_{i}"): - self._clear_all_textures(node) - - # 着色模型选择 - self._draw_shading_model_panel(material, i) - - # 显示当前纹理信息 - self._display_current_textures(node, material) - - def _get_material_base_color(self, material): - """获取材质基础颜色""" - try: - if hasattr(material, 'base_color') and material.base_color is not None: - return (material.base_color.x, material.base_color.y, material.base_color.z, material.base_color.w) - elif hasattr(material, 'get_base_color'): - color = material.get_base_color() - return (color.x, color.y, color.z, color.w) - elif hasattr(material, 'getDiffuse'): - color = material.getDiffuse() - return (color.x, color.y, color.z, color.w if hasattr(color, 'w') else 1.0) - else: - return (1.0, 1.0, 1.0, 1.0) # 默认白色 - except: - return (1.0, 1.0, 1.0, 1.0) # 默认白色 - - def _update_material_base_color(self, material, component, value): - """更新材质基础颜色""" - try: - base_color = self._get_material_base_color(material) - new_color = list(base_color) - - if component == 'r': - new_color[0] = value - elif component == 'g': - new_color[1] = value - elif component == 'b': - new_color[2] = value - elif component == 'a': - new_color[3] = value - - new_color_tuple = tuple(new_color) - - if hasattr(material, 'set_base_color'): - from panda3d.core import Vec4 - material.set_base_color(Vec4(*new_color_tuple)) - elif hasattr(material, 'setDiffuse'): - from panda3d.core import Vec4 - material.setDiffuse(Vec4(*new_color_tuple)) - except Exception as e: - print(f"更新材质基础颜色失败: {e}") - - def _update_material_roughness(self, material, value): - """更新材质粗糙度""" - try: - if hasattr(material, 'set_roughness'): - material.set_roughness(value) - except Exception as e: - print(f"更新材质粗糙度失败: {e}") - - def _update_material_metallic(self, material, value): - """更新材质金属性""" - try: - if hasattr(material, 'set_metallic'): - material.set_metallic(value) - except Exception as e: - print(f"更新材质金属性失败: {e}") - - def _update_material_ior(self, material, value): - """更新材质折射率""" - try: - if hasattr(material, 'set_refractive_index'): - material.set_refractive_index(value) - except Exception as e: - print(f"更新材质折射率失败: {e}") - - def _apply_material_preset(self, material, preset_name): - """应用材质预设""" - try: - from panda3d.core import Vec4, Material - - presets = { - "默认": { - "base_color": Vec4(0.8, 0.8, 0.8, 1.0), - "roughness": 0.5, - "metallic": 0.0, - "ior": 1.5 - }, - "金属": { - "base_color": Vec4(0.7, 0.7, 0.8, 1.0), - "roughness": 0.2, - "metallic": 1.0, - "ior": 2.5 - }, - "塑料": { - "base_color": Vec4(0.9, 0.9, 0.9, 1.0), - "roughness": 0.8, - "metallic": 0.0, - "ior": 1.45 - }, - "玻璃": { - "base_color": Vec4(0.9, 0.9, 1.0, 0.2), - "roughness": 0.0, - "metallic": 0.0, - "ior": 1.5 - }, - "木材": { - "base_color": Vec4(0.6, 0.4, 0.2, 1.0), - "roughness": 0.9, - "metallic": 0.0, - "ior": 1.55 - }, - "混凝土": { - "base_color": Vec4(0.5, 0.5, 0.5, 1.0), - "roughness": 1.0, - "metallic": 0.0, - "ior": 1.5 - } - } - - if preset_name in presets: - preset = presets[preset_name] - - # 应用基础颜色 - if hasattr(material, 'set_base_color'): - material.set_base_color(preset["base_color"]) - elif hasattr(material, 'setDiffuse'): - material.setDiffuse(preset["base_color"]) - - # 应用PBR属性 - if hasattr(material, 'set_roughness'): - material.set_roughness(preset["roughness"]) - if hasattr(material, 'set_metallic'): - material.set_metallic(preset["metallic"]) - if hasattr(material, 'set_refractive_index'): - material.set_refractive_index(preset["ior"]) - - print(f"已应用材质预设: {preset_name}") - except Exception as e: - print(f"应用材质预设失败: {e}") - - def _apply_material_to_node(self, node): - """为节点应用材质""" - try: - from panda3d.core import Material, Vec4 - - # 检查是否已有材质 - materials = node.find_all_materials() - - if not materials: - # 创建新材质 - material = Material(f"default-material-{node.getName()}") - material.setBaseColor(Vec4(0.8, 0.8, 0.8, 1.0)) - material.setDiffuse(Vec4(0.8, 0.8, 0.8, 1.0)) - material.setAmbient(Vec4(0.4, 0.4, 0.4, 1.0)) - material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) - material.setShininess(10.0) - node.setMaterial(material, 1) - print(f"已为新节点创建默认材质") - else: - print(f"节点已有 {len(materials)} 个材质") - except Exception as e: - print(f"应用材质失败: {e}") - - def _reset_material(self, node): - """重置节点材质""" - try: - materials = node.find_all_materials() - - for material in materials: - # 重置为默认材质属性 - from panda3d.core import Vec4 - default_color = Vec4(0.8, 0.8, 0.8, 1.0) - - if hasattr(material, 'set_base_color'): - material.set_base_color(default_color) - elif hasattr(material, 'setDiffuse'): - material.setDiffuse(default_color) - - if hasattr(material, 'set_roughness'): - material.set_roughness(0.5) - if hasattr(material, 'set_metallic'): - material.set_metallic(0.0) - if hasattr(material, 'set_refractive_index'): - material.set_refractive_index(1.5) - - print(f"已重置材质") - except Exception as e: - print(f"重置材质失败: {e}") - - def _select_texture_for_material(self, node, material, texture_type): - """为材质选择纹理""" - try: - # 设置当前纹理对话框状态 - self._current_texture_dialog = { - 'node': node, - 'material': material, - 'texture_type': texture_type - } - - # 初始化路径 - if not hasattr(self, '_texture_dialog_path'): - self._texture_dialog_path = '/home/hello/EG/Resources' - - # 设置文件类型过滤 - self._texture_dialog_filter = "*.png" - - except Exception as e: - print(f"选择纹理失败: {e}") - - def _apply_texture_to_material(self, node, material, texture_type, texture_path): - """应用纹理到材质""" - try: - from panda3d.core import TextureStage - from direct.showbase import Loader - - # 加载纹理 - loader = Loader.Loader(self) - texture = loader.loadTexture(texture_path) - - if not texture: - print(f"无法加载纹理: {texture_path}") - return - - # 设置纹理属性 - texture.setMagfilter(texture.FTLinear) - texture.setMinfilter(texture.FTLinearMipmapLinear) - - # 纹理槽位映射 - texture_slots = { - "diffuse": 0, # p3d_Texture0 - "ior": 2, # p3d_Texture2 - "normal": 1, # p3d_Texture1 - "roughness": 3, # p3d_Texture3 - "parallax": 4, # p3d_Texture4 - "metallic": 5, # p3d_Texture5 - "emission": 6, # p3d_Texture6 - "ao": 7, # p3d_Texture7 - "alpha": 8, # p3d_Texture8 - "detail": 9, # p3d_Texture9 - "gloss": 10 # p3d_Texture10 - } - - slot = texture_slots.get(texture_type, 0) - - # 创建纹理阶段 - texture_stage = TextureStage(f"{texture_type}_map") - texture_stage.setSort(slot) - texture_stage.setMode(TextureStage.MModulate) - - # 应用纹理到节点 - node.setTexture(texture_stage, texture) - - print(f"已应用{texture_type}纹理: {texture_path}") - - except Exception as e: - print(f"应用纹理失败: {e}") - - def _clear_all_textures(self, node): - """清除节点所有纹理""" - try: - # 清除所有纹理阶段 - node.clearTexture() - node.clearTexture() - print("已清除所有纹理") - except Exception as e: - print(f"清除纹理失败: {e}") - - def _display_current_textures(self, node, material): - """显示当前纹理信息""" - try: - from panda3d.core import TextureStage - - # 获取所有纹理阶段 - texture_stages = node.findAllTextureStages() - - if not texture_stages: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "当前无纹理") - return - - imgui.text("当前纹理:") - for stage in texture_stages: - texture = node.getTexture(stage) - if texture: - texture_name = texture.getName() or "未命名" - stage_name = stage.getName() or "未命名" - imgui.text(f" {stage_name}: {texture_name}") - except Exception as e: - print(f"显示纹理信息失败: {e}") - - def _draw_shading_model_panel(self, material, material_index): - """绘制着色模型选择面板""" - try: - imgui.text("着色模型") - - # RenderPipeline支持的着色模型 - shading_models = ["默认", "自发光", "透明"] - current_model = 0 # 默认选择 - - # 安全地获取当前着色模型 - try: - if hasattr(material, 'emission') and material.emission is not None: - current_model = int(material.emission.x) - except: - current_model = 0 - - # 着色模型选择 - if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_model]): - for j, model_name in enumerate(shading_models): - if imgui.selectable(model_name, j == current_model): - self._update_shading_model(material, j) - imgui.end_combo() - - # 如果是透明着色模型,添加透明度控制 - if current_model == 3: # 透明着色模型 - imgui.text("透明度设置") - try: - if hasattr(material, 'shading_model_param0'): - current_opacity = material.shading_model_param0 - else: - current_opacity = 1.0 - - changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0) - if changed: - self._update_transparency(material, new_opacity) - except: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用") - - except Exception as e: - print(f"绘制着色模型面板失败: {e}") - - def _update_shading_model(self, material, model_index): - """更新着色模型""" - try: - from panda3d.core import Vec4 - - # 根据不同的着色模型设置相应的参数 - if model_index == 1: # 自发光着色模型 - print("设置自发光着色模型...") - if hasattr(material, 'set_emission'): - current_emission = material.emission or Vec4(0, 0, 0, 0) - new_emission = Vec4(1.0, current_emission.y, current_emission.z, current_emission.w) - material.set_emission(new_emission) - print("自发光着色模型设置完成") - - elif model_index == 3: # 透明着色模型 - print("设置透明着色模型...") - if hasattr(material, 'set_emission'): - current_emission = material.emission or Vec4(0, 0, 0, 0) - new_emission = Vec4(3.0, 0, 0, 0) # 3表示透明着色模型 - material.set_emission(new_emission) - - # 设置默认透明度 - if hasattr(material, 'shading_model_param0'): - material.shading_model_param0 = 0.8 # 默认80%不透明度 - - print("透明着色模型设置完成") - - else: # 默认着色模型 - print("设置默认着色模型...") - if hasattr(material, 'set_emission'): - current_emission = material.emission or Vec4(0, 0, 0, 0) - new_emission = Vec4(0.0, current_emission.y, current_emission.z, current_emission.w) - material.set_emission(new_emission) - print("默认着色模型设置完成") - - print(f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})") - - except Exception as e: - print(f"更新着色模型失败: {e}") - - def _update_transparency(self, material, opacity_value): - """更新透明度""" - try: - if hasattr(material, 'shading_model_param0'): - material.shading_model_param0 = opacity_value - print(f"透明度已更新: {opacity_value}") - except Exception as e: - print(f"更新透明度失败: {e}") - - def _draw_texture_file_dialog(self): - """绘制纹理文件选择对话框""" - if not hasattr(self, '_current_texture_dialog') or not self._current_texture_dialog: - return - - try: - dialog_data = self._current_texture_dialog - node = dialog_data['node'] - material = dialog_data['material'] - texture_type = dialog_data['texture_type'] - - # 设置对话框标志 - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - # 获取屏幕尺寸,居中显示对话框 - display_size = imgui.get_io().display_size - dialog_width = 600 - dialog_height = 400 - imgui.set_next_window_size((dialog_width, dialog_height)) - imgui.set_next_window_pos( - ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) - ) - - # 显示文件选择对话框 - opened, window_open = imgui.begin(f"选择{texture_type}纹理文件##texture_dialog", True, flags) - if not window_open: - self._current_texture_dialog = None - imgui.end() - return - - imgui.text(f"选择{texture_type}纹理文件") - imgui.separator() - - # 当前路径显示 - current_path = getattr(self, '_texture_dialog_path', '/home/hello/EG/Resources') - imgui.text(f"当前路径: {current_path}") - - imgui.separator() - - # 文件类型过滤 - imgui.text("支持的纹理格式:") - file_types = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.tga", "*.dds"] - - current_filter = getattr(self, '_texture_dialog_filter', "*.png") - if imgui.begin_combo("文件类型##texture_filter", current_filter): - for i, file_type in enumerate(file_types): - if imgui.selectable(file_type, i == file_types.index(current_filter)): - self._texture_dialog_filter = file_type - imgui.end_combo() - - imgui.separator() - - # 路径导航按钮 - if imgui.button("上级目录##up_dir"): - current_path = os.path.dirname(current_path) - self._texture_dialog_path = current_path - - imgui.same_line() - if imgui.button("主目录##home_dir"): - self._texture_dialog_path = '/home/hello/EG/Resources' - - imgui.same_line() - if imgui.button("当前目录##current_dir"): - self._texture_dialog_path = '/home/hello/EG/Resources' - - imgui.same_line() - if imgui.button("纹理目录##textures_dir"): - self._texture_dialog_path = '/home/hello/EG/Resources/textures' - - imgui.separator() - - # 文件列表 - if imgui.begin_child("file_list##texture_files", (580, 200)): - try: - # 列出目录和文件 - items = [] - if os.path.exists(current_path): - for item in os.listdir(current_path): - item_path = os.path.join(current_path, item) - if os.path.isdir(item_path): - items.append(('dir', item, item_path)) - elif any(item.lower().endswith(ext[1:]) for ext in file_types): - items.append(('file', item, item_path)) - - # 排序:目录在前,文件在后 - items.sort(key=lambda x: (x[0], x[1].lower())) - - for item_type, item_name, item_path in items: - if item_type == 'dir': - if imgui.selectable(f"📁 {item_name}##dir_{item_name}", False)[0]: - self._texture_dialog_path = item_path - else: - selected, _ = imgui.selectable(f"📄 {item_name}##file_{item_name}", False) - if selected: - # 应用选择的纹理 - self._apply_texture_to_material(node, material, texture_type, item_path) - # 关闭对话框 - self._current_texture_dialog = None - break - - except Exception as e: - imgui.text_colored((1.0, 0.5, 0.5, 1.0), f"读取目录失败: {e}") - - imgui.end_child() - - imgui.separator() - - # 路径输入框 - changed, new_path = imgui.input_text("文件路径##texture_path", current_path, 512) - if changed: - self._texture_dialog_path = new_path - - imgui.same_line() - if imgui.button("确认路径##confirm_path"): - if os.path.isfile(new_path): - # 检查文件扩展名 - file_ext = os.path.splitext(new_path)[1].lower() - if file_ext in [ext[1:] for ext in file_types]: - self._apply_texture_to_material(node, material, texture_type, new_path) - self._current_texture_dialog = None - else: - print(f"不支持的文件格式: {file_ext}") - else: - print("请选择有效的文件") - - imgui.separator() - - # 按钮 - if imgui.button("取消##cancel_texture"): - self._current_texture_dialog = None - - imgui.end() - - except Exception as e: - print(f"绘制纹理对话框失败: {e}") - # 确保在异常情况下也调用 imgui.end() - try: - imgui.end() - except: - pass + def _apply_gui_font(self, *args, **kwargs): + return self.property_helpers._apply_gui_font(*args, **kwargs) - def start_transform_monitoring(self, node): - """开始变换监控""" - if node and not node.isEmpty(): - self._monitored_node = node - self._transform_monitoring = True - self._transform_update_timer = 0 - - # 记录初始变换值 - self._update_last_transform_values() - - def stop_transform_monitoring(self): - """停止变换监控""" - self._transform_monitoring = False - self._monitored_node = None - self._last_transform_values = {} - - def _update_last_transform_values(self): - """更新最后记录的变换值""" - if self._monitored_node and not self._monitored_node.isEmpty(): - try: - pos = self._monitored_node.getPos() - hpr = self._monitored_node.getHpr() - scale = self._monitored_node.getScale() - - self._last_transform_values = { - 'pos': (pos.x, pos.y, pos.z), - 'hpr': (hpr.x, hpr.y, hpr.z), - 'scale': (scale.x, scale.y, scale.z) - } - except Exception as e: - print(f"更新变换值失败: {e}") - - def _check_transform_changes(self): - """检查变换变化""" - if not self._transform_monitoring or not self._monitored_node: - return - - try: - pos = self._monitored_node.getPos() - hpr = self._monitored_node.getHpr() - scale = self._monitored_node.getScale() - - current_values = { - 'pos': (pos.x, pos.y, pos.z), - 'hpr': (hpr.x, hpr.y, hpr.z), - 'scale': (scale.x, scale.y, scale.z) - } - - # 检查是否有变化 - if current_values != self._last_transform_values: - # 更新记录的值 - self._last_transform_values = current_values - # 触发属性面板更新(通过设置更新标志) - self.property_panel_update_timer = 0 - - except Exception as e: - print(f"检查变换变化失败: {e}") - - def update_transform_monitoring(self, dt): - """更新变换监控(在主循环中调用)""" - if not self._transform_monitoring: - return - - self._transform_update_timer += dt - if self._transform_update_timer >= self._transform_update_interval: - self._transform_update_timer = 0 - self._check_transform_changes() - - def show_color_picker(self, target_object, property_name, initial_color, callback=None): - """显示颜色选择器""" - self._color_picker_active = True - self._color_picker_target = (target_object, property_name) - self._color_picker_current_color = initial_color - self._color_picker_callback = callback - - def _draw_color_picker(self): - """绘制颜色选择器对话框""" - if not self._color_picker_active: - return - - # 设置对话框标志 - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - # 获取屏幕尺寸,居中显示对话框 - display_size = imgui.get_io().display_size - dialog_width = 300 - dialog_height = 400 - imgui.set_next_window_size((dialog_width, dialog_height)) - imgui.set_next_window_pos( - ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) - ) - - with imgui_ctx.begin("颜色选择器", True, flags) as window: - if not window.opened: - self._color_picker_active = False - self._color_picker_target = None - return - - imgui.text("选择颜色") - imgui.separator() - - # 颜色编辑器 - changed, new_color = imgui.color_edit4( - "颜色##color_picker", - self._color_picker_current_color - ) - - if changed: - self._color_picker_current_color = new_color - - # 预设颜色 - imgui.text("预设颜色") - preset_colors = [ - (1.0, 0.0, 0.0, 1.0), # 红色 - (0.0, 1.0, 0.0, 1.0), # 绿色 - (0.0, 0.0, 1.0, 1.0), # 蓝色 - (1.0, 1.0, 0.0, 1.0), # 黄色 - (1.0, 0.0, 1.0, 1.0), # 洋红 - (0.0, 1.0, 1.0, 1.0), # 青色 - (1.0, 1.0, 1.0, 1.0), # 白色 - (0.0, 0.0, 0.0, 1.0), # 黑色 - (0.5, 0.5, 0.5, 1.0), # 灰色 - (0.188, 0.404, 0.753, 1.0), # 主题蓝色 - (0.176, 1.0, 0.769, 1.0), # 主题绿色 - (0.953, 0.616, 0.471, 1.0), # 主题橙色 - ] - - # 绘制预设颜色按钮 - colors_per_row = 6 - for i, color in enumerate(preset_colors): - if i % colors_per_row != 0: - imgui.same_line() - - imgui.color_button(f"preset_{i}", color, 0, (30, 30)) - if imgui.is_item_clicked(): - self._color_picker_current_color = color - - imgui.separator() - - # 按钮区域 - if imgui.button("确定"): - self._apply_color_selection() - self._color_picker_active = False - self._color_picker_target = None - - imgui.same_line() - if imgui.button("取消"): - self._color_picker_active = False - self._color_picker_target = None - - def _apply_color_selection(self): - """应用颜色选择""" - if not self._color_picker_target: - return - - target_object, property_name = self._color_picker_target - - try: - # 应用颜色到目标对象 - if hasattr(target_object, 'setColor'): - target_object.setColor(self._color_picker_current_color) - elif hasattr(target_object, property_name): - setattr(target_object, property_name, self._color_picker_current_color) - - # 调用回调函数 - if self._color_picker_callback: - self._color_picker_callback(self._color_picker_current_color) - - except Exception as e: - print(f"应用颜色失败: {e}") - - def _draw_color_button(self, label, color, size=(50, 20)): - """绘制颜色按钮并支持点击打开颜色选择器""" - imgui.color_button(label, color, 0, size) - if imgui.is_item_clicked(): - # 打开颜色选择器 - self.show_color_picker(None, None, color) - - def _refresh_available_fonts(self): - """刷新可用字体列表""" - try: - import platform - from pathlib import Path - - system = platform.system().lower() - font_paths = [] - - if system == "linux": - font_dirs = [ - "/usr/share/fonts/truetype/", - "/usr/share/fonts/opentype/", - "/usr/local/share/fonts/", - "~/.fonts/" - ] - elif system == "windows": - font_dirs = [ - "C:/Windows/Fonts/", - ] - elif system == "darwin": - font_dirs = [ - "/System/Library/Fonts/", - "/Library/Fonts/", - "~/Library/Fonts/" - ] - else: - font_dirs = [] - - # 扫描字体目录 - common_fonts = [] - for font_dir in font_dirs: - font_path = Path(font_dir).expanduser() - if font_path.exists(): - for font_file in font_path.rglob("*.ttf"): - common_fonts.append(str(font_file)) - for font_file in font_path.rglob("*.otf"): - common_fonts.append(str(font_file)) - for font_file in font_path.rglob("*.ttc"): - common_fonts.append(str(font_file)) - - # 过滤常见字体 - font_keywords = [ - "arial", "helvetica", "times", "courier", "verdana", "georgia", - "comic", "impact", "trebuchet", "palatino", "garamond", - "noto", "dejavu", "liberation", "ubuntu", "roboto", "open", - "droid", "source", "wenquanyi", "wqy", "pingfang", "stheiti", - "microsoft", "msyh", "simsun", "simhei", "kaiti", "fangsong" - ] - - self._available_fonts = [] - for font_path in common_fonts: - font_name = Path(font_path).name.lower() - if any(keyword in font_name for keyword in font_keywords): - self._available_fonts.append(font_path) - - # 添加一些默认字体路径 - default_fonts = [ - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", - "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", - "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf", - "C:/Windows/Fonts/arial.ttf", - "C:/Windows/Fonts/msyh.ttc", - "/System/Library/Fonts/PingFang.ttc" - ] - - for font_path in default_fonts: - if Path(font_path).exists() and font_path not in self._available_fonts: - self._available_fonts.append(font_path) - - print(f"✓ 找到 {len(self._available_fonts)} 个可用字体") - - except Exception as e: - print(f"⚠ 字体扫描失败: {e}") - self._available_fonts = [] - - def show_font_selector(self, target_object, property_name, current_font, callback=None): - """显示字体选择器""" - self._font_selector_active = True - self._font_selector_target = (target_object, property_name) - self._font_selector_current_font = current_font or "" - self._font_selector_callback = callback - - def _draw_font_selector(self): - """绘制字体选择器对话框""" - if not self._font_selector_active: - return - - # 设置对话框标志 - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - # 获取屏幕尺寸,居中显示对话框 - display_size = imgui.get_io().display_size - dialog_width = 400 - dialog_height = 500 - imgui.set_next_window_size((dialog_width, dialog_height)) - imgui.set_next_window_pos( - ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) - ) - - with imgui_ctx.begin("字体选择器", True, flags) as window: - if not window.opened: - self._font_selector_active = False - self._font_selector_target = None - return - - imgui.text("选择字体") - imgui.separator() - - # 当前字体显示 - imgui.text(f"当前字体: {self._font_selector_current_font or '默认'}") - - # 字体搜索框 - changed, search_text = imgui.input_text("搜索", "", 256) - imgui.separator() - - # 字体列表 - if imgui.begin_child("font_list", (380, 300)): - for font_path in self._available_fonts: - font_name = Path(font_path).name - - # 搜索过滤 - if search_text and search_text.lower() not in font_name.lower(): - continue - - # 字体项 - if imgui.selectable(font_name, font_path == self._font_selector_current_font): - self._font_selector_current_font = font_path - - # 显示字体路径作为工具提示 - if imgui.is_item_hovered(): - imgui.set_tooltip(font_path) - - imgui.end_child() - - imgui.separator() - - # 按钮区域 - if imgui.button("确定"): - self._apply_font_selection() - self._font_selector_active = False - self._font_selector_target = None - - imgui.same_line() - if imgui.button("取消"): - self._font_selector_active = False - self._font_selector_target = None - - imgui.same_line() - if imgui.button("刷新字体"): - self._refresh_available_fonts() - - def _apply_font_selection(self): - """应用字体选择""" - if not self._font_selector_target: - return - - target_object, property_name = self._font_selector_target - - try: - # 应用字体到目标对象 - if hasattr(target_object, property_name): - setattr(target_object, property_name, self._font_selector_current_font) - - # 调用回调函数 - if self._font_selector_callback: - self._font_selector_callback(self._font_selector_current_font) - - except Exception as e: - print(f"应用字体失败: {e}") - - def _draw_font_selector_button(self, label, current_font): - """绘制字体选择器按钮""" - font_name = Path(current_font).name if current_font else "默认字体" - display_text = f"{font_name[:20]}..." if len(font_name) > 20 else font_name - - if imgui.button(f"{label}: {display_text}##font_selector"): - self.show_font_selector(None, None, current_font) - - def _draw_console(self): - """绘制控制台面板""" - # 使用面板类型的窗口标志,支持docking - flags = self.style_manager.get_window_flags("panel") - - with self.style_manager.begin_styled_window("控制台", self.showConsole, flags): - self.showConsole = True # 确保窗口保持打开 - - imgui.text("控制台输出") - imgui.separator() - - # 显示消息系统中的消息 - if hasattr(self, 'messages') and self.messages: - for message in self.messages: - # 显示时间戳 - imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"[{message['timestamp']}]") - imgui.same_line() - - # 根据消息类型显示图标 - if message['text'].startswith('✓'): - if self.icons.get('success'): - imgui.image(self.icons['success'], (12, 12)) - imgui.same_line() - elif message['text'].startswith('✗'): - if self.icons.get('delete_fail_icon'): - imgui.image(self.icons['delete_fail_icon'], (12, 12)) - imgui.same_line() - elif message['text'].startswith('⚠'): - if self.icons.get('warning'): - imgui.image(self.icons['warning'], (12, 12)) - imgui.same_line() - - # 显示消息文本 - imgui.text_colored(message['color'], message['text']) - else: - # 默认消息 - imgui.text_colored((0.157, 0.620, 1.0, 1.0), "[系统]") - imgui.same_line() - imgui.text("引擎已就绪") - - # 输入框 - imgui.separator() - changed, command = imgui.input_text(">", "", 256) - if changed and command: - self.add_info_message(f"执行命令: {command}") - # TODO: 实现命令执行逻辑 - - imgui.separator() - - # 视角控制信息 - imgui.text("视角控制:") - imgui.text(" WASD - 移动") - imgui.text(" Q/E - 上下") - imgui.text(" 右键拖拽 - 旋转视角") - imgui.text(" 滚轮 - 前进/后退") - - # 相机位置信息 - cam_pos = self.camera.getPos() - cam_hpr = self.camera.getHpr() - imgui.text(f"位置: X={cam_pos.x:.1f}, Y={cam_pos.y:.1f}, Z={cam_pos.z:.1f}") - imgui.text(f"旋转: H={cam_hpr.x:.1f}, P={cam_hpr.y:.1f}, R={cam_hpr.z:.1f}") - - # 控制状态 - imgui.checkbox("启用视角控制", self.camera_control_enabled) - - # 重置按钮 - if imgui.button("重置相机"): - self.camera.setPos(0, -20, 5) - self.camera.setHpr(0, 0, 0) - self.add_info_message("相机位置已重置") - - def _draw_script_panel(self): - """绘制脚本管理面板(与Qt版本功能一致)""" - # 使用面板类型的窗口标志,支持docking - flags = self.style_manager.get_window_flags("panel") - - with self.style_manager.begin_styled_window("脚本管理", self.showScriptPanel, flags): - self.showScriptPanel = True # 确保窗口保持打开 - - # 1. 脚本系统状态组 - self._draw_script_status_group() - - imgui.spacing() - - # 2. 创建脚本组 - self._draw_create_script_group() - - imgui.spacing() - - # 3. 可用脚本组 - self._draw_available_scripts_group() - - imgui.spacing() - - # 4. 脚本挂载组 - self._draw_script_mounting_group() - - def _draw_script_status_group(self): - """绘制脚本系统状态组""" - if imgui.collapsing_header("脚本系统状态", imgui.TreeNodeFlags_.default_open): - # 脚本系统状态 - imgui.text("脚本引擎状态:") - imgui.same_line() - - # 检查脚本管理器是否正常工作 - if hasattr(self, 'script_manager') and self.script_manager: - if hasattr(self.script_manager, 'engine') and self.script_manager.engine: - imgui.text_colored((0.0, 1.0, 0.0, 1.0), "✓ 已启动") - else: - imgui.text_colored((1.0, 0.5, 0.0, 1.0), "⚠ 引擎未初始化") - else: - imgui.text_colored((1.0, 0.0, 0.0, 1.0), "✗ 未启动") - - # 热重载状态 - imgui.text("热重载状态:") - imgui.same_line() - - hot_reload_enabled = False - if hasattr(self, 'script_manager') and self.script_manager: - hot_reload_enabled = getattr(self.script_manager, 'hot_reload_enabled', False) - - if hot_reload_enabled: - imgui.text_colored((0.0, 1.0, 0.0, 1.0), "✓ 已启用") - else: - imgui.text_colored((1.0, 0.5, 0.0, 1.0), "✗ 已禁用") - - imgui.same_line() - if imgui.button("切换热重载##toggle_hot_reload"): - self._toggle_hot_reload() - - def _draw_create_script_group(self): - """绘制创建脚本组""" - if imgui.collapsing_header("创建脚本"): - # 脚本名称输入 - imgui.text("脚本名称:") - imgui.same_line() - - # 获取当前脚本名称 - if not hasattr(self, '_new_script_name'): - self._new_script_name = "new_script" - - changed, new_name = imgui.input_text("##script_name", self._new_script_name, 256) - if changed: - self._new_script_name = new_name - - # 模板选择 - imgui.text("脚本模板:") - imgui.same_line() - - if not hasattr(self, '_selected_template'): - self._selected_template = 0 - - templates = ["基础脚本", "移动脚本", "旋转脚本", "缩放脚本", "动画脚本"] - changed, selected = imgui.combo("##script_template", self._selected_template, templates) - if changed: - self._selected_template = selected - - # 创建按钮 - if imgui.button("创建脚本##create_script"): - self._create_new_script() - - imgui.same_line() - if imgui.button("从文件创建##create_from_file"): - self._on_create_script() - - def _draw_available_scripts_group(self): - """绘制可用脚本组""" - if imgui.collapsing_header("可用脚本"): - # 刷新脚本列表 - if imgui.button("刷新列表##refresh_scripts"): - self._refresh_scripts_list() - - imgui.same_line() - if imgui.button("重载全部##reload_all_scripts"): - self._reload_all_scripts() - - imgui.separator() - - # 获取可用脚本列表 - available_scripts = [] - if hasattr(self, 'script_manager') and self.script_manager: - try: - available_scripts = self.script_manager.get_available_scripts() - except Exception as e: - print(f"获取脚本列表失败: {e}") - - # 显示脚本列表 - if available_scripts: - for i, script_name in enumerate(available_scripts): - selected, _ = imgui.selectable(f"{script_name}##script_{i}", False) - if selected: - self._on_script_selected(script_name) - - # 双击编辑 - if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - self._edit_script(script_name) - else: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无可用脚本") - - def _draw_script_mounting_group(self): - """绘制脚本挂载组""" - if imgui.collapsing_header("脚本挂载"): - # 显示当前选中对象 - selected_node = None - if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): - selected_node = self.selection.selectedNode - - if selected_node and not selected_node.isEmpty(): - imgui.text("选中对象:") - imgui.same_line() - imgui.text_colored((0.0, 1.0, 0.0, 1.0), selected_node.getName() or "未命名对象") - - imgui.spacing() - - # 脚本选择和挂载 - imgui.text("选择脚本:") - imgui.same_line() - - # 获取可用脚本 - available_scripts = [] - if hasattr(self, 'script_manager') and self.script_manager: - try: - available_scripts = self.script_manager.get_available_scripts() - except Exception as e: - print(f"获取脚本列表失败: {e}") - - if available_scripts: - if not hasattr(self, '_mount_script_index'): - self._mount_script_index = 0 - - changed, selected = imgui.combo("##mount_script", self._mount_script_index, available_scripts) - if changed: - self._mount_script_index = selected - - imgui.same_line() - if imgui.button("挂载##mount_script"): - if self._mount_script_index < len(available_scripts): - script_name = available_scripts[self._mount_script_index] - self._mount_script_to_selected(script_name) - else: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无可用脚本") - - imgui.spacing() - - # 显示已挂载的脚本 - imgui.text("已挂载脚本:") - - mounted_scripts = [] - if hasattr(self, 'script_manager') and self.script_manager: - try: - mounted_scripts = self.script_manager.get_scripts_on_object(selected_node) - except Exception as e: - print(f"获取已挂载脚本失败: {e}") - - if mounted_scripts: - for i, script_component in enumerate(mounted_scripts): - # 从ScriptComponent获取脚本名称 - script_name = getattr(script_component, 'script_name', None) - if not script_name and hasattr(script_component, '__class__'): - script_name = script_component.__class__.__name__ - - if not script_name: - script_name = f"Script_{i}" - - imgui.text(f"• {script_name}") - imgui.same_line() - if imgui.button(f"卸载##unmount_{i}"): - self._unmount_script_from_selected(script_name) - imgui.same_line() - if imgui.button(f"编辑##edit_mounted_{i}"): - self._edit_script(script_name) - else: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无挂载脚本") - else: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请先选择一个对象") - - def _toggle_hot_reload(self): - """切换热重载状态""" - if hasattr(self, 'script_manager') and self.script_manager: - try: - current_state = getattr(self.script_manager, 'hot_reload_enabled', False) - self.script_manager.hot_reload_enabled = not current_state - - new_state = "启用" if not current_state else "禁用" - self.add_success_message(f"热重载已{new_state}") - print(f"[脚本系统] 热重载已{new_state}") - except Exception as e: - self.add_error_message(f"切换热重载失败: {str(e)}") - print(f"[脚本系统] 切换热重载失败: {e}") - - def _create_new_script(self): - """创建新脚本""" - if not hasattr(self, '_new_script_name') or not self._new_script_name.strip(): - self.add_error_message("请输入脚本名称") - return - - script_name = self._new_script_name.strip() - if not script_name.endswith('.py'): - script_name += '.py' - - # 确定模板类型 - template_map = { - 0: "basic", - 1: "movement", - 2: "rotation", - 3: "scale", - 4: "animation" - } - template_type = template_map.get(getattr(self, '_selected_template', 0), "basic") - - try: - if hasattr(self, 'script_manager') and self.script_manager: - result = self.script_manager.create_script_file(script_name, template_type) - if result: - self.add_success_message(f"脚本 {script_name} 创建成功") - print(f"[脚本系统] 创建脚本成功: {script_name}") - # 刷新脚本列表 - self._refresh_scripts_list() - else: - self.add_error_message(f"脚本 {script_name} 创建失败") - else: - self.add_error_message("脚本管理器未初始化") - except Exception as e: - self.add_error_message(f"创建脚本失败: {str(e)}") - print(f"[脚本系统] 创建脚本失败: {e}") - - def _refresh_scripts_list(self): - """刷新脚本列表""" - try: - if hasattr(self, 'script_manager') and self.script_manager: - # 这里可以添加缓存逻辑,避免频繁刷新 - available_scripts = self.script_manager.get_available_scripts() - print(f"[脚本系统] 刷新脚本列表: {len(available_scripts)} 个脚本") - self.add_success_message(f"脚本列表已刷新,共 {len(available_scripts)} 个脚本") - else: - self.add_error_message("脚本管理器未初始化") - except Exception as e: - self.add_error_message(f"刷新脚本列表失败: {str(e)}") - print(f"[脚本系统] 刷新脚本列表失败: {e}") - - def _reload_all_scripts(self): - """重载所有脚本""" - try: - if hasattr(self, 'script_manager') and self.script_manager: - # 获取所有可用脚本并逐个重载 - available_scripts = self.script_manager.get_available_scripts() - success_count = 0 - - for script_name in available_scripts: - if self.script_manager.reload_script(script_name): - success_count += 1 - - self.add_success_message(f"重载完成: {success_count}/{len(available_scripts)} 个脚本成功") - print(f"[脚本系统] 重载脚本: {success_count}/{len(available_scripts)} 成功") - else: - self.add_error_message("脚本管理器未初始化") - except Exception as e: - self.add_error_message(f"重载脚本失败: {str(e)}") - print(f"[脚本系统] 重载脚本失败: {e}") - - def _on_script_selected(self, script_name): - """处理脚本选择事件""" - print(f"[脚本系统] 选择脚本: {script_name}") - self.add_info_message(f"已选择脚本: {script_name}") - - def _edit_script(self, script_name): - """编辑脚本""" - try: - if hasattr(self, 'script_manager') and self.script_manager: - # 获取脚本信息 - script_info = self.script_manager.get_script_info(script_name) - if script_info and script_info.get("file"): - script_path = script_info["file"] - - # 打开系统默认编辑器 - import subprocess - import platform - - system = platform.system() - try: - if system == "Windows": - subprocess.run(['notepad', script_path]) - elif system == "Darwin": # macOS - subprocess.run(['open', script_path]) - else: # Linux - subprocess.run(['xdg-open', script_path]) - - self.add_success_message(f"已打开脚本编辑器: {script_name}") - print(f"[脚本系统] 编辑脚本: {script_path}") - except Exception as e: - self.add_error_message(f"打开编辑器失败: {str(e)}") - else: - self.add_error_message(f"找不到脚本文件: {script_name}") - else: - self.add_error_message("脚本管理器未初始化") - except Exception as e: - self.add_error_message(f"编辑脚本失败: {str(e)}") - print(f"[脚本系统] 编辑脚本失败: {e}") - - def _mount_script_to_selected(self, script_name): - """挂载脚本到选中对象""" - selected_node = None - if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): - selected_node = self.selection.selectedNode - - if not selected_node or selected_node.isEmpty(): - self.add_error_message("请先选择一个对象") - return - - try: - if hasattr(self, 'script_manager') and self.script_manager: - script_component = self.script_manager.add_script_to_object(selected_node, script_name) - if script_component: - self.add_success_message(f"脚本 {script_name} 已挂载到 {selected_node.getName()}") - print(f"[脚本系统] 挂载脚本: {script_name} -> {selected_node.getName()}") - else: - self.add_error_message(f"挂载脚本 {script_name} 失败") - else: - self.add_error_message("脚本管理器未初始化") - except Exception as e: - self.add_error_message(f"挂载脚本失败: {str(e)}") - print(f"[脚本系统] 挂载脚本失败: {e}") - - def _unmount_script_from_selected(self, script_name): - """从选中对象卸载脚本""" - selected_node = None - if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): - selected_node = self.selection.selectedNode - - if not selected_node or selected_node.isEmpty(): - self.add_error_message("请先选择一个对象") - return - - try: - if hasattr(self, 'script_manager') and self.script_manager: - result = self.script_manager.remove_script_from_object(selected_node, script_name) - if result: - self.add_success_message(f"脚本 {script_name} 已从 {selected_node.getName()} 卸载") - print(f"[脚本系统] 卸载脚本: {script_name} <- {selected_node.getName()}") - else: - self.add_error_message(f"卸载脚本 {script_name} 失败") - else: - self.add_error_message("脚本管理器未初始化") - except Exception as e: - self.add_error_message(f"卸载脚本失败: {str(e)}") - print(f"[脚本系统] 卸载脚本失败: {e}") - - # ==================== 菜单处理函数 ==================== - - def _on_new_project(self): - """处理新建项目菜单项""" - self.add_info_message("打开新建项目对话框") - self.show_new_project_dialog = True - - def _on_open_project(self): - """处理打开项目菜单项""" - self.add_info_message("打开项目对话框") - self.show_open_project_dialog = True - - def _on_save_project(self): - """处理保存项目菜单项""" - if hasattr(self, 'project_manager') and self.project_manager: - try: - # 检查是否有当前项目路径 - if not self.project_manager.current_project_path: - self.add_warning_message("没有当前项目路径,请先创建或打开项目") - self.show_save_as_dialog = True - return - - # 直接调用保存逻辑,避免Qt依赖 - if self._save_project_impl(): - self.add_success_message("项目保存成功") - else: - self.add_error_message("项目保存失败") - except Exception as e: - self.add_error_message(f"项目保存失败: {e}") - else: - self.add_error_message("项目管理器未初始化") - - def _on_save_as_project(self): - """处理另存为项目菜单项""" - self.add_info_message("另存为项目(功能待实现)") - # TODO: 实现另存为对话框 - # self.show_save_as_dialog = True - - def _on_exit(self): - """处理退出菜单项""" - self.add_info_message("退出应用程序") - self.userExit() - - # ==================== 键盘事件处理函数 ==================== - - def _on_ctrl_pressed(self): - """Ctrl键按下""" - self.ctrl_pressed = True - - def _on_ctrl_released(self): - """Ctrl键释放""" - self.ctrl_pressed = False - - def _on_alt_pressed(self): - """Alt键按下""" - self.alt_pressed = True - - def _on_alt_released(self): - """Alt键释放""" - self.alt_pressed = False - - def _on_n_pressed(self): - """N键按下 - 检查Ctrl+N组合键""" - if self.ctrl_pressed: - self._on_new_project() - - def _on_o_pressed(self): - """O键按下 - 检查Ctrl+O组合键""" - if self.ctrl_pressed: - self._on_open_project() - - - - def _on_f4_pressed(self): - """F4键按下 - 检查Alt+F4组合键""" - if self.alt_pressed: - self._on_exit() - - # 移除了单独的按键处理方法,现在直接使用组合键事件 - - def _on_delete_pressed(self): - """Delete键按下 - 删除选中节点""" - self._on_delete() - - def _on_wheel_up(self): - """滚轮向上滚动 - 相机前进""" - try: - if not self.camera_control_enabled: - return - - # 检查鼠标是否在ImGui窗口上 - if self._is_mouse_over_imgui(): - return - - # 沿相机前向向量移动 - forward = self.camera.getMat().getRow3(1) - distance = 20.0 * globalClock.getDt() - currentPos = self.camera.getPos() - newPos = currentPos + forward * distance - self.camera.setPos(newPos) - except Exception as e: - print(f"滚轮前进失败: {e}") - - def _on_wheel_down(self): - """滚轮向下滚动 - 相机后退""" - try: - # 检查鼠标是否在ImGui窗口上 - if self._is_mouse_over_imgui(): - return - - # 沿相机前向向量移动 - forward = self.camera.getMat().getRow3(1) - distance = -20.0 * globalClock.getDt() - currentPos = self.camera.getPos() - newPos = currentPos + forward * distance - self.camera.setPos(newPos) - except Exception as e: - print(f"滚轮后退失败: {e}") - - def _is_mouse_over_imgui(self): - """检测鼠标是否在ImGui窗口上""" - try: - # 检查是否有任何ImGui窗口想要捕获鼠标 - if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse: - return True - - # 检查鼠标是否在任何ImGui窗口内 - mouse_pos = self.mouseWatcherNode.getMouse() - if not mouse_pos: - return False - - # 简单的边界检查(可以根据需要扩展) - display_size = imgui.get_io().display_size - mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2 - mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2) - - # 检查是否在常见的ImGui界面区域内 - # 这里可以根据实际的ImGui窗口位置进行更精确的检测 - if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏) - return True - if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏) - return True - if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域 - return True - - return False - except Exception as e: - print(f"ImGui界面检测失败: {e}") - return False - - def processImGuiMouseClick(self, x, y): - """处理ImGui鼠标点击事件,返回是否消费了该事件""" - try: - # ImGui优先策略:如果ImGui想要捕获鼠标,则由ImGui处理 - if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse: - return True - - # 检查是否有任何ImGui窗口悬停 - try: - if imgui.is_any_window_hovered(): - return True - except AttributeError: - # 如果方法不存在,跳过这个检查 - pass - - # 检查鼠标是否在ImGui界面区域内 - if self._is_mouse_over_imgui(): - return True - - # 如果以上条件都不满足,则让3D场景处理该事件 - return False - - except Exception as e: - print(f"ImGui鼠标点击处理失败: {e}") - return False - - # ==================== 消息系统 ==================== - - def add_message(self, text, color=(1.0, 1.0, 1.0, 1.0)): - """添加消息到消息列表,同时输出到终端""" - import datetime - timestamp = datetime.datetime.now().strftime("%H:%M:%S") - - # 输出到终端 - print(f"[{timestamp}] {text}") - - # 添加到GUI消息列表 - self.messages.append({ - 'text': text, - 'color': color, - 'timestamp': timestamp - }) - - # 限制消息数量 - if len(self.messages) > self.max_messages: - self.messages = self.messages[-self.max_messages:] - - def add_success_message(self, text): - """添加成功消息""" - self.add_message(f"✓ {text}", (0.176, 1.0, 0.769, 1.0)) - - def add_error_message(self, text): - """添加错误消息""" - self.add_message(f"✗ {text}", (1.0, 0.3, 0.3, 1.0)) - - def add_warning_message(self, text): - """添加警告消息""" - self.add_message(f"⚠ {text}", (0.953, 0.616, 0.471, 1.0)) - - def add_info_message(self, text): - """添加信息消息""" - self.add_message(f"ℹ {text}", (0.157, 0.620, 1.0, 1.0)) - - # ==================== 编辑菜单功能实现 ==================== - - def _on_undo(self): - """处理撤销操作""" - try: - if hasattr(self, 'command_manager') and self.command_manager: - if self.command_manager.can_undo(): - success = self.command_manager.undo() - if success: - self.add_success_message("撤销操作成功") - else: - self.add_error_message("撤销操作失败") - else: - self.add_warning_message("没有可撤销的操作") - else: - self.add_error_message("命令管理器未初始化") - except Exception as e: - self.add_error_message(f"撤销操作失败: {e}") - - def _on_redo(self): - """处理重做操作""" - try: - if hasattr(self, 'command_manager') and self.command_manager: - if self.command_manager.can_redo(): - success = self.command_manager.redo() - if success: - self.add_success_message("重做操作成功") - else: - self.add_error_message("重做操作失败") - else: - self.add_warning_message("没有可重做的操作") - else: - self.add_error_message("命令管理器未初始化") - except Exception as e: - self.add_error_message(f"重做操作失败: {e}") - - def _on_copy(self): - """处理复制操作""" - try: - if not hasattr(self, 'selection') or not self.selection: - self.add_error_message("选择系统未初始化") - return - - # 获取当前选中的节点 - selected_node = self.selection.selectedNode - if not selected_node: - self.add_warning_message("没有选中的节点") - return - - # 检查节点有效性(不能复制根节点) - if selected_node.getName() == "render": - self.add_warning_message("不能复制根节点") - return - - # 序列化节点 - if hasattr(self, 'scene_manager') and self.scene_manager: - node_data = self.scene_manager.serializeNodeForCopy(selected_node) - if node_data: - self.clipboard = [node_data] - self.clipboard_mode = "copy" - self.add_success_message(f"已复制节点: {selected_node.getName()}") - else: - self.add_error_message("节点序列化失败") - else: - self.add_error_message("场景管理器未初始化") - except Exception as e: - self.add_error_message(f"复制操作失败: {e}") - - def _on_cut(self): - """处理剪切操作""" - try: - if not hasattr(self, 'selection') or not self.selection: - self.add_error_message("选择系统未初始化") - return - - # 获取当前选中的节点 - selected_node = self.selection.selectedNode - if not selected_node: - self.add_warning_message("没有选中的节点") - return - - # 检查节点有效性(不能剪切根节点和系统节点) - node_name = selected_node.getName() - if node_name == "render": - self.add_warning_message("不能剪切根节点") - return - - # 序列化节点 - if hasattr(self, 'scene_manager') and self.scene_manager: - node_data = self.scene_manager.serializeNodeForCopy(selected_node) - if node_data: - self.clipboard = [node_data] - self.clipboard_mode = "cut" - - # 删除原节点 - self._delete_node(selected_node) - self.selection.clearSelection() - - self.add_success_message(f"已剪切节点: {node_name}") - else: - self.add_error_message("节点序列化失败") - else: - self.add_error_message("场景管理器未初始化") - except Exception as e: - self.add_error_message(f"剪切操作失败: {e}") - - def _on_paste(self): - """处理粘贴操作""" - try: - if not self.clipboard: - self.add_warning_message("剪切板为空") - return - - if not hasattr(self, 'scene_manager') or not self.scene_manager: - self.add_error_message("场景管理器未初始化") - return - - # 确定粘贴目标父节点 - parent_node = None - if hasattr(self, 'selection') and self.selection: - selected_node = self.selection.selectedNode - if selected_node: - parent_node = selected_node - - # 如果没有选中节点,使用渲染根节点 - if not parent_node: - parent_node = self.render - - # 反序列化并添加节点 - for node_data in self.clipboard: - new_node = self.scene_manager.deserializeNode(node_data, parent_node) - if new_node: - self.add_success_message(f"已粘贴节点: {new_node.getName()}") - - # 如果是剪切模式,清空剪切板 - if self.clipboard_mode == "cut": - self.clipboard = [] - self.clipboard_mode = "" - else: - self.add_error_message("节点反序列化失败") - except Exception as e: - self.add_error_message(f"粘贴操作失败: {e}") - - def _on_delete(self): - """处理删除操作""" - try: - if not hasattr(self, 'selection') or not self.selection: - self.add_error_message("选择系统未初始化") - return - - # 获取当前选中的节点 - selected_node = self.selection.selectedNode - if not selected_node: - self.add_warning_message("没有选中的节点") - return - - # 检查节点有效性(不能删除根节点) - node_name = selected_node.getName() - if node_name == "render": - self.add_warning_message("不能删除根节点") - return - - # 删除节点 - if hasattr(self, 'scene_manager') and self.scene_manager: - self._delete_node(selected_node) - self.selection.clearSelection() - self.add_success_message(f"已删除节点: {node_name}") - else: - self.add_error_message("场景管理器未初始化") - except Exception as e: - self.add_error_message(f"删除操作失败: {e}") - - def _delete_node(self, node): - """删除节点的通用方法 - 使用命令系统""" - try: - if not node or node.isEmpty(): - return False - - node_name = node.getName() or "未命名节点" - parent = node.getParent() - - # 创建删除命令 - if hasattr(self, 'command_manager') and self.command_manager: - from core.Command_System import DeleteNodeCommand - command = DeleteNodeCommand(node, parent, self) - self.command_manager.execute_command(command) - print(f"[命令系统] 创建删除命令: {node_name}") - else: - # 备用方案:直接删除并执行清理 - print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}") - self._perform_node_cleanup(node) - node.removeNode() - - print(f"[删除] 成功删除节点: {node_name}") - return True - - except Exception as e: - print(f"[删除] 删除节点失败: {e}") - return False - - def _perform_node_cleanup(self, node): - """执行节点清理逻辑""" - try: - node_name = node.getName() or "未命名节点" - - # 从场景管理器的模型列表中移除(如果是模型) - if hasattr(self, 'scene_manager') and self.scene_manager: - if node in self.scene_manager.models: - self.scene_manager.models.remove(node) - print(f"[场景管理器] 从模型列表移除: {node_name}") - - # 停止所有与该节点相关的脚本 - if hasattr(self, 'script_manager') and self.script_manager: - try: - # 移除该节点上的所有脚本 - if node in self.script_manager.object_scripts: - del self.script_manager.object_scripts[node] - print(f"[脚本系统] 移除节点 {node_name} 的所有脚本") - except Exception as e: - print(f"[脚本系统] 移除脚本失败: {e}") - - # 清理碰撞体 - if hasattr(self, 'collision_manager') and self.collision_manager: - try: - self.collision_manager.remove_collision_for_node(node) - print(f"[碰撞系统] 移除节点 {node_name} 的碰撞体") - except Exception as e: - print(f"[碰撞系统] 移除碰撞体失败: {e}") - - # 清理Actor缓存(如果有动画) - if hasattr(self, '_actor_cache') and node in self._actor_cache: - actor = self._actor_cache[node] - try: - # 清理相关任务 - taskMgr.remove(f"maintain_anim_pos_{id(actor)}") - # 清理Actor - if not actor.isEmpty(): - actor.cleanup() - actor.removeNode() - print(f"[动画系统] 清理节点 {node_name} 的Actor缓存") - except Exception as e: - print(f"[动画系统] 清理Actor缓存失败: {e}") - finally: - del self._actor_cache[node] - - except Exception as e: - print(f"[清理] 节点清理失败: {e}") - - # ==================== 对话框绘制函数 ==================== - - def _draw_new_project_dialog(self): - """绘制新建项目对话框""" - if not self.show_new_project_dialog: - return - - # 初始化默认值 - if not hasattr(self, 'new_project_name'): - self.new_project_name = "新项目" - if not hasattr(self, 'new_project_path'): - self.new_project_path = "./projects/" - - # 设置对话框标志 - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - # 获取屏幕尺寸,居中显示对话框 - display_size = imgui.get_io().display_size - dialog_width = 400 - dialog_height = 300 - imgui.set_next_window_size((dialog_width, dialog_height)) - imgui.set_next_window_pos( - ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) - ) - - with imgui_ctx.begin("新建项目", True, flags) as window: - if not window.opened: - self.show_new_project_dialog = False - return - - imgui.text("创建新项目") - imgui.separator() - - # 项目名称输入 - changed, project_name = imgui.input_text("项目名称", self.new_project_name, 256) - if changed: - self.new_project_name = project_name - - # 项目路径输入 - changed, project_path = imgui.input_text("项目路径", self.new_project_path, 256) - if changed: - self.new_project_path = project_path - - imgui.same_line() - if imgui.button("浏览..."): - self.path_browser_mode = "new_project" - self.path_browser_current_path = os.path.dirname(self.new_project_path) if self.new_project_path else os.getcwd() - self.show_path_browser = True - self._refresh_path_browser() - - imgui.separator() - - # 按钮区域 - if imgui.button("创建"): - if self.new_project_name and self.new_project_path: - self._create_new_project(self.new_project_name, self.new_project_path) - self.show_new_project_dialog = False - - imgui.same_line() - if imgui.button("取消"): - self.show_new_project_dialog = False - - def _draw_open_project_dialog(self): - """绘制打开项目对话框""" - if not self.show_open_project_dialog: - return - - # 初始化默认值 - if not hasattr(self, 'open_project_path'): - self.open_project_path = "./projects/" - - # 设置对话框标志 - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - # 获取屏幕尺寸,居中显示对话框 - display_size = imgui.get_io().display_size - dialog_width = 500 - dialog_height = 400 - imgui.set_next_window_size((dialog_width, dialog_height)) - imgui.set_next_window_pos( - ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) - ) - - with imgui_ctx.begin("打开项目", True, flags) as window: - if not window.opened: - self.show_open_project_dialog = False - return - - imgui.text("选择项目") - imgui.separator() - - imgui.text("项目路径:") - changed, project_path = imgui.input_text("##project_path", self.open_project_path, 512) - if changed: - self.open_project_path = project_path - - imgui.same_line() - if imgui.button("浏览..."): - self.path_browser_mode = "open_project" - self.path_browser_current_path = self.open_project_path if self.open_project_path else os.getcwd() - self.show_path_browser = True - self._refresh_path_browser() - - imgui.separator() - - # 按钮区域 - if imgui.button("打开"): - if self.open_project_path: - self._open_project_path(self.open_project_path) - self.show_open_project_dialog = False - - imgui.same_line() - if imgui.button("取消"): - self.show_open_project_dialog = False - - def _draw_path_browser(self): - """绘制路径选择对话框""" - if not self.show_path_browser: - return - - # 设置对话框标志 - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - # 获取屏幕尺寸,居中显示对话框 - display_size = imgui.get_io().display_size - dialog_width = 600 - dialog_height = 500 - imgui.set_next_window_size((dialog_width, dialog_height)) - imgui.set_next_window_pos( - ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) - ) - - with imgui_ctx.begin("选择路径", True, flags) as window: - if not window.opened: - self.show_path_browser = False - return - - imgui.text("选择路径") - imgui.separator() - - # 当前路径显示 - imgui.text("当前路径:") - imgui.same_line() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.path_browser_current_path) - - imgui.separator() - - # 路径导航按钮 - if imgui.button("上级目录"): - parent_path = os.path.dirname(self.path_browser_current_path) - if parent_path != self.path_browser_current_path: - self.path_browser_current_path = parent_path - self._refresh_path_browser() - - imgui.same_line() - if imgui.button("主目录"): - self.path_browser_current_path = os.path.expanduser("~") - self._refresh_path_browser() - - imgui.same_line() - if imgui.button("当前目录"): - self.path_browser_current_path = os.getcwd() - self._refresh_path_browser() - - imgui.separator() - - # 文件和目录列表 - if self.path_browser_items: - # 先显示目录 - for item in self.path_browser_items: - if item['is_dir']: - # 尝试使用图标或文本标识目录 - if self.icons.get('property_select_image'): # 使用现有图标作为文件夹图标 - imgui.image(self.icons['property_select_image'], (16, 16)) - imgui.same_line() - else: - imgui.text_colored((0.4, 0.6, 1.0, 1.0), ">") - imgui.same_line() - - if imgui.selectable(item['name'], False)[0]: - self.path_browser_current_path = item['path'] - self._refresh_path_browser() - if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - self.path_browser_current_path = item['path'] - self._refresh_path_browser() - -# 显示文件(根据模式显示不同类型的文件) - if self.path_browser_mode == "open_project": - for item in self.path_browser_items: - if not item['is_dir'] and item['name'].endswith('.json'): - imgui.text_colored((1.0, 1.0, 0.7, 1.0), "[FILE]") - imgui.same_line() - if imgui.selectable(item['name'], False)[0]: - self.path_browser_selected_path = item['path'] - if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - # 选择包含project.json的目录 - self.path_browser_current_path = os.path.dirname(item['path']) - self._apply_selected_path() - elif self.path_browser_mode == "import_model": - for item in self.path_browser_items: - if not item['is_dir']: - file_ext = os.path.splitext(item['name'])[1].lower() - # 根据文件类型显示不同颜色 - if file_ext in ['.gltf', '.glb']: - color = (0.7, 1.0, 0.7, 1.0) # 绿色 - glTF - elif file_ext == '.fbx': - color = (1.0, 0.7, 0.7, 1.0) # 红色 - FBX - elif file_ext in ['.bam', '.egg']: - color = (0.7, 0.7, 1.0, 1.0) # 蓝色 - Panda3D - elif file_ext == '.obj': - color = (1.0, 1.0, 0.7, 1.0) # 黄色 - OBJ - else: - color = (0.8, 0.8, 0.8, 1.0) # 灰色 - 其他 - - imgui.text_colored(color, f"[{file_ext[1:].upper()}]") - imgui.same_line() - if imgui.selectable(item['name'], False)[0]: - self.path_browser_selected_path = item['path'] - if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): - self.path_browser_selected_path = item['path'] - self._apply_selected_path() - - imgui.separator() - - # 选中路径显示 - if self.path_browser_selected_path: - imgui.text("选中路径:") - imgui.same_line() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.path_browser_selected_path) - - # 按钮区域 - if imgui.button("确定"): - self._apply_selected_path() - self.show_path_browser = False - - imgui.same_line() - if imgui.button("取消"): - self.show_path_browser = False - - def _draw_import_dialog(self): - """绘制导入模型对话框""" - if not self.show_import_dialog: - return - - # 设置对话框标志 - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - # 获取屏幕尺寸,居中显示对话框 - display_size = imgui.get_io().display_size - dialog_width = 600 - dialog_height = 500 - imgui.set_next_window_size((dialog_width, dialog_height)) - imgui.set_next_window_pos( - ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) - ) - - with imgui_ctx.begin("导入模型", True, flags) as window: - if not window.opened: - self.show_import_dialog = False - return - - imgui.text("选择要导入的模型文件") - imgui.separator() - - # 文件路径输入 - imgui.text("文件路径:") - changed, file_path = imgui.input_text("##import_file_path", self.import_file_path, 512) - if changed: - self.import_file_path = file_path - - imgui.same_line() - if imgui.button("浏览..."): - self.path_browser_mode = "import_model" - self.path_browser_current_path = os.path.dirname(self.import_file_path) if self.import_file_path else os.getcwd() - self.show_path_browser = True - self._refresh_path_browser() - - imgui.separator() - - # 支持的格式说明 - imgui.text("支持的文件格式:") - formats_text = ", ".join(self.supported_formats) - imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text) - - imgui.separator() - - # 文件预览信息 - if self.import_file_path and os.path.exists(self.import_file_path): - file_size = os.path.getsize(self.import_file_path) - imgui.text(f"文件大小: {file_size / 1024:.2f} KB") - - file_ext = os.path.splitext(self.import_file_path)[1].lower() - if file_ext in self.supported_formats: - imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持") - else: - imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式") - else: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的文件路径") - - imgui.separator() - - # 按钮区域 - can_import = (self.import_file_path and - os.path.exists(self.import_file_path) and - os.path.splitext(self.import_file_path)[1].lower() in self.supported_formats) - - # 根据状态设置按钮颜色 - if can_import: - if imgui.button("导入"): - self._import_model() - self.show_import_dialog = False - else: - # 禁用状态的按钮(灰色显示) - imgui.push_style_color(imgui.Col_.button, (0.3, 0.3, 0.3, 1.0)) - imgui.button("导入") - imgui.pop_style_color() - - imgui.same_line() - if imgui.button("取消"): - self.show_import_dialog = False - - def _create_new_project(self, name, path): - """创建新项目的实际实现""" - if not hasattr(self, 'project_manager') or not self.project_manager: - print("✗ 项目管理器未初始化") - return - - try: - if self._create_new_project_impl(name, path): - print(f"✓ 项目创建成功: {name}") - else: - print(f"✗ 项目创建失败: {name}") - except Exception as e: - print(f"✗ 项目创建失败: {e}") - - def _open_project_path(self, path): - """打开项目的实际实现""" - if not hasattr(self, 'project_manager') or not self.project_manager: - print("✗ 项目管理器未初始化") - return - - try: - print(f"打开项目: {path}") - if self._open_project_impl(path): - print(f"✓ 项目打开成功: {path}") - else: - print(f"✗ 项目打开失败: {path}") - except Exception as e: - print(f"✗ 项目打开失败: {e}") - - # ==================== 项目管理具体实现 ==================== - - def _save_project_impl(self): - """保存项目的具体实现(不依赖Qt)""" - import json - import datetime - import os - - project_path = self.project_manager.current_project_path - scenes_path = os.path.join(project_path, "scenes") - - # 固定的场景文件名 - scene_file = os.path.join(scenes_path, "scene.bam") - - # 如果存在旧文件,先删除 - if os.path.exists(scene_file): - try: - os.remove(scene_file) - print(f"已删除旧场景文件: {scene_file}") - except Exception as e: - print(f"删除旧场景文件失败: {str(e)}") - return False - - # 保存场景 - if self.scene_manager.saveScene(scene_file, project_path): - # 更新项目配置文件 - config_file = os.path.join(project_path, "project.json") - if os.path.exists(config_file): - with open(config_file, "r", encoding="utf-8") as f: - project_config = json.load(f) - - # 更新最后修改时间 - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # 记录场景文件路径 - project_config["scene_file"] = os.path.relpath(scene_file, project_path) - - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - - # 更新项目配置 - self.project_manager.project_config = project_config - return True - return False - - def _open_project_impl(self, project_path): - """打开项目的具体实现(不依赖Qt)""" - import json - import datetime - import os - - try: - # 检查项目管理器是否已初始化 - if not hasattr(self, 'project_manager') or not self.project_manager: - print("✗ 项目管理器未初始化") - self.add_error_message("项目管理器未初始化") - return False - - # 检查场景管理器是否已初始化 - if not hasattr(self, 'scene_manager') or not self.scene_manager: - print("✗ 场景管理器未初始化") - self.add_error_message("场景管理器未初始化") - return False - - # 检查是否是有效的项目文件夹 - config_file = os.path.join(project_path, "project.json") - if not os.path.exists(config_file): - print(f"⚠ 选择的不是有效的项目文件夹: {project_path}") - self.add_warning_message(f"选择的不是有效的项目文件夹: {project_path}") - return False + def _apply_gui_font_size(self, *args, **kwargs): + return self.property_helpers._apply_gui_font_size(*args, **kwargs) - # 读取项目配置 - try: - with open(config_file, "r", encoding="utf-8") as f: - project_config = json.load(f) - except Exception as e: - print(f"✗ 读取项目配置文件失败: {e}") - self.add_error_message(f"读取项目配置文件失败: {e}") - return False + def _apply_gui_font_style(self, *args, **kwargs): + return self.property_helpers._apply_gui_font_style(*args, **kwargs) - # 检查场景文件 - scene_file = os.path.join(project_path, "scenes", "scene.bam") - if os.path.exists(scene_file): - # 加载场景 - try: - if self.scene_manager.loadScene(scene_file): - # 更新项目配置 - project_config["scene_file"] = os.path.relpath(scene_file, project_path) - print(f"✓ 场景加载成功: {scene_file}") - else: - print(f"⚠ 场景加载失败: {scene_file}") - self.add_warning_message(f"场景加载失败: {scene_file}") - except Exception as e: - print(f"✗ 加载场景时发生错误: {e}") - self.add_error_message(f"加载场景时发生错误: {e}") - # 继续执行,不阻止项目打开 + def _has_collision(self, *args, **kwargs): + return self.property_helpers._has_collision(*args, **kwargs) - # 更新项目配置 - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - try: - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - except Exception as e: - print(f"✗ 保存项目配置失败: {e}") - self.add_error_message(f"保存项目配置失败: {e}") + def _get_current_collision_shape(self, *args, **kwargs): + return self.property_helpers._get_current_collision_shape(*args, **kwargs) - # 更新项目状态 - self.project_manager.current_project_path = project_path - self.project_manager.project_config = project_config - - # 更新窗口标题 - project_name = os.path.basename(project_path) - self._update_window_title(project_name) - - print(f"✓ 项目打开成功: {project_path}") - self.add_success_message(f"项目打开成功: {project_name}") - return True - - except Exception as e: - print(f"✗ 打开项目时发生错误: {e}") - self.add_error_message(f"打开项目时发生错误: {e}") - return False - - def _create_new_project_impl(self, name, path): - """创建新项目的具体实现(不依赖Qt)""" - import json - import datetime - import os - - full_project_path = os.path.normpath(os.path.join(path, name)) - print(f"创建项目路径: {full_project_path}") - - try: - # 创建项目文件夹结构 - os.makedirs(full_project_path) - os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹 - os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹 - scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹 - os.makedirs(scenes_path) - - # 创建项目配置文件 - project_config = { - "name": name, - "path": full_project_path, - "created": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "version": "1.0", - "scene_file": "scenes/scene.bam" - } - - # 保存项目配置 - config_file = os.path.join(full_project_path, "project.json") - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - - # 保存初始场景 - scene_file = os.path.join(scenes_path, "scene.bam") - self.scene_manager.saveScene(scene_file, full_project_path) - - # 更新项目管理器状态 - self.project_manager.current_project_path = full_project_path - self.project_manager.project_config = project_config - - # 更新窗口标题 - self._update_window_title(name) - - return True - - except Exception as e: - print(f"创建项目失败: {e}") - return False - - def _update_window_title(self, project_name): - """更新窗口标题""" - try: - props = WindowProperties() - props.set_title(f"EG Engine - {project_name}") - self.win.request_properties(props) - print(f"窗口标题已更新: EG Engine - {project_name}") - except Exception as e: - print(f"更新窗口标题失败: {e}") - - # ==================== 路径浏览器辅助方法 ==================== - - def _refresh_path_browser(self): - """刷新路径浏览器内容""" - try: - self.path_browser_items = [] - - if not os.path.exists(self.path_browser_current_path): - self.add_error_message(f"路径不存在: {self.path_browser_current_path}") - return - - # 获取目录中的所有项目 - items = [] - try: - for item_name in os.listdir(self.path_browser_current_path): - item_path = os.path.join(self.path_browser_current_path, item_name) - is_dir = os.path.isdir(item_path) - - items.append({ - 'name': item_name, - 'path': item_path, - 'is_dir': is_dir - }) - except PermissionError: - self.add_error_message(f"无法访问路径: {self.path_browser_current_path}") - return - - # 根据模式过滤文件 - if self.path_browser_mode == "import_model": - # 只显示支持的模型文件 - filtered_items = [] - for item in items: - if item['is_dir']: - filtered_items.append(item) - else: - file_ext = os.path.splitext(item['name'])[1].lower() - if file_ext in self.supported_formats: - filtered_items.append(item) - items = filtered_items - - # 排序:目录在前,文件在后,按名称排序 - items.sort(key=lambda x: (not x['is_dir'], x['name'].lower())) - self.path_browser_items = items - - except Exception as e: - self.add_error_message(f"刷新路径浏览器失败: {e}") - - def _apply_selected_path(self): - """应用选择的路径""" - try: - if self.path_browser_mode == "new_project": - # 新建项目模式:直接使用当前路径 - self.new_project_path = self.path_browser_current_path - self.add_info_message(f"已选择项目路径: {self.new_project_path}") - elif self.path_browser_mode == "open_project": - # 打开项目模式:使用当前路径 - self.open_project_path = self.path_browser_current_path - self.add_info_message(f"已选择项目路径: {self.open_project_path}") - elif self.path_browser_mode == "import_model": - # 导入模型模式:使用选择的文件路径 - self.import_file_path = self.path_browser_selected_path - self.add_info_message(f"已选择文件: {self.import_file_path}") - except Exception as e: - self.add_error_message(f"应用路径失败: {e}") - - # ==================== 创建功能对话框实现 ==================== - - def _draw_gui_button_dialog(self): - """绘制GUI按钮创建对话框""" - if not self.show_gui_button_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建GUI按钮", self.show_gui_button_dialog, flags) as window: - if not window: - self.show_gui_button_dialog = False - return - - # 初始化参数 - if 'button_text' not in self.dialog_params: - self.dialog_params['button_text'] = "按钮" - if 'button_pos' not in self.dialog_params: - self.dialog_params['button_pos'] = [0.0, 0.0, 0.0] - if 'button_size' not in self.dialog_params: - self.dialog_params['button_size'] = [0.1, 0.1, 0.1] - - imgui.text("GUI按钮参数设置") - imgui.separator() - - # 文本输入 - changed, self.dialog_params['button_text'] = imgui.input_text("按钮文本", self.dialog_params['button_text'], 256) - - # 位置输入 - changed, x = imgui.input_float("X坐标", self.dialog_params['button_pos'][0]) - if changed: - self.dialog_params['button_pos'][0] = x - changed, y = imgui.input_float("Y坐标", self.dialog_params['button_pos'][1]) - if changed: - self.dialog_params['button_pos'][1] = y - changed, z = imgui.input_float("Z坐标", self.dialog_params['button_pos'][2]) - if changed: - self.dialog_params['button_pos'][2] = z - - # 大小输入 - changed, width = imgui.input_float("宽度", self.dialog_params['button_size'][0]) - if changed: - self.dialog_params['button_size'][0] = width - changed, height = imgui.input_float("高度", self.dialog_params['button_size'][1]) - if changed: - self.dialog_params['button_size'][1] = height - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - pos = tuple(self.dialog_params['button_pos']) - text = self.dialog_params['button_text'] - size = tuple(self.dialog_params['button_size'][:2]) - - result = self.createGUIButton(pos, text, size) - self.add_success_message(f"GUI按钮创建成功: {text}") - self.show_gui_button_dialog = False - except Exception as e: - self.add_error_message(f"创建GUI按钮失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_gui_button_dialog = False - - def _draw_gui_label_dialog(self): - """绘制GUI标签创建对话框""" - if not self.show_gui_label_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建GUI标签", self.show_gui_label_dialog, flags) as window: - if not window: - self.show_gui_label_dialog = False - return - - # 初始化参数 - if 'label_text' not in self.dialog_params: - self.dialog_params['label_text'] = "标签" - if 'label_pos' not in self.dialog_params: - self.dialog_params['label_pos'] = [0.0, 0.0, 0.0] - if 'label_size' not in self.dialog_params: - self.dialog_params['label_size'] = [0.1, 0.1, 0.1] - - imgui.text("GUI标签参数设置") - imgui.separator() - - # 文本输入 - changed, self.dialog_params['label_text'] = imgui.input_text("标签文本", self.dialog_params['label_text'], 256) - - # 位置输入 - changed, x = imgui.input_float("X坐标", self.dialog_params['label_pos'][0]) - if changed: - self.dialog_params['label_pos'][0] = x - changed, y = imgui.input_float("Y坐标", self.dialog_params['label_pos'][1]) - if changed: - self.dialog_params['label_pos'][1] = y - changed, z = imgui.input_float("Z坐标", self.dialog_params['label_pos'][2]) - if changed: - self.dialog_params['label_pos'][2] = z - - # 大小输入 - changed, width = imgui.input_float("宽度", self.dialog_params['label_size'][0]) - if changed: - self.dialog_params['label_size'][0] = width - changed, height = imgui.input_float("高度", self.dialog_params['label_size'][1]) - if changed: - self.dialog_params['label_size'][1] = height - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - pos = tuple(self.dialog_params['label_pos']) - text = self.dialog_params['label_text'] - size = tuple(self.dialog_params['label_size'][:2]) - - result = self.createGUILabel(pos, text, size) - self.add_success_message(f"GUI标签创建成功: {text}") - self.show_gui_label_dialog = False - except Exception as e: - self.add_error_message(f"创建GUI标签失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_gui_label_dialog = False - - def _draw_gui_entry_dialog(self): - """绘制GUI输入框创建对话框""" - if not self.show_gui_entry_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建GUI输入框", self.show_gui_entry_dialog, flags) as window: - if not window: - self.show_gui_entry_dialog = False - return - - # 初始化参数 - if 'entry_pos' not in self.dialog_params: - self.dialog_params['entry_pos'] = [0.0, 0.0, 0.0] - if 'entry_size' not in self.dialog_params: - self.dialog_params['entry_size'] = [0.2, 0.05, 0.1] - - imgui.text("GUI输入框参数设置") - imgui.separator() - - # 位置输入 - changed, x = imgui.input_float("X坐标", self.dialog_params['entry_pos'][0]) - if changed: - self.dialog_params['entry_pos'][0] = x - changed, y = imgui.input_float("Y坐标", self.dialog_params['entry_pos'][1]) - if changed: - self.dialog_params['entry_pos'][1] = y - changed, z = imgui.input_float("Z坐标", self.dialog_params['entry_pos'][2]) - if changed: - self.dialog_params['entry_pos'][2] = z - - # 大小输入 - changed, width = imgui.input_float("宽度", self.dialog_params['entry_size'][0]) - if changed: - self.dialog_params['entry_size'][0] = width - changed, height = imgui.input_float("高度", self.dialog_params['entry_size'][1]) - if changed: - self.dialog_params['entry_size'][1] = height - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - pos = tuple(self.dialog_params['entry_pos']) - size = tuple(self.dialog_params['entry_size'][:2]) - - result = self.createGUIEntry(pos, size) - self.add_success_message("GUI输入框创建成功") - self.show_gui_entry_dialog = False - except Exception as e: - self.add_error_message(f"创建GUI输入框失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_gui_entry_dialog = False - - def _draw_gui_image_dialog(self): - """绘制GUI图片创建对话框""" - if not self.show_gui_image_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建GUI图片", self.show_gui_image_dialog, flags) as window: - if not window: - self.show_gui_image_dialog = False - return - - # 初始化参数 - if 'image_pos' not in self.dialog_params: - self.dialog_params['image_pos'] = [0.0, 0.0, 0.0] - if 'image_size' not in self.dialog_params: - self.dialog_params['image_size'] = [0.2, 0.2, 0.1] - if 'image_path' not in self.dialog_params: - self.dialog_params['image_path'] = "" - - imgui.text("GUI图片参数设置") - imgui.separator() - - # 位置输入 - changed, x = imgui.input_float("X坐标", self.dialog_params['image_pos'][0]) - if changed: - self.dialog_params['image_pos'][0] = x - changed, y = imgui.input_float("Y坐标", self.dialog_params['image_pos'][1]) - if changed: - self.dialog_params['image_pos'][1] = y - changed, z = imgui.input_float("Z坐标", self.dialog_params['image_pos'][2]) - if changed: - self.dialog_params['image_pos'][2] = z - - # 大小输入 - changed, width = imgui.input_float("宽度", self.dialog_params['image_size'][0]) - if changed: - self.dialog_params['image_size'][0] = width - changed, height = imgui.input_float("高度", self.dialog_params['image_size'][1]) - if changed: - self.dialog_params['image_size'][1] = height - - # 图片路径 - changed, self.dialog_params['image_path'] = imgui.input_text("图片路径", self.dialog_params['image_path'], 512) - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - pos = tuple(self.dialog_params['image_pos']) - size = tuple(self.dialog_params['image_size'][:2]) - image_path = self.dialog_params['image_path'] - - result = self.createGUIImage(pos, image_path, size) - self.add_success_message("GUI图片创建成功") - self.show_gui_image_dialog = False - except Exception as e: - self.add_error_message(f"创建GUI图片失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_gui_image_dialog = False - - def _draw_3d_text_dialog(self): - """绘制3D文本创建对话框""" - if not self.show_3d_text_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建3D文本", self.show_3d_text_dialog, flags) as window: - if not window: - self.show_3d_text_dialog = False - return - - # 初始化参数 - if 'text3d_text' not in self.dialog_params: - self.dialog_params['text3d_text'] = "3D文本" - if 'text3d_pos' not in self.dialog_params: - self.dialog_params['text3d_pos'] = [0.0, 0.0, 0.0] - if 'text3d_size' not in self.dialog_params: - self.dialog_params['text3d_size'] = 1.0 - - imgui.text("3D文本参数设置") - imgui.separator() - - # 文本输入 - changed, self.dialog_params['text3d_text'] = imgui.input_text("文本内容", self.dialog_params['text3d_text'], 256) - - # 位置输入 - changed, x = imgui.input_float("X坐标", self.dialog_params['text3d_pos'][0]) - if changed: - self.dialog_params['text3d_pos'][0] = x - changed, y = imgui.input_float("Y坐标", self.dialog_params['text3d_pos'][1]) - if changed: - self.dialog_params['text3d_pos'][1] = y - changed, z = imgui.input_float("Z坐标", self.dialog_params['text3d_pos'][2]) - if changed: - self.dialog_params['text3d_pos'][2] = z - - # 大小输入 - changed, self.dialog_params['text3d_size'] = imgui.input_float("文本大小", self.dialog_params['text3d_size']) - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - pos = tuple(self.dialog_params['text3d_pos']) - text = self.dialog_params['text3d_text'] - size = self.dialog_params['text3d_size'] - - result = self.create3DText(pos, text, size) - self.add_success_message(f"3D文本创建成功: {text}") - self.show_3d_text_dialog = False - except Exception as e: - self.add_error_message(f"创建3D文本失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_3d_text_dialog = False - - def _draw_3d_image_dialog(self): - """绘制3D图片创建对话框""" - if not self.show_3d_image_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建3D图片", self.show_3d_image_dialog, flags) as window: - if not window: - self.show_3d_image_dialog = False - return - - # 初始化参数 - if 'image3d_pos' not in self.dialog_params: - self.dialog_params['image3d_pos'] = [0.0, 0.0, 0.0] - if 'image3d_size' not in self.dialog_params: - self.dialog_params['image3d_size'] = [1.0, 1.0, 1.0] - if 'image3d_path' not in self.dialog_params: - self.dialog_params['image3d_path'] = "" - - imgui.text("3D图片参数设置") - imgui.separator() - - # 位置输入 - changed, x = imgui.input_float("X坐标", self.dialog_params['image3d_pos'][0]) - if changed: - self.dialog_params['image3d_pos'][0] = x - changed, y = imgui.input_float("Y坐标", self.dialog_params['image3d_pos'][1]) - if changed: - self.dialog_params['image3d_pos'][1] = y - changed, z = imgui.input_float("Z坐标", self.dialog_params['image3d_pos'][2]) - if changed: - self.dialog_params['image3d_pos'][2] = z - - # 大小输入 - changed, width = imgui.input_float("宽度", self.dialog_params['image3d_size'][0]) - if changed: - self.dialog_params['image3d_size'][0] = width - changed, height = imgui.input_float("高度", self.dialog_params['image3d_size'][1]) - if changed: - self.dialog_params['image3d_size'][1] = height - - # 图片路径 - changed, self.dialog_params['image3d_path'] = imgui.input_text("图片路径", self.dialog_params['image3d_path'], 512) - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - pos = tuple(self.dialog_params['image3d_pos']) - size = tuple(self.dialog_params['image3d_size'][:2]) - image_path = self.dialog_params['image3d_path'] - - result = self.create3DImage(pos, image_path, size) - self.add_success_message("3D图片创建成功") - self.show_3d_image_dialog = False - except Exception as e: - self.add_error_message(f"创建3D图片失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_3d_image_dialog = False - - # 添加其他创建对话框的占位符方法 - def _draw_video_screen_dialog(self): - """绘制视频屏幕创建对话框""" - if not self.show_video_screen_dialog: - return - self.show_video_screen_dialog = False - self.add_info_message("视频屏幕创建功能开发中...") - - def _draw_2d_video_screen_dialog(self): - """绘制2D视频屏幕创建对话框""" - if not self.show_2d_video_screen_dialog: - return - self.show_2d_video_screen_dialog = False - self.add_info_message("2D视频屏幕创建功能开发中...") - - def _draw_spherical_video_dialog(self): - """绘制球形视频创建对话框""" - if not self.show_spherical_video_dialog: - return - self.show_spherical_video_dialog = False - self.add_info_message("球形视频创建功能开发中...") - - def _draw_virtual_screen_dialog(self): - """绘制虚拟屏幕创建对话框""" - if not self.show_virtual_screen_dialog: - return - self.show_virtual_screen_dialog = False - self.add_info_message("虚拟屏幕创建功能开发中...") - - def _draw_spot_light_dialog(self): - """绘制聚光灯创建对话框""" - if not self.show_spot_light_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建聚光灯", self.show_spot_light_dialog, flags) as window: - if not window: - self.show_spot_light_dialog = False - return - - # 初始化参数 - if 'spotlight_pos' not in self.dialog_params: - self.dialog_params['spotlight_pos'] = [0.0, 0.0, 5.0] - if 'spotlight_color' not in self.dialog_params: - self.dialog_params['spotlight_color'] = [1.0, 1.0, 1.0] - if 'spotlight_intensity' not in self.dialog_params: - self.dialog_params['spotlight_intensity'] = 1.0 - - imgui.text("聚光灯参数设置") - imgui.separator() - - # 位置输入 - changed, x = imgui.input_float("X坐标", self.dialog_params['spotlight_pos'][0]) - if changed: - self.dialog_params['spotlight_pos'][0] = x - changed, y = imgui.input_float("Y坐标", self.dialog_params['spotlight_pos'][1]) - if changed: - self.dialog_params['spotlight_pos'][1] = y - changed, z = imgui.input_float("Z坐标", self.dialog_params['spotlight_pos'][2]) - if changed: - self.dialog_params['spotlight_pos'][2] = z - - # 颜色输入 - changed, r = imgui.input_float("红色 (R)", self.dialog_params['spotlight_color'][0], 0.0, 1.0) - if changed: - self.dialog_params['spotlight_color'][0] = max(0.0, min(1.0, r)) - changed, g = imgui.input_float("绿色 (G)", self.dialog_params['spotlight_color'][1], 0.0, 1.0) - if changed: - self.dialog_params['spotlight_color'][1] = max(0.0, min(1.0, g)) - changed, b = imgui.input_float("蓝色 (B)", self.dialog_params['spotlight_color'][2], 0.0, 1.0) - if changed: - self.dialog_params['spotlight_color'][2] = max(0.0, min(1.0, b)) - - # 强度输入 - changed, self.dialog_params['spotlight_intensity'] = imgui.input_float("强度", self.dialog_params['spotlight_intensity'], 0.1, 2.0) - if changed: - self.dialog_params['spotlight_intensity'] = max(0.1, self.dialog_params['spotlight_intensity']) - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - pos = tuple(self.dialog_params['spotlight_pos']) - - result = self.createSpotLight(pos) - if result: - # 设置颜色和强度 - light = result.node() - if hasattr(light, 'setColor'): - color = tuple(self.dialog_params['spotlight_color']) - light.setColor(color + (1.0,)) # 添加alpha通道 - if hasattr(light, 'setEnergy'): - light.setEnergy(self.dialog_params['spotlight_intensity']) - - self.add_success_message("聚光灯创建成功") - self.show_spot_light_dialog = False - else: - self.add_error_message("聚光灯创建失败") - except Exception as e: - self.add_error_message(f"创建聚光灯失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_spot_light_dialog = False - - def _draw_point_light_dialog(self): - """绘制点光源创建对话框""" - if not self.show_point_light_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建点光源", self.show_point_light_dialog, flags) as window: - if not window: - self.show_point_light_dialog = False - return - - # 初始化参数 - if 'pointlight_pos' not in self.dialog_params: - self.dialog_params['pointlight_pos'] = [0.0, 0.0, 5.0] - if 'pointlight_color' not in self.dialog_params: - self.dialog_params['pointlight_color'] = [1.0, 1.0, 1.0] - if 'pointlight_intensity' not in self.dialog_params: - self.dialog_params['pointlight_intensity'] = 1.0 - if 'pointlight_radius' not in self.dialog_params: - self.dialog_params['pointlight_radius'] = 10.0 - - imgui.text("点光源参数设置") - imgui.separator() - - # 位置输入 - changed, x = imgui.input_float("X坐标", self.dialog_params['pointlight_pos'][0]) - if changed: - self.dialog_params['pointlight_pos'][0] = x - changed, y = imgui.input_float("Y坐标", self.dialog_params['pointlight_pos'][1]) - if changed: - self.dialog_params['pointlight_pos'][1] = y - changed, z = imgui.input_float("Z坐标", self.dialog_params['pointlight_pos'][2]) - if changed: - self.dialog_params['pointlight_pos'][2] = z - - # 颜色输入 - changed, r = imgui.input_float("红色 (R)", self.dialog_params['pointlight_color'][0], 0.0, 1.0) - if changed: - self.dialog_params['pointlight_color'][0] = max(0.0, min(1.0, r)) - changed, g = imgui.input_float("绿色 (G)", self.dialog_params['pointlight_color'][1], 0.0, 1.0) - if changed: - self.dialog_params['pointlight_color'][1] = max(0.0, min(1.0, g)) - changed, b = imgui.input_float("蓝色 (B)", self.dialog_params['pointlight_color'][2], 0.0, 1.0) - if changed: - self.dialog_params['pointlight_color'][2] = max(0.0, min(1.0, b)) - - # 强度输入 - changed, self.dialog_params['pointlight_intensity'] = imgui.input_float("强度", self.dialog_params['pointlight_intensity'], 0.1, 2.0) - if changed: - self.dialog_params['pointlight_intensity'] = max(0.1, self.dialog_params['pointlight_intensity']) - - # 半径输入 - changed, self.dialog_params['pointlight_radius'] = imgui.input_float("影响半径", self.dialog_params['pointlight_radius'], 1.0, 50.0) - if changed: - self.dialog_params['pointlight_radius'] = max(1.0, self.dialog_params['pointlight_radius']) - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - pos = tuple(self.dialog_params['pointlight_pos']) - - result = self.createPointLight(pos) - if result: - # 设置颜色和强度 - light = result.node() - if hasattr(light, 'setColor'): - color = tuple(self.dialog_params['pointlight_color']) - light.setColor(color + (1.0,)) # 添加alpha通道 - if hasattr(light, 'setEnergy'): - light.setEnergy(self.dialog_params['pointlight_intensity']) - if hasattr(light, 'setAttenuation'): - # 设置衰减: (constant, linear, quadratic) - radius = self.dialog_params['pointlight_radius'] - light.setAttenuation((1.0, 0.5/radius, 0.5/(radius*radius))) - - self.add_success_message("点光源创建成功") - self.show_point_light_dialog = False - else: - self.add_error_message("点光源创建失败") - except Exception as e: - self.add_error_message(f"创建点光源失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_point_light_dialog = False - - def _draw_terrain_dialog(self): - """绘制地形创建对话框""" - if not self.show_terrain_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建平面地形", self.show_terrain_dialog, flags) as window: - if not window: - self.show_terrain_dialog = False - return - - # 初始化参数 - if 'terrain_width' not in self.dialog_params: - self.dialog_params['terrain_width'] = 10.0 - if 'terrain_height' not in self.dialog_params: - self.dialog_params['terrain_height'] = 10.0 - if 'terrain_resolution' not in self.dialog_params: - self.dialog_params['terrain_resolution'] = 129 - - imgui.text("平面地形参数设置") - imgui.separator() - - # 尺寸输入 - changed, self.dialog_params['terrain_width'] = imgui.input_float("宽度", self.dialog_params['terrain_width'], 1.0, 100.0) - if changed: - self.dialog_params['terrain_width'] = max(1.0, self.dialog_params['terrain_width']) - - changed, self.dialog_params['terrain_height'] = imgui.input_float("高度", self.dialog_params['terrain_height'], 1.0, 100.0) - if changed: - self.dialog_params['terrain_height'] = max(1.0, self.dialog_params['terrain_height']) - - # 分辨率输入 - changed, self.dialog_params['terrain_resolution'] = imgui.input_int("分辨率", self.dialog_params['terrain_resolution']) - if changed: - # 确保分辨率是有效的 (2的幂次方 + 1) - valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] - if self.dialog_params['terrain_resolution'] not in valid_resolutions: - closest_res = min(valid_resolutions, key=lambda x: abs(x - self.dialog_params['terrain_resolution'])) - self.dialog_params['terrain_resolution'] = closest_res - - imgui.separator() - imgui.text("有效分辨率值: 17, 33, 65, 129, 257, 513, 1025") - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - width = self.dialog_params['terrain_width'] - height = self.dialog_params['terrain_height'] - resolution = self.dialog_params['terrain_resolution'] - - # 转换为地形管理器期望的格式 - size = (width, height) - - result = self.createFlatTerrain(size, resolution) - if result: - self.add_success_message("平面地形创建成功") - self.show_terrain_dialog = False - else: - self.add_error_message("平面地形创建失败") - except Exception as e: - self.add_error_message(f"创建平面地形失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_terrain_dialog = False - - def _draw_script_dialog(self): - """绘制脚本创建对话框""" - if not self.show_script_dialog: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("创建脚本", self.show_script_dialog, flags) as window: - if not window: - self.show_script_dialog = False - return - - # 初始化参数 - if 'script_name' not in self.dialog_params: - self.dialog_params['script_name'] = "new_script" - if 'script_template' not in self.dialog_params: - self.dialog_params['script_template'] = 0 # 0=basic, 1=movement - - imgui.text("脚本参数设置") - imgui.separator() - - # 脚本名称输入 - changed, self.dialog_params['script_name'] = imgui.input_text("脚本名称", self.dialog_params['script_name'], 256) - - # 模板选择 - templates = ["基础模板", "移动模板"] - changed, self.dialog_params['script_template'] = imgui.combo("模板类型", self.dialog_params['script_template'], templates) - - imgui.separator() - - # 模板说明 - if self.dialog_params['script_template'] == 0: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "基础模板: 包含基本的脚本结构") - else: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "移动模板: 包含移动相关的基本功能") - - imgui.separator() - - # 按钮 - if imgui.button("创建"): - try: - script_name = self.dialog_params['script_name'] - if not script_name: - self.add_error_message("请输入脚本名称") - return - - template = "basic" if self.dialog_params['script_template'] == 0 else "movement" - - result = self.createScript(script_name, template) - if result: - self.add_success_message(f"脚本创建成功: {script_name}") - - # 如果启用了热重载,自动加载新脚本 - if self.hotReloadEnabled: - try: - self.loadScript(result) - self.add_info_message(f"脚本已自动加载: {script_name}") - except Exception as e: - self.add_warning_message(f"脚本自动加载失败: {str(e)}") - - self.show_script_dialog = False - else: - self.add_error_message("脚本创建失败") - except Exception as e: - self.add_error_message(f"创建脚本失败: {str(e)}") - - imgui.same_line() - if imgui.button("取消"): - self.show_script_dialog = False - - def _draw_script_browser(self): - """绘制脚本文件浏览器""" - if not self.show_script_browser: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("选择脚本文件", self.show_script_browser, flags) as window: - if not window: - self.show_script_browser = False - return - - imgui.text("选择脚本文件") - imgui.separator() - - # 导航按钮 - if imgui.button("上级目录"): - parent_path = os.path.dirname(self.script_browser_current_path) - if parent_path != self.script_browser_current_path: - self.script_browser_current_path = parent_path - self._refresh_script_browser() - - imgui.same_line() - if imgui.button("脚本目录"): - # 切换到脚本目录 - if hasattr(self, 'script_manager') and self.script_manager: - scripts_dir = self.script_manager.scripts_directory - if os.path.exists(scripts_dir): - self.script_browser_current_path = scripts_dir - self._refresh_script_browser() - - imgui.same_line() - if imgui.button("当前目录"): - self.script_browser_current_path = os.getcwd() - self._refresh_script_browser() - - imgui.separator() - - # 当前路径显示 - imgui.text("当前路径:") - imgui.same_line() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.script_browser_current_path) - - imgui.separator() - - # 文件列表 - if imgui.begin_child("script_file_list", (580, 300)): - for item in self.script_browser_items: - if item['is_dir']: - # 目录 - imgui.text_colored((0.3, 0.8, 1.0, 1.0), f"📁 {item['name']}") - if imgui.is_item_clicked(): - self.script_browser_current_path = item['path'] - self._refresh_script_browser() - else: - # Python文件 - imgui.text(f"📄 {item['name']}") - if imgui.is_item_clicked(): - self.script_browser_selected_path = item['path'] - imgui.end_child() - - imgui.separator() - - # 选中的文件信息 - if self.script_browser_selected_path and os.path.exists(self.script_browser_selected_path): - file_size = os.path.getsize(self.script_browser_selected_path) - imgui.text(f"文件大小: {file_size / 1024:.2f} KB") - - file_ext = os.path.splitext(self.script_browser_selected_path)[1].lower() - if file_ext == '.py': - imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ Python脚本文件") - else: - imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不是Python脚本文件") - else: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的Python脚本文件") - - imgui.separator() - - # 按钮 - can_load = (self.script_browser_selected_path and - os.path.exists(self.script_browser_selected_path) and - os.path.splitext(self.script_browser_selected_path)[1].lower() == '.py') - - if can_load: - if imgui.button("加载脚本"): - try: - result = self.loadScript(self.script_browser_selected_path) - if result: - script_name = os.path.basename(self.script_browser_selected_path) - self.add_success_message(f"脚本加载成功: {script_name}") - self.show_script_browser = False - else: - self.add_error_message("脚本加载失败") - except Exception as e: - self.add_error_message(f"加载脚本失败: {str(e)}") - else: - imgui.push_style_var(imgui.StyleVar_.alpha, 0.5) - imgui.button("加载脚本") - imgui.pop_style_var() - - imgui.same_line() - if imgui.button("取消"): - self.show_script_browser = False - self.script_browser_selected_path = "" - - def _refresh_script_browser(self): - """刷新脚本浏览器内容""" - try: - self.script_browser_items = [] - - if not os.path.exists(self.script_browser_current_path): - self.script_browser_current_path = os.getcwd() - - # 获取目录中的所有项目 - items = [] - try: - for item_name in os.listdir(self.script_browser_current_path): - item_path = os.path.join(self.script_browser_current_path, item_name) - - if os.path.isdir(item_path): - items.append({ - 'name': item_name, - 'path': item_path, - 'is_dir': True - }) - elif os.path.isfile(item_path): - file_ext = os.path.splitext(item_name)[1].lower() - if file_ext == '.py': # 只显示Python文件 - items.append({ - 'name': item_name, - 'path': item_path, - 'is_dir': False - }) - except PermissionError: - print(f"权限错误: 无法访问目录 {self.script_browser_current_path}") - - # 排序:目录在前,文件在后 - items.sort(key=lambda x: (not x['is_dir'], x['name'].lower())) - self.script_browser_items = items - - except Exception as e: - print(f"刷新脚本浏览器时出错: {e}") - self.script_browser_items = [] - - def _draw_heightmap_browser(self): - """绘制高度图文件浏览器""" - if not self.show_heightmap_browser: - return - - flags = (imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_collapse | - imgui.WindowFlags_.modal) - - with imgui_ctx.begin("选择高度图文件", self.show_heightmap_browser, flags) as window: - if not window: - self.show_heightmap_browser = False - return - - imgui.text("选择高度图文件") - imgui.separator() - - # 导航按钮 - if imgui.button("上级目录"): - parent_path = os.path.dirname(self.heightmap_browser_current_path) - if parent_path != self.heightmap_browser_current_path: - self.heightmap_browser_current_path = parent_path - self._refresh_heightmap_browser() - - imgui.same_line() - if imgui.button("主目录"): - self.heightmap_browser_current_path = os.getcwd() - self._refresh_heightmap_browser() - - imgui.same_line() - if imgui.button("当前目录"): - self.heightmap_browser_current_path = os.getcwd() - self._refresh_heightmap_browser() - - imgui.separator() - - # 当前路径显示 - imgui.text("当前路径:") - imgui.same_line() - imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.heightmap_browser_current_path) - - imgui.separator() - - # 文件列表 - if imgui.begin_child("file_list", (580, 300)): - for item in self.heightmap_browser_items: - if item['is_dir']: - # 目录 - imgui.text_colored((0.3, 0.8, 1.0, 1.0), f"📁 {item['name']}") - if imgui.is_item_clicked(): - self.heightmap_browser_current_path = item['path'] - self._refresh_heightmap_browser() - else: - # 文件 - imgui.text(f"📄 {item['name']}") - if imgui.is_item_clicked(): - self.heightmap_browser_selected_path = item['path'] - self.heightmap_file_path = item['path'] - imgui.end_child() - - imgui.separator() - - # 支持的格式说明 - imgui.text("支持的文件格式:") - formats_text = ", ".join(self.supported_heightmap_formats) - imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text) - - imgui.separator() - - # 选中的文件信息 - if self.heightmap_file_path and os.path.exists(self.heightmap_file_path): - file_size = os.path.getsize(self.heightmap_file_path) - imgui.text(f"文件大小: {file_size / 1024:.2f} KB") - - file_ext = os.path.splitext(self.heightmap_file_path)[1].lower() - if file_ext in self.supported_heightmap_formats: - imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持") - else: - imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式") - else: - imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的高度图文件") - - imgui.separator() - - # 按钮 - can_import = (self.heightmap_file_path and - os.path.exists(self.heightmap_file_path) and - os.path.splitext(self.heightmap_file_path)[1].lower() in self.supported_heightmap_formats) - - if can_import: - if imgui.button("创建地形"): - try: - # 使用默认缩放参数创建地形 - scale = (1.0, 1.0, 10.0) # X, Y, Z缩放 - result = self.createTerrainFromHeightMap(self.heightmap_file_path, scale) - if result: - self.add_success_message("高度图地形创建成功") - self.show_heightmap_browser = False - else: - self.add_error_message("高度图地形创建失败") - except Exception as e: - self.add_error_message(f"创建高度图地形失败: {str(e)}") - else: - imgui.push_style_var(imgui.StyleVar_.alpha, 0.5) - imgui.button("创建地形") - imgui.pop_style_var() - - imgui.same_line() - if imgui.button("取消"): - self.show_heightmap_browser = False - self.heightmap_file_path = "" - - def _refresh_heightmap_browser(self): - """刷新高度图浏览器内容""" - try: - self.heightmap_browser_items = [] - - if not os.path.exists(self.heightmap_browser_current_path): - self.heightmap_browser_current_path = os.getcwd() - - # 获取目录中的所有项目 - items = [] - try: - for item_name in os.listdir(self.heightmap_browser_current_path): - item_path = os.path.join(self.heightmap_browser_current_path, item_name) - - if os.path.isdir(item_path): - items.append({ - 'name': item_name, - 'path': item_path, - 'is_dir': True - }) - elif os.path.isfile(item_path): - file_ext = os.path.splitext(item_name)[1].lower() - if file_ext in self.supported_heightmap_formats: - items.append({ - 'name': item_name, - 'path': item_path, - 'is_dir': False - }) - except PermissionError: - print(f"权限错误: 无法访问目录 {self.heightmap_browser_current_path}") - - # 排序:目录在前,文件在后 - items.sort(key=lambda x: (not x['is_dir'], x['name'].lower())) - self.heightmap_browser_items = items - - except Exception as e: - print(f"刷新高度图浏览器时出错: {e}") - self.heightmap_browser_items = [] - - # ==================== 导入功能实现 ==================== - - def _on_import_model(self): - """处理导入模型菜单项""" - self.add_info_message("打开导入模型对话框") - self.show_import_dialog = True - - def _import_model(self): - """导入模型的具体实现""" - try: - if not self.import_file_path: - self.add_error_message("请选择要导入的文件") - return - - if not os.path.exists(self.import_file_path): - self.add_error_message(f"文件不存在: {self.import_file_path}") - return - - # 检查文件格式 - file_ext = os.path.splitext(self.import_file_path)[1].lower() - if file_ext not in self.supported_formats: - self.add_error_message(f"不支持的文件格式: {file_ext}") - return - - # 调用场景管理器导入模型 - if hasattr(self, 'scene_manager') and self.scene_manager: - self.add_info_message(f"正在导入模型: {os.path.basename(self.import_file_path)}") - - # 导入模型 - model_node = self.scene_manager.importModel(self.import_file_path) - - if model_node: - # 添加材质处理确保颜色正常 - if hasattr(self.scene_manager, 'processMaterials'): - self.scene_manager.processMaterials(model_node) - self.add_info_message("已应用默认材质") - - # 额外的材质处理,确保颜色正确显示 - try: - # 强制刷新模型显示 - model_node.clearMaterial() - model_node.clearTexture() - - # 重新应用材质 - if hasattr(self.scene_manager, 'processMaterials'): - self.scene_manager.processMaterials(model_node) - - # 设置默认的基础颜色(如果模型没有颜色) - try: - color = model_node.getColor() - if color and len(color) >= 4 and color == (1, 1, 1, 1): # 默认白色 - model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰 - elif not color: # 如果没有颜色 - model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰 - except: - # 如果getColor失败,直接设置默认颜色 - model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰 - - except Exception as e: - self.add_warning_message(f"材质处理警告: {e}") - - # 设置模型位置 - model_node.setPos(0, 0, 0) - - # 添加到场景管理器的模型列表 - if hasattr(self.scene_manager, 'models'): - self.scene_manager.models.append(model_node) - - # 选中新导入的模型 - if hasattr(self, 'selection') and self.selection: - self.selection.selectNode(model_node) - - self.add_success_message(f"模型导入成功: {os.path.basename(self.import_file_path)}") - else: - self.add_error_message("模型导入失败") - else: - self.add_error_message("场景管理器未初始化") - - except Exception as e: - self.add_error_message(f"导入模型失败: {e}") - - # 清空导入路径 - self.import_file_path = "" - + def _get_current_collision_shape_type(self, *args, **kwargs): + return self.property_helpers._get_current_collision_shape_type(*args, **kwargs) + + def _get_collision_position_offset(self, *args, **kwargs): + return self.property_helpers._get_collision_position_offset(*args, **kwargs) + + def _is_collision_visible(self, *args, **kwargs): + return self.property_helpers._is_collision_visible(*args, **kwargs) + + def _add_collision_to_node(self, *args, **kwargs): + return self.property_helpers._add_collision_to_node(*args, **kwargs) + + def _remove_collision_from_node(self, *args, **kwargs): + return self.property_helpers._remove_collision_from_node(*args, **kwargs) + + def _toggle_collision_visibility(self, *args, **kwargs): + return self.property_helpers._toggle_collision_visibility(*args, **kwargs) + + def _update_collision_position(self, *args, **kwargs): + return self.property_helpers._update_collision_position(*args, **kwargs) + + def _get_shape_type_from_name(self, *args, **kwargs): + return self.property_helpers._get_shape_type_from_name(*args, **kwargs) + + def _get_sphere_radius(self, *args, **kwargs): + return self.property_helpers._get_sphere_radius(*args, **kwargs) + + def _update_sphere_radius(self, *args, **kwargs): + return self.property_helpers._update_sphere_radius(*args, **kwargs) + + def _get_box_size(self, *args, **kwargs): + return self.property_helpers._get_box_size(*args, **kwargs) + + def _update_box_size(self, *args, **kwargs): + return self.property_helpers._update_box_size(*args, **kwargs) + + def _get_capsule_radius(self, *args, **kwargs): + return self.property_helpers._get_capsule_radius(*args, **kwargs) + + def _update_capsule_radius(self, *args, **kwargs): + return self.property_helpers._update_capsule_radius(*args, **kwargs) + + def _get_capsule_height(self, *args, **kwargs): + return self.property_helpers._get_capsule_height(*args, **kwargs) + + def _update_capsule_height(self, *args, **kwargs): + return self.property_helpers._update_capsule_height(*args, **kwargs) + + def _get_plane_normal(self, *args, **kwargs): + return self.property_helpers._get_plane_normal(*args, **kwargs) + + def _update_plane_normal(self, *args, **kwargs): + return self.property_helpers._update_plane_normal(*args, **kwargs) + + def _manual_collision_detection(self, *args, **kwargs): + return self.property_helpers._manual_collision_detection(*args, **kwargs) + + def _update_node_name(self, *args, **kwargs): + return self.property_helpers._update_node_name(*args, **kwargs) + + def _get_material_base_color(self, *args, **kwargs): + return self.property_helpers._get_material_base_color(*args, **kwargs) + + def _update_material_base_color(self, *args, **kwargs): + return self.property_helpers._update_material_base_color(*args, **kwargs) + + def _update_material_roughness(self, *args, **kwargs): + return self.property_helpers._update_material_roughness(*args, **kwargs) + + def _update_material_metallic(self, *args, **kwargs): + return self.property_helpers._update_material_metallic(*args, **kwargs) + + def _update_material_ior(self, *args, **kwargs): + return self.property_helpers._update_material_ior(*args, **kwargs) + + def _apply_material_preset(self, *args, **kwargs): + return self.property_helpers._apply_material_preset(*args, **kwargs) + + def _apply_material_to_node(self, *args, **kwargs): + return self.property_helpers._apply_material_to_node(*args, **kwargs) + + def _reset_material(self, *args, **kwargs): + return self.property_helpers._reset_material(*args, **kwargs) + + def _select_texture_for_material(self, *args, **kwargs): + return self.property_helpers._select_texture_for_material(*args, **kwargs) + + def _apply_texture_to_material(self, *args, **kwargs): + return self.property_helpers._apply_texture_to_material(*args, **kwargs) + + def _clear_all_textures(self, *args, **kwargs): + return self.property_helpers._clear_all_textures(*args, **kwargs) + + def _display_current_textures(self, *args, **kwargs): + return self.property_helpers._display_current_textures(*args, **kwargs) + + def _update_shading_model(self, *args, **kwargs): + return self.property_helpers._update_shading_model(*args, **kwargs) + + def _update_transparency(self, *args, **kwargs): + return self.property_helpers._update_transparency(*args, **kwargs) + + def _draw_texture_file_dialog(self, *args, **kwargs): + return self.property_helpers._draw_texture_file_dialog(*args, **kwargs) + + def start_transform_monitoring(self, *args, **kwargs): + return self.property_helpers.start_transform_monitoring(*args, **kwargs) + + def stop_transform_monitoring(self, *args, **kwargs): + return self.property_helpers.stop_transform_monitoring(*args, **kwargs) + + def _update_last_transform_values(self, *args, **kwargs): + return self.property_helpers._update_last_transform_values(*args, **kwargs) + + def _check_transform_changes(self, *args, **kwargs): + return self.property_helpers._check_transform_changes(*args, **kwargs) + + def update_transform_monitoring(self, *args, **kwargs): + return self.property_helpers.update_transform_monitoring(*args, **kwargs) + + def show_color_picker(self, *args, **kwargs): + return self.property_helpers.show_color_picker(*args, **kwargs) + + def _draw_color_picker(self, *args, **kwargs): + return self.property_helpers._draw_color_picker(*args, **kwargs) + + def _apply_color_selection(self, *args, **kwargs): + return self.property_helpers._apply_color_selection(*args, **kwargs) + + def _draw_color_button(self, *args, **kwargs): + return self.property_helpers._draw_color_button(*args, **kwargs) + + def _refresh_available_fonts(self, *args, **kwargs): + return self.property_helpers._refresh_available_fonts(*args, **kwargs) + + def show_font_selector(self, *args, **kwargs): + return self.property_helpers.show_font_selector(*args, **kwargs) + + def _draw_font_selector(self, *args, **kwargs): + return self.property_helpers._draw_font_selector(*args, **kwargs) + + def _apply_font_selection(self, *args, **kwargs): + return self.property_helpers._apply_font_selection(*args, **kwargs) + + def _draw_font_selector_button(self, *args, **kwargs): + return self.property_helpers._draw_font_selector_button(*args, **kwargs) + def _draw_console(self, *args, **kwargs): + return self.script_panels._draw_console(*args, **kwargs) + + def _draw_script_panel(self, *args, **kwargs): + return self.script_panels._draw_script_panel(*args, **kwargs) + + def _draw_script_status_group(self, *args, **kwargs): + return self.script_panels._draw_script_status_group(*args, **kwargs) + + def _draw_create_script_group(self, *args, **kwargs): + return self.script_panels._draw_create_script_group(*args, **kwargs) + + def _draw_available_scripts_group(self, *args, **kwargs): + return self.script_panels._draw_available_scripts_group(*args, **kwargs) + + def _draw_script_mounting_group(self, *args, **kwargs): + return self.script_panels._draw_script_mounting_group(*args, **kwargs) + + + def _toggle_hot_reload(self, *args, **kwargs): + return self.app_actions._toggle_hot_reload(*args, **kwargs) + + def _create_new_script(self, *args, **kwargs): + return self.app_actions._create_new_script(*args, **kwargs) + + def _refresh_scripts_list(self, *args, **kwargs): + return self.app_actions._refresh_scripts_list(*args, **kwargs) + + def _reload_all_scripts(self, *args, **kwargs): + return self.app_actions._reload_all_scripts(*args, **kwargs) + + def _on_script_selected(self, *args, **kwargs): + return self.app_actions._on_script_selected(*args, **kwargs) + + def _edit_script(self, *args, **kwargs): + return self.app_actions._edit_script(*args, **kwargs) + + def _mount_script_to_selected(self, *args, **kwargs): + return self.app_actions._mount_script_to_selected(*args, **kwargs) + + def _unmount_script_from_selected(self, *args, **kwargs): + return self.app_actions._unmount_script_from_selected(*args, **kwargs) + + def _on_new_project(self, *args, **kwargs): + return self.app_actions._on_new_project(*args, **kwargs) + + def _on_open_project(self, *args, **kwargs): + return self.app_actions._on_open_project(*args, **kwargs) + + def _on_save_project(self, *args, **kwargs): + return self.app_actions._on_save_project(*args, **kwargs) + + def _on_save_as_project(self, *args, **kwargs): + return self.app_actions._on_save_as_project(*args, **kwargs) + + def _on_exit(self, *args, **kwargs): + return self.app_actions._on_exit(*args, **kwargs) + + def _on_ctrl_pressed(self, *args, **kwargs): + return self.app_actions._on_ctrl_pressed(*args, **kwargs) + + def _on_ctrl_released(self, *args, **kwargs): + return self.app_actions._on_ctrl_released(*args, **kwargs) + + def _on_alt_pressed(self, *args, **kwargs): + return self.app_actions._on_alt_pressed(*args, **kwargs) + + def _on_alt_released(self, *args, **kwargs): + return self.app_actions._on_alt_released(*args, **kwargs) + + def _on_n_pressed(self, *args, **kwargs): + return self.app_actions._on_n_pressed(*args, **kwargs) + + def _on_o_pressed(self, *args, **kwargs): + return self.app_actions._on_o_pressed(*args, **kwargs) + + def _on_f4_pressed(self, *args, **kwargs): + return self.app_actions._on_f4_pressed(*args, **kwargs) + + def _on_delete_pressed(self, *args, **kwargs): + return self.app_actions._on_delete_pressed(*args, **kwargs) + + def _on_escape_pressed(self, *args, **kwargs): + return self.app_actions._on_escape_pressed(*args, **kwargs) + + def _on_wheel_up(self, *args, **kwargs): + return self.app_actions._on_wheel_up(*args, **kwargs) + + def _on_wheel_down(self, *args, **kwargs): + return self.app_actions._on_wheel_down(*args, **kwargs) + + def _is_mouse_over_imgui(self, *args, **kwargs): + return self.app_actions._is_mouse_over_imgui(*args, **kwargs) + + def processImGuiMouseClick(self, *args, **kwargs): + return self.app_actions.processImGuiMouseClick(*args, **kwargs) + + def add_message(self, *args, **kwargs): + return self.app_actions.add_message(*args, **kwargs) + + def add_success_message(self, *args, **kwargs): + return self.app_actions.add_success_message(*args, **kwargs) + + def add_error_message(self, *args, **kwargs): + return self.app_actions.add_error_message(*args, **kwargs) + + def add_warning_message(self, *args, **kwargs): + return self.app_actions.add_warning_message(*args, **kwargs) + + def add_info_message(self, *args, **kwargs): + return self.app_actions.add_info_message(*args, **kwargs) + + def _on_undo(self, *args, **kwargs): + return self.app_actions._on_undo(*args, **kwargs) + + def _on_redo(self, *args, **kwargs): + return self.app_actions._on_redo(*args, **kwargs) + + def _on_copy(self, *args, **kwargs): + return self.app_actions._on_copy(*args, **kwargs) + + def _on_cut(self, *args, **kwargs): + return self.app_actions._on_cut(*args, **kwargs) + + def _on_paste(self, *args, **kwargs): + return self.app_actions._on_paste(*args, **kwargs) + + def _on_delete(self, *args, **kwargs): + return self.app_actions._on_delete(*args, **kwargs) + + def _delete_node(self, *args, **kwargs): + return self.app_actions._delete_node(*args, **kwargs) + + def _perform_node_cleanup(self, *args, **kwargs): + return self.app_actions._perform_node_cleanup(*args, **kwargs) + + def _create_new_project(self, *args, **kwargs): + return self.app_actions._create_new_project(*args, **kwargs) + + def _open_project_path(self, *args, **kwargs): + return self.app_actions._open_project_path(*args, **kwargs) + + def _save_project_impl(self, *args, **kwargs): + return self.app_actions._save_project_impl(*args, **kwargs) + + def _open_project_impl(self, *args, **kwargs): + return self.app_actions._open_project_impl(*args, **kwargs) + + def _create_new_project_impl(self, *args, **kwargs): + return self.app_actions._create_new_project_impl(*args, **kwargs) + + def _update_window_title(self, *args, **kwargs): + return self.app_actions._update_window_title(*args, **kwargs) + + def _import_model_for_runtime(self, *args, **kwargs): + return self.app_actions._import_model_for_runtime(*args, **kwargs) + + def _on_import_model(self, *args, **kwargs): + return self.app_actions._on_import_model(*args, **kwargs) + + def _import_model(self, *args, **kwargs): + return self.app_actions._import_model(*args, **kwargs) + def _draw_new_project_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_new_project_dialog(*args, **kwargs) + + def _draw_open_project_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_open_project_dialog(*args, **kwargs) + + def _draw_path_browser(self, *args, **kwargs): + return self.dialog_panels._draw_path_browser(*args, **kwargs) + + def _draw_import_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_import_dialog(*args, **kwargs) + + def _refresh_path_browser(self, *args, **kwargs): + return self.dialog_panels._refresh_path_browser(*args, **kwargs) + + def _apply_selected_path(self, *args, **kwargs): + return self.dialog_panels._apply_selected_path(*args, **kwargs) + + def _draw_gui_button_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_gui_button_dialog(*args, **kwargs) + + def _draw_gui_label_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_gui_label_dialog(*args, **kwargs) + + def _draw_gui_entry_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_gui_entry_dialog(*args, **kwargs) + + def _draw_gui_image_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_gui_image_dialog(*args, **kwargs) + + def _draw_3d_text_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_3d_text_dialog(*args, **kwargs) + + def _draw_3d_image_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_3d_image_dialog(*args, **kwargs) + + def _draw_video_screen_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_video_screen_dialog(*args, **kwargs) + + def _draw_2d_video_screen_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_2d_video_screen_dialog(*args, **kwargs) + + def _draw_spherical_video_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_spherical_video_dialog(*args, **kwargs) + + def _draw_virtual_screen_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_virtual_screen_dialog(*args, **kwargs) + + def _draw_spot_light_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_spot_light_dialog(*args, **kwargs) + + def _draw_point_light_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_point_light_dialog(*args, **kwargs) + + def _draw_terrain_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_terrain_dialog(*args, **kwargs) + + def _draw_script_dialog(self, *args, **kwargs): + return self.dialog_panels._draw_script_dialog(*args, **kwargs) + + def _draw_script_browser(self, *args, **kwargs): + return self.dialog_panels._draw_script_browser(*args, **kwargs) + + def _refresh_script_browser(self, *args, **kwargs): + return self.dialog_panels._refresh_script_browser(*args, **kwargs) + + def _draw_heightmap_browser(self, *args, **kwargs): + return self.dialog_panels._draw_heightmap_browser(*args, **kwargs) + + def _refresh_heightmap_browser(self, *args, **kwargs): + return self.dialog_panels._refresh_heightmap_browser(*args, **kwargs) def setup_drag_drop_support(self): """设置拖拽支持""" try: @@ -7206,7 +1400,7 @@ class MyWorld(CoreWorld): return False # 导入模型 - model_node = self.scene_manager.importModel(file_path) + model_node = self._import_model_for_runtime(file_path) if model_node: # 应用材质确保颜色正常 @@ -7228,787 +1422,136 @@ class MyWorld(CoreWorld): self.add_message("error", f"导入模型时发生错误: {str(e)}") return False - def _draw_drag_drop_interface(self): - """绘制拖拽界面""" - # 检查资源管理器的拖拽状态 - if self.resource_manager.is_dragging(): - self.is_dragging = True - self.dragged_files = self.resource_manager.get_dragged_files() - self.show_drag_overlay = True - - # 绘制拖拽覆盖层 - if self.show_drag_overlay: - self._draw_drag_overlay() - - # 检查是否有拖拽的文件需要处理 - if self.is_dragging and self.dragged_files: - # 显示拖拽状态 - self._draw_drag_status() - - # 检查是否释放鼠标(结束拖拽) - if imgui.is_mouse_released(0): - self._handle_drag_drop_completion() - - def _handle_drag_drop_completion(self): - """处理拖拽完成""" - # 检查是否在3D视图中释放 - mouse_pos = imgui.get_mouse_pos() - viewport = imgui.get_main_viewport() - - # 简单检查:如果不在任何ImGui窗口上,则认为是在3D视图中 - if not imgui.is_any_window_hovered(): - # 导入支持的3D模型文件 - imported_count = 0 - for file_path in self.dragged_files: - if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: - try: - self.scene_manager.importModel(str(file_path)) - self.add_success_message(f"成功导入模型: {file_path.name}") - imported_count += 1 - except Exception as e: - self.add_error_message(f"导入模型失败 {file_path.name}: {e}") - - if imported_count > 0: - self.add_success_message(f"共导入 {imported_count} 个模型") - - # 清除拖拽状态 - self.is_dragging = False - self.dragged_files.clear() - self.show_drag_overlay = False - self.resource_manager.clear_drag() - - def _draw_drag_overlay(self): - """绘制拖拽覆盖层""" - viewport = imgui.get_main_viewport() - imgui.set_next_window_pos((0, 0)) - imgui.set_next_window_size(viewport.work_size) - - flags = ( - imgui.WindowFlags_.no_title_bar | - imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_move | - imgui.WindowFlags_.no_scrollbar | - imgui.WindowFlags_.no_saved_settings | - imgui.WindowFlags_.no_background | - imgui.WindowFlags_.no_focus_on_appearing - ) - - imgui.begin("##DragOverlay", True, flags) - - # 绘制半透明背景 - draw_list = imgui.get_window_draw_list() - draw_list.add_rect_filled( - (0, 0), viewport.work_size, - imgui.get_color_u32((0, 0, 0, 0.1)) - ) - - # 绘制提示文本 - text_size = imgui.calc_text_size("释放以导入文件") - text_pos = ( - (viewport.work_size.x - text_size.x) / 2, - (viewport.work_size.y - text_size.y) / 2 - ) - - draw_list.add_text( - text_pos, - imgui.get_color_u32((1, 1, 1, 1)), - "释放以导入文件" - ) - - imgui.end() - - def _draw_drag_status(self): - """绘制拖拽状态""" - viewport = imgui.get_main_viewport() - - # 在右下角显示拖拽状态 - imgui.set_next_window_pos( - (viewport.work_size.x - 300, viewport.work_size.y - 150), - imgui.Cond_.first_use_ever - ) - - flags = ( - imgui.WindowFlags_.no_title_bar | - imgui.WindowFlags_.no_resize | - imgui.WindowFlags_.no_move | - imgui.WindowFlags_.no_scrollbar | - imgui.WindowFlags_.no_saved_settings - ) - - with imgui_ctx.begin("拖拽状态", True, flags): - imgui.text("拖拽的文件:") - for file_path in self.dragged_files: - filename = os.path.basename(file_path) - imgui.text(f" • {filename}") - - imgui.separator() - - if imgui.button("导入所有文件"): - self.process_dragged_files() - - imgui.same_line() - if imgui.button("取消"): - self.clear_dragged_files() - - def _draw_context_menus(self): - """绘制右键菜单""" - # 节点右键菜单 - if hasattr(self, '_context_menu_node') and self._context_menu_node: - imgui.open_popup("节点右键菜单") - self._context_menu_node = None - - if imgui.begin_popup("节点右键菜单"): - if imgui.menu_item("删除节点", "", False, True)[1]: - if hasattr(self, '_context_menu_target') and self._context_menu_target: - self._delete_node(self._context_menu_target) - imgui.close_current_popup() - - if imgui.menu_item("重命名", "", False, True)[1]: - self._renaming_node = True - imgui.close_current_popup() - - imgui.separator() - - if imgui.menu_item("复制", "", False, True)[1]: - if hasattr(self, '_context_menu_target') and self._context_menu_target: - self._copy_node(self._context_menu_target) - imgui.close_current_popup() - - if imgui.menu_item("聚焦", "", False, True)[1]: - if hasattr(self, '_context_menu_target') and self._context_menu_target: - if hasattr(self, 'selection') and self.selection: - self.selection.updateSelection(self._context_menu_target) - self.selection.focusCameraOnSelectedNodeAdvanced() - imgui.close_current_popup() - - imgui.end_popup() - - # 重命名对话框 - if hasattr(self, '_renaming_node') and self._renaming_node: - imgui.open_popup("重命名节点") - if not hasattr(self, '_rename_buffer'): - self._rename_buffer = "" - if hasattr(self, '_context_menu_target') and self._context_menu_target: - self._rename_buffer = self._context_menu_target.getName() or "" - - if imgui.begin_popup("重命名节点"): - changed, new_name = imgui.input_text("新名称", self._rename_buffer, 256) - if changed: - self._rename_buffer = new_name - - if imgui.button("确定"): - if hasattr(self, '_context_menu_target') and self._context_menu_target: - self._context_menu_target.setName(self._rename_buffer) - self._renaming_node = False - imgui.close_current_popup() - - imgui.same_line() - if imgui.button("取消"): - self._renaming_node = False - imgui.close_current_popup() - - imgui.end_popup() - - def _delete_node_simple(self, node): - """删除节点 - 简化版本""" - if not node or node.isEmpty(): - return - - # 从场景管理器中删除 - if hasattr(self, 'scene_manager') and self.scene_manager: - if hasattr(self.scene_manager, 'models') and node in self.scene_manager.models: - self.scene_manager.models.remove(node) - - # 从GUI管理器中删除 - if hasattr(self, 'gui_manager') and self.gui_manager: - gui_element = None - if hasattr(node, 'getPythonTag'): - gui_element = node.getPythonTag('gui_element') - if gui_element and hasattr(self.gui_manager, 'gui_elements'): - if gui_element in self.gui_manager.gui_elements: - self.gui_manager.gui_elements.remove(gui_element) - - # 使用主删除方法 - self._delete_node(node) - - # 获取节点名称(在删除之前) - node_name = node.getName() if not node.isEmpty() else '未命名节点' - - # 删除节点本身 - node.removeNode() - - # 清除选择 - if hasattr(self, 'selection') and self.selection: - if self.selection.selectedNode == node: - self.selection.clearSelection() - - # 添加成功消息 - self.add_success_message(f"已删除节点: {node_name}") - - def _copy_node(self, node): - """复制节点""" - if not node or node.isEmpty(): - return - - # 这里可以实现节点复制逻辑 - # 暂时只显示消息 - self.add_info_message(f"复制功能暂未实现: {node.getName() or '未命名节点'}") - - # ==================== 创建功能实现 ==================== - - def _on_create_empty_object(self): - """创建空对象""" - try: - result = self.createEmptyObject() - if result: - self.add_success_message("空对象创建成功") - else: - self.add_error_message("空对象创建失败") - return result - except Exception as e: - self.add_error_message(f"创建空对象失败: {str(e)}") - - def _on_create_cube(self): - """创建立方体""" - try: - result = self.createCube() - if result: - self.add_success_message("立方体创建成功") - else: - self.add_error_message("立方体创建失败") - return result - except Exception as e: - self.add_error_message(f"创建立方体失败: {str(e)}") - - def _on_create_sphere(self): - """创建球体""" - try: - result = self.createSphere() - if result: - self.add_success_message("球体创建成功") - else: - self.add_error_message("球体创建失败") - return result - except Exception as e: - self.add_error_message(f"创建球体失败: {str(e)}") - - def _on_create_cylinder(self): - """创建圆柱体""" - try: - result = self.createCylinder() - if result: - self.add_success_message("圆柱体创建成功") - else: - self.add_error_message("圆柱体创建失败") - return result - except Exception as e: - self.add_error_message(f"创建圆柱体失败: {str(e)}") - - def _on_create_plane(self): - """创建平面""" - try: - result = self.createPlane() - if result: - self.add_success_message("平面创建成功") - else: - self.add_error_message("平面创建失败") - return result - except Exception as e: - self.add_error_message(f"创建平面失败: {str(e)}") - - def _on_create_3d_text(self): - """创建3D文本""" - self.show_3d_text_dialog = True - - def _on_create_3d_image(self): - """创建3D图片""" - self.show_3d_image_dialog = True - - def _on_create_gui_button(self): - """创建GUI按钮""" - self.show_gui_button_dialog = True - - def _on_create_gui_label(self): - """创建GUI标签""" - self.show_gui_label_dialog = True - - def _on_create_gui_entry(self): - """创建GUI输入框""" - self.show_gui_entry_dialog = True - - def _on_create_gui_image(self): - """创建GUI图片""" - self.show_gui_image_dialog = True - - def _on_create_video_screen(self): - """创建视频屏幕""" - self.show_video_screen_dialog = True - - def _on_create_2d_video_screen(self): - """创建2D视频屏幕""" - self.show_2d_video_screen_dialog = True - - def _on_create_spherical_video(self): - """创建球形视频""" - self.show_spherical_video_dialog = True - - def _on_create_virtual_screen(self): - """创建虚拟屏幕""" - self.show_virtual_screen_dialog = True - - def _on_create_spot_light(self): - """创建聚光灯""" - self.show_spot_light_dialog = True - - def _on_create_point_light(self): - """创建点光源""" - self.show_point_light_dialog = True - - def _on_create_flat_terrain(self): - """创建平面地形""" - self.show_terrain_dialog = True - - def _on_create_heightmap_terrain(self): - """从高度图创建地形""" - self.show_heightmap_browser = True - - def _on_create_script(self): - """创建脚本""" - self.show_script_dialog = True - - def _on_load_script(self): - """加载脚本文件""" - self.show_script_browser = True - - def _on_reload_all_scripts(self): - """重载所有脚本""" - try: - if hasattr(self, 'script_manager') and self.script_manager: - self.script_manager.reloadAllScripts() - self.add_success_message("所有脚本重载成功") - else: - self.add_error_message("脚本管理器未初始化") - except Exception as e: - self.add_error_message(f"重载脚本失败: {str(e)}") - - def _on_open_scripts_manager(self): - """打开脚本管理器""" - self.showScriptPanel = True - self.add_info_message("脚本管理器已打开") - - def _on_create_2d_sample_panel(self): - """创建2D示例面板""" - try: - result = self.create2DSamplePanel() - if result: - self.add_success_message("2D示例面板创建成功") - else: - self.add_error_message("2D示例面板创建失败") - except Exception as e: - self.add_error_message(f"创建2D示例面板失败: {str(e)}") - - def _on_create_3d_sample_panel(self): - """创建3D实例面板""" - try: - result = self.create3DSamplePanel() - if result: - self.add_success_message("3D实例面板创建成功") - else: - self.add_error_message("3D实例面板创建失败") - except Exception as e: - self.add_error_message(f"创建3D实例面板失败: {str(e)}") - - def _on_create_web_panel(self): - """创建Web面板""" - try: - result = self.createWebPanel() - if result: - self.add_success_message("Web面板创建成功") - else: - self.add_error_message("Web面板创建失败") - except Exception as e: - self.add_error_message(f"创建Web面板失败: {str(e)}") - - # ==================== 3D对象和GUI创建方法 ==================== - - def createEmptyObject(self): - """创建空对象""" - try: - from panda3d.core import NodePath - # 创建一个空节点 - empty_node = NodePath("EmptyObject") - empty_node.reparentTo(self.render) - empty_node.setPos(0, 0, 0) - - # 添加到场景管理器 - if hasattr(self, 'scene_manager') and self.scene_manager: - self.scene_manager.models.append(empty_node) - - # 更新场景树 - if hasattr(self, 'updateSceneTree'): - self.updateSceneTree() - - print("✓ 空对象创建成功") - return empty_node - except Exception as e: - print(f"✗ 创建空对象失败: {e}") - return None - - def create3DText(self, pos=(0, 0, 0), text="3D Text", scale=1.0): - """创建3D文本""" - try: - from panda3d.core import TextNode, NodePath - - # 创建文本节点 - text_node = TextNode("3DText") - text_node.setText(text) - text_node.setAlign(TextNode.ACenter) - text_node.setTextColor(1, 1, 1, 1) # 白色 - - # 设置中文字体 - chinese_font = self._get_chinese_font() - if chinese_font: - text_node.setFont(chinese_font) - - # 创建节点路径并设置位置 - text_np = NodePath(text_node) - text_np.reparentTo(self.render) - text_np.setPos(pos) - text_np.setScale(scale) - - # 添加到场景管理器 - if hasattr(self, 'scene_manager') and self.scene_manager: - self.scene_manager.models.append(text_np) - - # 更新场景树 - if hasattr(self, 'updateSceneTree'): - self.updateSceneTree() - - print(f"✓ 3D文本创建成功: {text}") - return text_np - except Exception as e: - print(f"✗ 创建3D文本失败: {e}") - return None - - def create3DImage(self, pos=(0, 0, 0), image_path="", size=(1, 1)): - """创建3D图片""" - try: - from panda3d.core import CardMaker, NodePath - - if not image_path or not os.path.exists(image_path): - print("✗ 图片文件不存在") - return None - - # 创建卡片几何体 - card_maker = CardMaker("3DImage") - card_maker.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - card = card_maker.generate() - - # 创建节点路径 - image_np = NodePath(card) - image_np.reparentTo(self.render) - image_np.setPos(pos) - - # 加载纹理 - from panda3d.core import Texture, Filename - tex = self.loader.loadTexture(Filename.fromOsSpecific(image_path)) - if tex: - image_np.setTexture(tex, 1) - else: - print("✗ 加载纹理失败") - image_np.removeNode() - return None - - # 添加到场景管理器 - if hasattr(self, 'scene_manager') and self.scene_manager: - self.scene_manager.models.append(image_np) - - # 更新场景树 - if hasattr(self, 'updateSceneTree'): - self.updateSceneTree() - - print(f"✓ 3D图片创建成功: {os.path.basename(image_path)}") - return image_np - except Exception as e: - print(f"✗ 创建3D图片失败: {e}") - return None - - def createCube(self, pos=(0, 0, 0), size=1.0): - """创建立方体""" - try: - # 尝试使用Panda3D的内置几何体 - from panda3d.core import NodePath, GeomNode, Geom, GeomVertexFormat, GeomVertexData, GeomVertexWriter, GeomTriangles, GeomPoints - from panda3d.core import Vec3, Vec4, RenderState, ShadeModelAttrib - - # 创建顶点数据格式 - format = GeomVertexFormat.getV3n3cpt2() - vdata = GeomVertexData('cube', format, Geom.UHStatic) - vdata.setNumRows(24) - - vertex = GeomVertexWriter(vdata, 'vertex') - normal = GeomVertexWriter(vdata, 'normal') - color = GeomVertexWriter(vdata, 'color') - - # 立方体的8个顶点 - s = size / 2 - vertices = [ - (-s, -s, -s), (s, -s, -s), (s, s, -s), (-s, s, -s), # 底面 - (-s, -s, s), (s, -s, s), (s, s, s), (-s, s, s) # 顶面 - ] - - # 立方体的6个面,每个面4个顶点 - faces = [ - (0, 1, 2, 3), # 底面 - (4, 7, 6, 5), # 顶面 - (0, 4, 5, 1), # 前面 - (2, 6, 7, 3), # 后面 - (0, 3, 7, 4), # 左面 - (1, 5, 6, 2) # 右面 - ] - - # 法线 - normals = [ - (0, 0, -1), (0, 0, 1), (0, -1, 0), (0, 1, 0), (-1, 0, 0), (1, 0, 0) - ] - - # 添加顶点数据 - for face_idx, face in enumerate(faces): - n = normals[face_idx] - for vertex_idx in face: - v = vertices[vertex_idx] - vertex.addData3f(*v) - normal.addData3f(*n) - color.addData4f(0.8, 0.8, 0.8, 1.0) # 灰色 - - # 创建几何体 - geom = Geom(vdata) - - # 添加三角形 - for face_idx in range(6): - base = face_idx * 4 - # 每个面分成2个三角形 - tri = GeomTriangles(Geom.UHStatic) - tri.addConsecutiveVertices(base, 3) - tri.closePrimitive() - - tri2 = GeomTriangles(Geom.UHStatic) - tri2.addVertices(base + 2, base + 3, base) - tri2.closePrimitive() - - geom.addPrimitive(tri) - geom.addPrimitive(tri2) - - # 创建节点 - geom_node = GeomNode('cube') - geom_node.addGeom(geom) - - cube = NodePath(geom_node) - cube.reparentTo(self.render) - cube.setPos(pos) - - # 设置渲染状态 - state = RenderState.make(ShadeModelAttrib.make(ShadeModelAttrib.MSmooth)) - cube.set_state(state) - - # 添加到场景管理器 - if hasattr(self, 'scene_manager') and self.scene_manager: - self.scene_manager.models.append(cube) - - # 更新场景树 - if hasattr(self, 'updateSceneTree'): - self.updateSceneTree() - - print("✓ 立方体创建成功") - return cube - except Exception as e: - print(f"✗ 创建立方体失败: {e}") - return None - - def createSphere(self, pos=(0, 0, 0), radius=1.0): - """创建球体""" - try: - from panda3d.core import CardMaker, NodePath - - # 创建一个简单的球体(使用多个卡片近似) - sphere_root = NodePath("Sphere") - - # 创建球体的多个面来近似球形 - num_segments = 6 - for i in range(num_segments): - angle1 = (i / num_segments) * 360 - - # 创建球体片段 - segment_maker = CardMaker(f"SphereSegment{i}") - segment_maker.setFrame(-radius, radius, -radius, radius) - segment = NodePath(segment_maker.generate()) - segment.reparentTo(sphere_root) - - # 设置位置和旋转来形成球体 - segment.setPos(0, 0, 0) - segment.setH(angle1) - segment.setP(angle1/2) # 倾斜角度 - segment.setScale(radius) - - # 设置颜色 - from panda3d.core import Vec4 - sphere_root.setColor(Vec4(0.8, 0.6, 0.4, 1.0)) # 棕色 - - sphere_root.reparentTo(self.render) - sphere_root.setPos(pos) - - # 添加到场景管理器 - if hasattr(self, 'scene_manager') and self.scene_manager: - self.scene_manager.models.append(sphere_root) - - # 更新场景树 - if hasattr(self, 'updateSceneTree'): - self.updateSceneTree() - - print("✓ 球体创建成功") - return sphere_root - except Exception as e: - print(f"✗ 创建球体失败: {e}") - return None - - def createCylinder(self, pos=(0, 0, 0), radius=1.0, height=2.0): - """创建圆柱体""" - try: - import math - from panda3d.core import CardMaker, NodePath - - # 创建圆柱体 - cylinder_root = NodePath("Cylinder") - - # 创建圆柱体的多个侧面 - num_segments = 12 - for i in range(num_segments): - angle1 = (i / num_segments) * 360 - angle2 = ((i + 1) / num_segments) * 360 - - # 计算圆柱体侧面的位置 - x1 = radius * math.cos(math.radians(angle1)) - y1 = radius * math.sin(math.radians(angle1)) - - # 创建圆柱体侧面 - segment_maker = CardMaker(f"CylinderSegment{i}") - segment = NodePath(segment_maker.generate()) - segment.reparentTo(cylinder_root) - - # 设置位置和旋转 - segment.setPos(x1, y1, 0) - segment.setH(angle1) - segment.setScale(0.5, height, 1) # 调整宽度和高度 - - # 设置颜色 - from panda3d.core import Vec4 - cylinder_root.setColor(Vec4(0.4, 0.8, 0.4, 1.0)) # 绿色 - - cylinder_root.reparentTo(self.render) - cylinder_root.setPos(pos) - - # 添加到场景管理器 - if hasattr(self, 'scene_manager') and self.scene_manager: - self.scene_manager.models.append(cylinder_root) - - # 更新场景树 - if hasattr(self, 'updateSceneTree'): - self.updateSceneTree() - - print("✓ 圆柱体创建成功") - return cylinder_root - except Exception as e: - print(f"✗ 创建圆柱体失败: {e}") - return None - - def createPlane(self, pos=(0, 0, 0), size=(1, 1)): - """创建平面""" - try: - from panda3d.core import CardMaker, NodePath - - # 创建平面 - card_maker = CardMaker("Plane") - card_maker.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - plane = NodePath(card_maker.generate()) - - plane.reparentTo(self.render) - plane.setPos(pos) - - # 添加到场景管理器 - if hasattr(self, 'scene_manager') and self.scene_manager: - self.scene_manager.models.append(plane) - - # 更新场景树 - if hasattr(self, 'updateSceneTree'): - self.updateSceneTree() - - print("✓ 平面创建成功") - return plane - except Exception as e: - print(f"✗ 创建平面失败: {e}") - return None - - def create2DSamplePanel(self): - """创建2D示例面板""" - try: - from core.InfoPanelManager import createSampleInfoPanel - result = createSampleInfoPanel(self.render) - return result - except Exception as e: - print(f"创建2D示例面板失败: {e}") - return None - - def create3DSamplePanel(self): - """创建3D实例面板""" - try: - if hasattr(self, 'info_panel_manager') and self.info_panel_manager: - # 创建3D信息面板 - panel_id = f"3d_sample_{int(time.time())}" - result = self.info_panel_manager.create3DInfoPanel( - panel_id=panel_id, - position=(0, 0, 2), - size=(1.0, 0.6), - bg_color=(0.15, 0.25, 0.35, 0.95), - border_color=(0.3, 0.5, 0.7, 1.0), - title_color=(0.7, 0.9, 1.0, 1.0), - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 添加示例内容 - if result: - sample_data = { - "标题": "3D信息面板", - "状态": "运行中", - "创建时间": time.strftime("%Y-%m-%d %H:%M:%S"), - "位置": f"X:0, Y:0, Z:2" - } - self.info_panel_manager.updatePanelContent(panel_id, content=sample_data) - - return result - return None - except Exception as e: - print(f"创建3D示例面板失败: {e}") - return None - - def createWebPanel(self, url="https://www.example.com"): - """创建Web面板""" - try: - if hasattr(self, 'info_panel_manager') and self.info_panel_manager: - panel_id = f"web_panel_{int(time.time())}" - result = self.info_panel_manager.createHTTPInfoPanel( - panel_id=panel_id, - url=url, - position=(0.8, 0.0), - size=(0.35, 0.4), - update_interval=5.0 # 每5秒更新一次 - ) - return result - return None - except Exception as e: - print(f"创建Web面板失败: {e}") - return None - - # ==================== GUI创建方法 ==================== - + def _draw_drag_drop_interface(self, *args, **kwargs): + return self.interaction_panels._draw_drag_drop_interface(*args, **kwargs) + + def _handle_drag_drop_completion(self, *args, **kwargs): + return self.interaction_panels._handle_drag_drop_completion(*args, **kwargs) + + def _draw_drag_overlay(self, *args, **kwargs): + return self.interaction_panels._draw_drag_overlay(*args, **kwargs) + + def _draw_drag_status(self, *args, **kwargs): + return self.interaction_panels._draw_drag_status(*args, **kwargs) + + def _draw_context_menus(self, *args, **kwargs): + return self.interaction_panels._draw_context_menus(*args, **kwargs) + + def _delete_node_simple(self, *args, **kwargs): + return self.interaction_panels._delete_node_simple(*args, **kwargs) + + def _copy_node(self, *args, **kwargs): + return self.interaction_panels._copy_node(*args, **kwargs) + + + def _on_create_empty_object(self, *args, **kwargs): + return self.create_actions._on_create_empty_object(*args, **kwargs) + + def _on_create_cube(self, *args, **kwargs): + return self.create_actions._on_create_cube(*args, **kwargs) + + def _on_create_sphere(self, *args, **kwargs): + return self.create_actions._on_create_sphere(*args, **kwargs) + + def _on_create_cylinder(self, *args, **kwargs): + return self.create_actions._on_create_cylinder(*args, **kwargs) + + def _on_create_plane(self, *args, **kwargs): + return self.create_actions._on_create_plane(*args, **kwargs) + + def _on_create_3d_text(self, *args, **kwargs): + return self.create_actions._on_create_3d_text(*args, **kwargs) + + def _on_create_3d_image(self, *args, **kwargs): + return self.create_actions._on_create_3d_image(*args, **kwargs) + + def _on_create_gui_button(self, *args, **kwargs): + return self.create_actions._on_create_gui_button(*args, **kwargs) + + def _on_create_gui_label(self, *args, **kwargs): + return self.create_actions._on_create_gui_label(*args, **kwargs) + + def _on_create_gui_entry(self, *args, **kwargs): + return self.create_actions._on_create_gui_entry(*args, **kwargs) + + def _on_create_gui_image(self, *args, **kwargs): + return self.create_actions._on_create_gui_image(*args, **kwargs) + + def _on_create_video_screen(self, *args, **kwargs): + return self.create_actions._on_create_video_screen(*args, **kwargs) + + def _on_create_2d_video_screen(self, *args, **kwargs): + return self.create_actions._on_create_2d_video_screen(*args, **kwargs) + + def _on_create_spherical_video(self, *args, **kwargs): + return self.create_actions._on_create_spherical_video(*args, **kwargs) + + def _on_create_virtual_screen(self, *args, **kwargs): + return self.create_actions._on_create_virtual_screen(*args, **kwargs) + + def _on_create_spot_light(self, *args, **kwargs): + return self.create_actions._on_create_spot_light(*args, **kwargs) + + def _on_create_point_light(self, *args, **kwargs): + return self.create_actions._on_create_point_light(*args, **kwargs) + + def _on_create_flat_terrain(self, *args, **kwargs): + return self.create_actions._on_create_flat_terrain(*args, **kwargs) + + def _on_create_heightmap_terrain(self, *args, **kwargs): + return self.create_actions._on_create_heightmap_terrain(*args, **kwargs) + + def _on_create_script(self, *args, **kwargs): + return self.create_actions._on_create_script(*args, **kwargs) + + def _on_load_script(self, *args, **kwargs): + return self.create_actions._on_load_script(*args, **kwargs) + + def _on_reload_all_scripts(self, *args, **kwargs): + return self.create_actions._on_reload_all_scripts(*args, **kwargs) + + def _on_open_scripts_manager(self, *args, **kwargs): + return self.create_actions._on_open_scripts_manager(*args, **kwargs) + + def _on_create_2d_sample_panel(self, *args, **kwargs): + return self.create_actions._on_create_2d_sample_panel(*args, **kwargs) + + def _on_create_3d_sample_panel(self, *args, **kwargs): + return self.create_actions._on_create_3d_sample_panel(*args, **kwargs) + + def _on_create_web_panel(self, *args, **kwargs): + return self.create_actions._on_create_web_panel(*args, **kwargs) + + + def createEmptyObject(self, *args, **kwargs): + return self.object_factory.createEmptyObject(*args, **kwargs) + + def create3DText(self, *args, **kwargs): + return self.object_factory.create3DText(*args, **kwargs) + + def create3DImage(self, *args, **kwargs): + return self.object_factory.create3DImage(*args, **kwargs) + + def createCube(self, *args, **kwargs): + return self.object_factory.createCube(*args, **kwargs) + + def createSphere(self, *args, **kwargs): + return self.object_factory.createSphere(*args, **kwargs) + + def createCylinder(self, *args, **kwargs): + return self.object_factory.createCylinder(*args, **kwargs) + + def createPlane(self, *args, **kwargs): + return self.object_factory.createPlane(*args, **kwargs) + + def create2DSamplePanel(self, *args, **kwargs): + return self.object_factory.create2DSamplePanel(*args, **kwargs) + + def create3DSamplePanel(self, *args, **kwargs): + return self.object_factory.create3DSamplePanel(*args, **kwargs) + + def createWebPanel(self, *args, **kwargs): + return self.object_factory.createWebPanel(*args, **kwargs) def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): """创建2D GUI按钮""" try: diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2bb40970..69fd9bb3 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -99,3 +99,4 @@ xdg==5 xkit==0.0.0 zipp==1.0.0 openvr==2.2.0 +imgui-bundle diff --git a/ssbo_component/README.md b/ssbo_component/README.md new file mode 100644 index 00000000..e820624d --- /dev/null +++ b/ssbo_component/README.md @@ -0,0 +1,67 @@ + +# SSBO Editor Component Usage Guide + +This directory contains a modular SSBO Scene Editor component for Panda3D RenderPipeline. + +## 📁 File Structure + +- `ssbo_editor.py`: Main component class `SSBOEditor`. Handles ImGui, RP integration, and input. +- `ssbo_controller.py`: Core logic for object ID baking and SSBO matrix packing. +- `effects/ssbo_instancing.yaml`: The custom shader effect for RenderPipeline (includes Normal Matrix fix). +- `demo_component.py`: Example usage script. + +## 🚀 How to Integrate + +### 1. Prerequisites + +Ensure your project has: +- `RenderPipeline` setup +- `imgui_bundle` and `p3dimgui` installed +- `panda3d` (version 1.10.15+) + +### 2. Basic Setup (See demo_component.py) + +```python +from direct.showbase.ShowBase import ShowBase +from rpcore import RenderPipeline +from ssbo_component.ssbo_editor import SSBOEditor + +class MyApp(ShowBase): + def __init__(self): + # 1. Initialize RP + self.rp = RenderPipeline() + self.rp.pre_showbase_init() + super().__init__() + self.rp.create(self) + + # 2. Add SSBO Component + # Pass your ShowBase instance (self), the RP instance (self.rp), and your model path + self.editor = SSBOEditor( + base_app=self, + render_pipeline=self.rp, + model_path="path/to/your/model.glb", + font_path="path/to/chinese/font.ttc" # Optional + ) + +app = MyApp() +app.run() +``` + +### 3. Key Features + +- **SSBO Instancing**: Efficiently renders thousands of objects using hardware instancing. +- **Normal Correction**: Default shaders are patched to correctly handle non-uniform scaling (Inverse Transpose Normal Matrix). +- **ImGui Editor**: Built-in scene tree browsing, search, and selection. +- **Object Manipulation**: Select and move objects (Arrows + Z/X keys). +- **Shadow Integration**: Correctly injects SSBO data into RP shadow passes. +- **Motion Blur Support**: Computes per-object velocity for correct motion blur. + +### 4. Notes + +- **Model Preparation**: The input model should have a clear hierarchy. The component will flatten it but preserve logical objects for selection. +- **Effects Path**: The `ssbo_instancing.yaml` is loaded relative to the `ssbo_editor.py` file, so the directory structure inside `ssbo_component` should be preserved. +- **GPU Picking**: Basic GPU picking is implemented using shaders in `ssbo_component/shaders/`. These are loaded automatically relative to the component path. + +## ⚠️ Important for RenderPipeline + +If you encounter `ImportError: No module named 'rplibs.six.moves'`, ensure you apply the compatibility fix at the start of your main script (as seen in `demo_component.py`). diff --git a/ssbo_component/demo_component.py b/ssbo_component/demo_component.py new file mode 100644 index 00000000..09d8668f --- /dev/null +++ b/ssbo_component/demo_component.py @@ -0,0 +1,77 @@ +""" +SSBO Editor Component Demo +========================== +Demonstrates how to usage the SSBOEditor component in a new project. +""" +import sys +import os + +# ============================================================ +# [CRITICAL] RenderPipeline Python Compatibility Fix +# ============================================================ +if "d:/panda3d/RenderPipeline" not in sys.path: + sys.path.insert(0, "d:/panda3d/RenderPipeline") + +try: + import rplibs.six + import six + if not hasattr(rplibs.six, 'moves'): + rplibs.six.moves = six.moves + sys.modules['rplibs.six.moves'] = six.moves +except ImportError: + import six + import types + if "rplibs" not in sys.modules: + rplibs = types.ModuleType("rplibs") + sys.modules["rplibs"] = rplibs + if "rplibs.six" not in sys.modules: + rplibs.six = six + sys.modules["rplibs.six"] = six + sys.modules["rplibs.six.moves"] = six.moves + +# ============================================================ +# Main Application +# ============================================================ +from direct.showbase.ShowBase import ShowBase +from panda3d.core import loadPrcFileData, Filename +from rpcore import RenderPipeline + +# Import our new component +# Assuming d:/panda3d is in python path, or running from d:/panda3d +from ssbo_component.ssbo_editor import SSBOEditor + +loadPrcFileData("", "show-frame-rate-meter true") +loadPrcFileData("", "sync-video false") + +class MyGame(ShowBase): + def __init__(self): + # 1. RP Init + self.rp = RenderPipeline() + self.rp.pre_showbase_init() + super().__init__() + + # Position camera before RP create to avoid huge motion blur jump + self.cam.setPos(0, -500, 200) + self.cam.lookAt(0, 0, 0) + + self.rp.create(self) + self.rp.daytime_mgr.time = 0.415 + + # 2. Use the Component + # Point to your model + model_path = "d:/panda3d/panda3d/jyc.glb" + font_path = "d:/panda3d/font/msyh.ttc" + + print("Initializing SSBO Editor Component...") + self.editor = SSBOEditor( + base_app=self, + render_pipeline=self.rp, + model_path=model_path, + font_path=font_path + ) + + print("Demo ready.") + +if __name__ == "__main__": + app = MyGame() + app.run() diff --git a/ssbo_component/effects/ssbo_instancing.yaml b/ssbo_component/effects/ssbo_instancing.yaml new file mode 100644 index 00000000..74313aad --- /dev/null +++ b/ssbo_component/effects/ssbo_instancing.yaml @@ -0,0 +1,36 @@ +vertex: + inout: | + // SSBO Layout - Default column_major matches Panda3D's internal layout + layout(std430) buffer transforms { + mat4 transform_data[]; + }; + + // Input from Vertex Color (Baked ID) + in vec4 p3d_Color; + + transform: | + // Decode Local ID (p3d_Color stores Local ID within chunk) + int low = int(p3d_Color.r * 255.0 + 0.5); + int high = int(p3d_Color.g * 255.0 + 0.5); + int id = high * 256 + low; + id = clamp(id, 0, 65535); + + // Fetch Matrix from per-chunk SSBO + mat4 model = transform_data[id]; + + // Transform Position + vOutput.position = (model * p3d_Vertex).xyz; + + // Transform Normal + vOutput.normal = normalize(mat3(model) * p3d_Normal); + + post_transform: | + // Motion blur disabled for performance + +fragment: + inout: | + + + material: | + vec4 tex_color = texture(p3d_Texture0, vOutput.texcoord); + if (tex_color.a < 0.5) discard; diff --git a/ssbo_component/shaders/pick_id.frag b/ssbo_component/shaders/pick_id.frag new file mode 100644 index 00000000..efd297ab --- /dev/null +++ b/ssbo_component/shaders/pick_id.frag @@ -0,0 +1,21 @@ +#version 430 + +// Input from vertex shader +flat in int v_instance_id; + +// Output: Object ID encoded as RGB color +out vec4 fragColor; + +void main() { + // Encode instance ID into RGB channels + // R = low byte, G = high byte, B = 0, A = 1 + int low_byte = v_instance_id & 0xFF; + int high_byte = (v_instance_id >> 8) & 0xFF; + + fragColor = vec4( + float(low_byte) / 255.0, + float(high_byte) / 255.0, + 0.0, + 1.0 + ); +} diff --git a/ssbo_component/shaders/pick_id.vert b/ssbo_component/shaders/pick_id.vert new file mode 100644 index 00000000..fa901fe7 --- /dev/null +++ b/ssbo_component/shaders/pick_id.vert @@ -0,0 +1,21 @@ +#version 430 + +// No SSBO — vertices are already in world space. +// Just read vertex color for object ID and output it. + +in vec4 p3d_Vertex; +in vec4 p3d_Color; + +uniform mat4 p3d_ModelViewProjectionMatrix; + +flat out int v_instance_id; + +void main() { + // Decode object ID from vertex color R/G channels + int low_byte = int(p3d_Color.r * 255.0 + 0.5); + int high_byte = int(p3d_Color.g * 255.0 + 0.5); + v_instance_id = low_byte + (high_byte << 8); + + // Standard transform — vertices already in world space + gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; +} diff --git a/ssbo_component/ssbo_controller.py b/ssbo_component/ssbo_controller.py new file mode 100644 index 00000000..d6d97b4e --- /dev/null +++ b/ssbo_component/ssbo_controller.py @@ -0,0 +1,550 @@ + +from panda3d.core import ( + GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter, + InternalName, Vec3, Vec4, LMatrix4f, ShaderBuffer, GeomEnums, + BoundingSphere, NodePath, GeomNode, Texture, SamplerState, + Point3, BoundingBox, Quat +) +import struct +import time + +class ObjectController: + """ + 物体控制器 (No Custom Shader Mode) + ==================================== + Uses RP's default rendering (no rp.set_effect) for maximum FPS. + Vertex colors baked for picking. Movement modifies vertex data directly. + Stores original vertex positions per object for rotation/translation. + """ + def __init__(self): + self.name_to_ids = {} + self.id_to_name = {} + self.key_to_node = {} + self.node_list = [] + self.display_names = {} + self.global_transforms = [] # Original transforms (for center/position) + + self.id_to_chunk = {} # global_id -> (chunk_key, local_idx) + self.chunks = {} # chunk_key -> dict with 'node' key + + # Vertex index: local_id -> list of (geom_node_np, geom_idx, [row_indices]) + self.vertex_index = {} + + # Original vertex positions: local_id -> list of (Vec3,) matching row order + self.original_positions = {} + + # Current position offsets: local_id -> Vec3 delta + self.position_offsets = {} + self.local_to_global_id = {} + self.local_transform_state = {} + self.local_transform_base_positions = {} + self.virtual_tree = None + self.virtual_tree_meta = None + + self.model = None + self.chunk_node = None # Single chunk node + + def bake_ids_and_collect(self, model): + """ + Bake IDs into vertex colors, flatten, then build vertex index. + + NO transform reset — vertices keep world-space positions. + NO SSBO — uses RP default rendering. + """ + t0 = time.time() + + geom_nodes = list(model.find_all_matches("**/+GeomNode")) + print(f"[控制器] 找到 {len(geom_nodes)} 个 GeomNode") + + self.name_to_ids = {} + self.id_to_name = {} + self.key_to_node = {} + self.node_list = [] + self.display_names = {} + self.global_transforms = [] + self.id_to_chunk = {} + self.chunks = {} + self.vertex_index = {} + self.original_positions = {} + self.position_offsets = {} + self.local_to_global_id = {} + self.local_transform_state = {} + self.local_transform_base_positions = {} + self.virtual_tree = None + self.virtual_tree_meta = None + + global_id_counter = 0 + chunk_key = model.get_name() or "default" + + # No chunk wrapper — flatten directly on model (same as load_jyc_flatten.py) + self.chunk_node = model + self.chunks[chunk_key] = {'node': model, 'base_id': 0} + + # Flatten hierarchy + for np in geom_nodes: + np.wrt_reparent_to(model) + + local_idx = 0 + + for np in geom_nodes: + gnode = np.node() + + if gnode.get_num_parents() > 1: + parent = np.get_parent() + if not parent.is_empty(): + new_np = np.copy_to(parent) + np.detach_node() + np = new_np + gnode = np.node() + + unique_key = str(np) + display_name = np.get_name() or f"Object_{global_id_counter}" + + if unique_key not in self.name_to_ids: + self.name_to_ids[unique_key] = [] + self.key_to_node[unique_key] = np + self.node_list.append(unique_key) + self.display_names[unique_key] = display_name + + # Save original transform + mat_double = np.get_mat() + original_transform = LMatrix4f(mat_double) + + for i in range(gnode.get_num_geoms()): + geom = gnode.modify_geom(i) + vdata = geom.modify_vertex_data() + + if not vdata.has_column("color"): + new_format = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4()) + vdata.set_format(new_format) + + # Encode Local ID in R/G + low = local_idx % 256 + high = local_idx // 256 + r = low / 255.0 + g = high / 255.0 + + writer = GeomVertexWriter(vdata, InternalName.make("color")) + for row in range(vdata.get_num_rows()): + writer.set_row(row) + writer.set_data4f(r, g, 0.0, 1.0) + + self.global_transforms.append(original_transform) + self.id_to_chunk[global_id_counter] = (chunk_key, local_idx) + self.name_to_ids[unique_key].append(global_id_counter) + self.id_to_name[global_id_counter] = unique_key + self.local_to_global_id[local_idx] = global_id_counter + self.position_offsets[local_idx] = Vec3(0, 0, 0) + + global_id_counter += 1 + local_idx += 1 + + # DO NOT reset transform — keep world-space positions + + # Flatten directly on model — NO set_final, allows per-geom frustum culling + model.flatten_strong() + + t1 = time.time() + print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms") + + # Build vertex index AFTER flatten + self._build_vertex_index(model) + self._init_local_transform_state() + self.build_virtual_hierarchy() + + t2 = time.time() + print(f"[控制器] Vertex index built in {(t2-t1)*1000:.0f}ms, " + f"{len(self.vertex_index)} unique IDs indexed") + + self.model = model + self.node_list.sort() + return global_id_counter + + def build_virtual_hierarchy(self): + """Build a readonly virtual tree from node_list path keys.""" + root = { + "name": "", + "path": "", + "children": {}, + "leaf_key": None, + "display_name": "", + } + max_depth = 0 + leaf_count = 0 + + for key in self.node_list: + if not key: + continue + parts = [p for p in str(key).split("/") if p] + if not parts: + continue + max_depth = max(max_depth, len(parts)) + cursor = root + path_acc = "" + for i, part in enumerate(parts): + path_acc = f"{path_acc}/{part}" if path_acc else part + child = cursor["children"].get(part) + if child is None: + child = { + "name": part, + "path": path_acc, + "children": {}, + "leaf_key": None, + "display_name": part, + } + cursor["children"][part] = child + cursor = child + if i == len(parts) - 1: + cursor["leaf_key"] = key + cursor["display_name"] = self.display_names.get(key, part) + leaf_count += 1 + + self.virtual_tree = root + self.virtual_tree_meta = {"max_depth": max_depth, "leaf_count": leaf_count} + return root + + def get_virtual_hierarchy(self): + """Return cached virtual tree; build on demand.""" + if self.virtual_tree is None: + return self.build_virtual_hierarchy() + return self.virtual_tree + + def _build_vertex_index(self, chunk_root): + """ + After flatten, batch-read all vertex data with numpy to build: + local_id -> [(geom_node_np, geom_idx, row_indices_array)] + Also stores original vertex positions per object (as numpy arrays). + """ + import numpy as np + + for gn_np in chunk_root.find_all_matches("**/+GeomNode"): + gnode = gn_np.node() + for gi in range(gnode.get_num_geoms()): + geom = gnode.get_geom(gi) + vdata = geom.get_vertex_data() + num_rows = vdata.get_num_rows() + + if num_rows == 0: + continue + + # Find vertex and color column info + fmt = vdata.get_format() + + # Get position column + pos_col = fmt.get_column(InternalName.get_vertex()) + if pos_col is None: + continue + pos_array_idx = fmt.get_array_with(InternalName.get_vertex()) + pos_start = pos_col.get_start() + + # Get color column + color_col = fmt.get_column(InternalName.make("color")) + if color_col is None: + continue + color_array_idx = fmt.get_array_with(InternalName.make("color")) + color_start = color_col.get_start() + + # Read raw position array + pos_array_format = fmt.get_array(pos_array_idx) + pos_stride = pos_array_format.get_stride() + pos_handle = vdata.get_array(pos_array_idx).get_handle() + pos_raw = bytes(pos_handle.get_data()) + pos_buf = np.frombuffer(pos_raw, dtype=np.uint8).reshape(num_rows, pos_stride) + + # Extract xyz positions (3 floats starting at pos_start) + positions = np.ndarray((num_rows, 3), dtype=np.float32, + buffer=pos_buf[:, pos_start:pos_start+12].tobytes()) + + # Read raw color array + color_array_format = fmt.get_array(color_array_idx) + color_stride = color_array_format.get_stride() + + if color_array_idx == pos_array_idx: + color_buf = pos_buf + else: + color_handle = vdata.get_array(color_array_idx).get_handle() + color_raw = bytes(color_handle.get_data()) + color_buf = np.frombuffer(color_raw, dtype=np.uint8).reshape(num_rows, color_stride) + + # Decode color format to get ID + # Color can be stored as float32 RGBA or unorm8 RGBA + num_components = color_col.get_num_components() + component_bytes = color_col.get_component_bytes() + + if component_bytes == 4: # float32 per component + color_data = np.ndarray((num_rows, num_components), dtype=np.float32, + buffer=color_buf[:, color_start:color_start+num_components*4].tobytes()) + r_vals = (color_data[:, 0] * 255.0 + 0.5).astype(np.int32) + g_vals = (color_data[:, 1] * 255.0 + 0.5).astype(np.int32) + elif component_bytes == 1: # uint8 per component + color_bytes = color_buf[:, color_start:color_start+num_components].copy() + r_vals = color_bytes[:, 0].astype(np.int32) + g_vals = color_bytes[:, 1].astype(np.int32) + else: + # Fallback: skip this geom + continue + + local_ids = r_vals + (g_vals << 8) + + # Group rows by local_id using argsort (O(N log N) instead of O(N×K)) + sort_idx = np.argsort(local_ids) + sorted_ids = local_ids[sort_idx] + sorted_positions = positions[sort_idx] + + # Find group boundaries + boundaries = np.where(np.diff(sorted_ids) != 0)[0] + 1 + + # Split into groups + id_groups = np.split(sort_idx, boundaries) + pos_groups = np.split(sorted_positions, boundaries) + group_ids = sorted_ids[np.concatenate([[0], boundaries])] + + for k in range(len(group_ids)): + uid = int(group_ids[k]) + rows = id_groups[k] + pos = pos_groups[k] + + if uid not in self.vertex_index: + self.vertex_index[uid] = [] + self.original_positions[uid] = [] + + self.vertex_index[uid].append((gn_np, gi, rows)) + self.original_positions[uid].append(pos.copy()) + + def _init_local_transform_state(self): + """Initialize transform state for each local_idx after vertex index is ready.""" + self.local_transform_state = {} + self.local_transform_base_positions = {} + + for local_idx in self.vertex_index.keys(): + self.local_transform_base_positions[local_idx] = self.original_positions.get(local_idx, []) + self.local_transform_state[local_idx] = { + "offset": Vec3(0, 0, 0), + "quat": Quat.identQuat(), + "scale": Vec3(1, 1, 1), + "pivot": self.get_local_pivot(local_idx), + } + + def get_local_indices_from_global_ids(self, global_ids): + """Map global ids to unique local indices.""" + local_indices = [] + if not global_ids: + return local_indices + seen = set() + for global_id in global_ids: + mapping = self.id_to_chunk.get(global_id) + if not mapping: + continue + _, local_idx = mapping + if local_idx in seen: + continue + if local_idx not in self.vertex_index: + continue + seen.add(local_idx) + local_indices.append(local_idx) + return local_indices + + def get_local_pivot(self, local_idx): + """Get pivot for one local object (world-space center).""" + global_id = self.local_to_global_id.get(local_idx) + if global_id is None: + return Vec3(0, 0, 0) + return self.get_object_center(global_id) + + def get_selection_center(self, local_indices): + """Get center point for a multi-object selection.""" + if not local_indices: + return Vec3(0, 0, 0) + acc = Vec3(0, 0, 0) + valid = 0 + for local_idx in local_indices: + state = self.local_transform_state.get(local_idx) + if not state: + continue + acc += state.get("pivot", Vec3(0, 0, 0)) + state.get("offset", Vec3(0, 0, 0)) + valid += 1 + if valid == 0: + return Vec3(0, 0, 0) + return acc / float(valid) + + def begin_transform_session(self, local_indices): + """Create immutable baseline snapshot for one gizmo drag session.""" + if not local_indices: + return {"locals": {}} + + locals_snapshot = {} + for local_idx in local_indices: + base_state = self.local_transform_state.get(local_idx) + if not base_state: + continue + entries = self.vertex_index.get(local_idx, []) + base_positions = self.local_transform_base_positions.get(local_idx, []) + locals_snapshot[local_idx] = { + "offset": Vec3(base_state["offset"]), + "quat": Quat(base_state["quat"]), + "scale": Vec3(base_state["scale"]), + "pivot": Vec3(base_state["pivot"]), + "entries": entries, + "base_positions": base_positions, + } + return {"locals": locals_snapshot} + + def apply_transform_session(self, snapshot, delta_pos, delta_quat, delta_scale): + """Apply transform delta to all local indices in snapshot and rewrite vertices.""" + import numpy as np + + if not snapshot or "locals" not in snapshot: + return + if delta_pos is None: + delta_pos = Vec3(0, 0, 0) + if delta_quat is None: + delta_quat = Quat.identQuat() + if delta_scale is None: + delta_scale = Vec3(1, 1, 1) + + dscale = np.array([delta_scale.x, delta_scale.y, delta_scale.z], dtype=np.float32) + dpos = np.array([delta_pos.x, delta_pos.y, delta_pos.z], dtype=np.float32) + + for local_idx, local_data in snapshot["locals"].items(): + base_offset = local_data["offset"] + base_quat = local_data["quat"] + base_scale = local_data["scale"] + pivot = local_data["pivot"] + + final_offset = Vec3(base_offset) + delta_pos + final_quat = Quat(delta_quat * base_quat) + final_scale = Vec3( + base_scale.x * delta_scale.x, + base_scale.y * delta_scale.y, + base_scale.z * delta_scale.z, + ) + rot_mat = self._quat_to_np_mat3(final_quat) + + self.local_transform_state[local_idx]["offset"] = final_offset + self.local_transform_state[local_idx]["quat"] = final_quat + self.local_transform_state[local_idx]["scale"] = final_scale + self.position_offsets[local_idx] = final_offset + + pivot_np = np.array([pivot.x, pivot.y, pivot.z], dtype=np.float32) + base_s = np.array([base_scale.x, base_scale.y, base_scale.z], dtype=np.float32) + total_scale = base_s * dscale + total_offset = np.array([base_offset.x, base_offset.y, base_offset.z], dtype=np.float32) + dpos + + entries = local_data["entries"] + base_positions = local_data["base_positions"] + for i, (gn_np, gi, rows) in enumerate(entries): + if i >= len(base_positions): + continue + orig_pos = base_positions[i] + if orig_pos is None or len(orig_pos) == 0: + continue + centered = orig_pos - pivot_np + scaled = centered * total_scale + rotated = scaled @ rot_mat.T + new_pos = rotated + pivot_np + total_offset + + gnode = gn_np.node() + geom = gnode.modify_geom(gi) + vdata = geom.modify_vertex_data() + writer = GeomVertexWriter(vdata, "vertex") + + for j in range(len(rows)): + writer.set_row(int(rows[j])) + writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2])) + + def _quat_to_np_mat3(self, quat): + """Convert Panda3D Quat to 3x3 numpy rotation matrix.""" + import numpy as np + q = Quat(quat) + q.normalize() + w = float(q.getR()) + x = float(q.getI()) + y = float(q.getJ()) + z = float(q.getK()) + + xx = x * x + yy = y * y + zz = z * z + xy = x * y + xz = x * z + yz = y * z + wx = w * x + wy = w * y + wz = w * z + + return np.array([ + [1.0 - 2.0 * (yy + zz), 2.0 * (xy - wz), 2.0 * (xz + wy)], + [2.0 * (xy + wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz - wx)], + [2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - 2.0 * (xx + yy)], + ], dtype=np.float32) + + def create_ssbo(self): + """No SSBO needed — using RP default rendering.""" + return None + + def move_object(self, global_id, delta): + """ + Move an object by modifying vertex positions directly. + delta: Vec3 translation to apply. + Uses numpy for batch vertex updates. + """ + import numpy as np + + if global_id not in self.id_to_chunk: + return + + _, local_idx = self.id_to_chunk[global_id] + + if local_idx not in self.vertex_index: + return + + # Accumulate offset + self.position_offsets[local_idx] = self.position_offsets.get(local_idx, Vec3(0)) + delta + offset = self.position_offsets[local_idx] + offset_arr = np.array([offset.x, offset.y, offset.z], dtype=np.float32) + + # Update each (geom_node, geom_idx, rows) group + entries = self.vertex_index[local_idx] + originals = self.original_positions[local_idx] + + for i, (gn_np, gi, rows) in enumerate(entries): + orig_pos = originals[i] # numpy array (N, 3) + new_pos = orig_pos + offset_arr # vectorized add + + gnode = gn_np.node() + geom = gnode.modify_geom(gi) + vdata = geom.modify_vertex_data() + writer = GeomVertexWriter(vdata, "vertex") + + for j in range(len(rows)): + writer.set_row(int(rows[j])) + writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2])) + + def get_world_pos(self, global_id): + """Get current world position of an object.""" + if global_id not in self.id_to_chunk: + return Vec3(0, 0, 0) + _, local_idx = self.id_to_chunk[global_id] + + original_mat = self.global_transforms[global_id] + original_pos = original_mat.get_row3(3) + offset = self.position_offsets.get(local_idx, Vec3(0)) + + return Vec3(original_pos) + offset + + def get_object_center(self, global_id): + """Get the original center position of an object (for rotation pivot).""" + if global_id >= len(self.global_transforms): + return Vec3(0, 0, 0) + mat = self.global_transforms[global_id] + return Vec3(mat.get_row3(3)) + + def get_transform(self, global_id): + """Get original transform.""" + if global_id >= len(self.global_transforms): + return LMatrix4f.ident_mat() + return self.global_transforms[global_id] + + @property + def transforms(self): + return self.global_transforms diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py new file mode 100644 index 00000000..e82948f2 --- /dev/null +++ b/ssbo_component/ssbo_editor.py @@ -0,0 +1,617 @@ + +import sys +import os +import struct +import time +from panda3d.core import ( + Filename, loadPrcFileData, GeomVertexFormat, + GeomVertexWriter, InternalName, Shader, Texture, SamplerState, + Vec3, Vec4, Point2, Point3, LMatrix4f, ShaderBuffer, GeomEnums, OmniBoundingVolume, Quat, + TransparencyAttrib, BoundingSphere, NodePath, + GraphicsEngine, WindowProperties, FrameBufferProperties, + GraphicsPipe, GraphicsOutput, Camera, DisplayRegion, OrthographicLens, + BoundingBox +) + +import p3dimgui.backend as p3dimgui_backend +import p3dimgui.shaders as p3dimgui_shaders +from imgui_bundle import imgui +from rpcore.effect import Effect + +# Work around p3dimgui import-order issue where backend may import an unrelated +# top-level "shaders" module and miss these globals. +if not hasattr(p3dimgui_backend, "VERT_SHADER"): + p3dimgui_backend.VERT_SHADER = p3dimgui_shaders.VERT_SHADER +if not hasattr(p3dimgui_backend, "FRAG_SHADER"): + p3dimgui_backend.FRAG_SHADER = p3dimgui_shaders.FRAG_SHADER + +ImGuiBackend = p3dimgui_backend.ImGuiBackend + +from .ssbo_controller import ObjectController + +class SSBOEditor: + """ + SSBO Editor Component + ==================== + Encapsulates the SSBO rendering, ImGui editor, and interaction logic. + Can be integrated into any ShowBase application using RenderPipeline. + """ + + def __init__(self, base_app, render_pipeline, model_path=None, font_path=None): + self.base = base_app + self.rp = render_pipeline + self.controller = None + self.model = None + self.ssbo = None + self.font_path = font_path + # Picking resources may be created later when a model is loaded. + self.pick_buffer = None + self.pick_texture = None + self.pick_cam = None + self.pick_cam_np = None + self.pick_lens = None + + # Internal State + self.selected_name = None + self.selected_ids = [] + self.search_text = "" + self.last_search_text = None + self.filtered_nodes = [] + self.debug_mode = False + self.keys = {} + self._ssbo_transform_active = False + self._ssbo_selected_local_indices = [] + self._ssbo_transform_snapshot = None + self._ssbo_gizmo_proxy = None + self._ssbo_proxy_start = {"pos": None, "quat": None, "scale": None} + self._bound_transform_gizmo = None + + # Initialize ImGui Backend if not already present + if not hasattr(self.base, 'imgui_backend'): + print("[SSBOEditor] Initializing ImGui Backend...") + self.base.imgui_backend = ImGuiBackend() + + self.load_font() + + # Register Events + self.base.accept("imgui-new-frame", self.draw_imgui) + self.base.accept("f", self.focus_on_selected) + self.base.accept("d", self.toggle_debug) + self.base.accept("mouse1", self.on_mouse_click) + + # Register Input Tasks + for key in ['arrow_up', 'arrow_down', 'arrow_left', 'arrow_right', 'z', 'x']: + self.base.accept(key, self.keys.__setitem__, [key, True]) + self.base.accept(f"{key}-up", self.keys.__setitem__, [key, False]) + + # Add Tasks + self.base.taskMgr.add(self.update_task, "update_task") + + # Load Model if provided + if model_path: + self.load_model(model_path) + + def load_font(self): + """Load custom font for ImGui""" + io = imgui.get_io() + + # Load Chinese Glyph Ranges + glyph_ranges = None + try: + if hasattr(io.fonts, 'get_glyph_ranges_chinese_full'): + glyph_ranges = io.fonts.get_glyph_ranges_chinese_full() + elif hasattr(io.fonts, 'get_glyph_ranges_chinese_simplified_common'): + glyph_ranges = io.fonts.get_glyph_ranges_chinese_simplified_common() + except Exception as e: + print(f"[SSBOEditor] Warning: Could not get Chinese glyph ranges: {e}") + + try: + if self.font_path and os.path.exists(self.font_path): + io.fonts.clear() + # If glyph_ranges is None, it uses default (Basic Latin) + if glyph_ranges: + io.fonts.add_font_from_file_ttf(self.font_path, 18.0, glyph_ranges=glyph_ranges) + else: + io.fonts.add_font_from_file_ttf(self.font_path, 18.0) + else: + # Fallback to default or common font + default_font = os.path.join(os.path.dirname(os.path.dirname(__file__)), "font", "msyh.ttc") + if os.path.exists(default_font): + io.fonts.clear() + io.fonts.add_font_from_file_ttf(default_font, 18.0, glyph_ranges=glyph_ranges) + else: + io.fonts.clear() + io.fonts.add_font_default() + except Exception as e: + print(f"[SSBOEditor] Font load error: {e}") + io.fonts.clear() + io.fonts.add_font_default() + + def load_model(self, model_path): + """Load and process a model — NO custom shader, uses RP default rendering.""" + print(f"[SSBOEditor] Loading model: {model_path}") + fn = Filename.fromOsSpecific(model_path) + self.model = self.base.loader.loadModel(fn) + + self.controller = ObjectController() + count = self.controller.bake_ids_and_collect(self.model) + self._ssbo_transform_active = False + self._ssbo_selected_local_indices = [] + self._ssbo_transform_snapshot = None + self._cleanup_ssbo_proxy() + + self.model.reparent_to(self.base.render) + + # NO rp.set_effect() — use RP default rendering for max FPS + # NO SSBO creation — vertex positions are baked + + # Setup GPU Picking (uses simple vertex-color shader) + self.setup_gpu_picking() + + print(f"[SSBOEditor] Model loaded. Total objects: {count}") + + # No custom effect needed — RP default rendering for maximum FPS + + def _inject_ssbo_into_shadow_state(self, effect_path): + """Inject SSBO inputs into RP shadow tag state""" + try: + if not hasattr(self.rp.tag_mgr, 'containers'): return + + shadow_container = self.rp.tag_mgr.containers.get("shadow") + if not shadow_container: return + + tag_value = self.model.get_tag(shadow_container.tag_name) + if not tag_value: return + + effect = Effect.load(effect_path, {}) + if effect is None: return + + shadow_shader = effect.get_shader_obj("shadow") + if shadow_shader is None: return + + # Since inputs are now on Nodes (Chunks), we just need to ensure the shader is applied. + # extra_inputs is no longer needed if the inputs are on the nodes themselves? + # Wait, RP might override state. + # But specific shader inputs on NodePath have priority over State inputs usually? + # Let's try applying without extra inputs first. + + self.rp.tag_mgr.apply_state( + "shadow", self.model, shadow_shader, + tag_value, 65) + + print(f"[SSBO Shadow] Re-applied shadow state (tag='{tag_value}')") + except Exception as e: + print(f"[SSBO Shadow] Error injecting shadow state: {e}") + + def setup_gpu_picking(self): + """Setup GPU Picking (Basic implementation)""" + # ... (Buffer setup code remains same) ... + win_props = WindowProperties() + win_props.set_size(1, 1) + fb_props = FrameBufferProperties() + fb_props.set_rgba_bits(8, 8, 8, 8) + fb_props.set_depth_bits(16) + + self.pick_buffer = self.base.graphicsEngine.make_output( + self.base.pipe, "pick_buffer", -100, + fb_props, win_props, + GraphicsPipe.BF_refuse_window, + self.base.win.get_gsg(), self.base.win + ) + + if not self.pick_buffer: + print("[GPU Picking] Failed to create buffer!") + return + + self.pick_texture = Texture() + self.pick_texture.set_minfilter(Texture.FT_nearest) + self.pick_texture.set_magfilter(Texture.FT_nearest) + self.pick_buffer.add_render_texture(self.pick_texture, GraphicsOutput.RTM_copy_ram) + + self.pick_cam = Camera("pick_camera") + self.pick_cam_np = self.base.cam.attach_new_node(self.pick_cam) + self.pick_lens = self.base.camLens.make_copy() + self.pick_cam.set_lens(self.pick_lens) + + dr = self.pick_buffer.make_display_region() + dr.set_camera(self.pick_cam_np) + + # Load pick shader + current_dir = os.path.dirname(os.path.abspath(__file__)) + pick_vert = os.path.join(current_dir, "shaders", "pick_id.vert") + pick_frag = os.path.join(current_dir, "shaders", "pick_id.frag") + + pick_vert = Filename.fromOsSpecific(pick_vert).getFullpath() + pick_frag = Filename.fromOsSpecific(pick_frag).getFullpath() + + try: + pick_shader = Shader.load( + Shader.SL_GLSL, + pick_vert, + pick_frag + ) + self.pick_cam.set_scene(self.model) + initial_state = NodePath("initial") + initial_state.set_shader(pick_shader, 100) + # Remove global SSBO input, Chunks have their own inputs + # initial_state.set_shader_input("transforms", ssbo) + self.pick_cam.set_initial_state(initial_state.get_state()) + except Exception as e: + print(f"[GPU Picking] Warning: pick shaders failed to load: {e}") + print("Picking disabled.") + return + + self.pick_buffer.set_active(False) + self.pick_buffer.set_clear_color(Vec4(0, 0, 0, 0)) + self.pick_buffer.set_clear_color_active(True) + + def pick_object(self, mx, my): + if (not self.pick_buffer or not self.pick_texture or not self.pick_lens or + not self.controller or not self.model): + return False + + self.pick_lens.set_fov(0.1) + self.pick_lens.set_film_offset(0, 0) + self.pick_cam.set_lens(self.pick_lens) + + near_point = Point3() + far_point = Point3() + self.base.camLens.extrude(Point2(mx, my), near_point, far_point) + + self.pick_cam_np.set_pos(0, 0, 0) + self.pick_cam_np.look_at(far_point) + + self.pick_buffer.set_active(True) + self.base.graphicsEngine.render_frame() + self.pick_buffer.set_active(False) + self.base.graphicsEngine.extract_texture_data( + self.pick_texture, self.base.win.get_gsg() + ) + + ram_image = self.pick_texture.get_ram_image_as("RGBA") + if ram_image: + data = memoryview(ram_image) + if len(data) >= 4: + r, g, b, a = data[0], data[1], data[2], data[3] + if a > 0: + hit_id = r + (g << 8) + node_key = self.controller.id_to_name.get(hit_id) + if node_key: + print(f"[Pick] Hit: ID={hit_id} -> {node_key}") + self.select_node(node_key) + return True + + self.selected_name = None + self.selected_ids = [] + return False + + + def on_mouse_click(self): + io = imgui.get_io() + if io.want_capture_mouse: + return + if self.base.mouseWatcherNode.has_mouse(): + mpos = self.base.mouseWatcherNode.get_mouse() + # If clicking gizmo, skip SSBO pick. + if self._try_start_gizmo_drag(mpos.x, mpos.y): + return + prev_selected = self.selected_name + hit = self.pick_object(mpos.x, mpos.y) + # SSBO miss must clear current selection. + if not hit: + self._sync_selection_none() + # Always fallback to legacy ray pick when SSBO misses. + # This keeps scene selection usable if SSBO ID mapping is incomplete. + self._fallback_legacy_pick(mpos.x, mpos.y) + elif prev_selected != self.selected_name: + # Ensure selection visuals refresh when SSBO selection changes. + self._sync_selection_from_key(self.selected_name) + + def toggle_debug(self): + self.debug_mode = not self.debug_mode + + def clear_selection(self): + pass # No selection mask texture needed without custom shader + + def update_selection_mask(self): + pass # No selection mask texture needed without custom shader + + def select_node(self, key): + if not self.controller or key not in self.controller.name_to_ids: + return + self.selected_name = key + self.selected_ids = self.controller.name_to_ids.get(key, []) + self._sync_selection_from_key(key) + + def _sync_selection_from_key(self, key): + """Sync SSBO picked key to legacy SelectionSystem.""" + try: + if hasattr(self.base, "selection") and self.base.selection: + kind, target = self._resolve_ssbo_selection_target(key) + if kind == "proxy": + target_np = target + else: + target_np = target if target is not None else self.model + if target_np is None or target_np.isEmpty(): + target_np = self.model + self.base.selection.updateSelection(target_np) + except Exception as e: + print(f"[SSBOEditor] selection sync failed: {e}") + + def _sync_selection_none(self): + """Clear legacy SelectionSystem selection.""" + try: + self._ssbo_transform_active = False + self._ssbo_selected_local_indices = [] + self._ssbo_transform_snapshot = None + self._cleanup_ssbo_proxy() + if hasattr(self.base, "selection") and self.base.selection: + self.base.selection.updateSelection(None) + except Exception as e: + print(f"[SSBOEditor] clear selection sync failed: {e}") + + def bind_transform_gizmo(self, transform_gizmo): + """Bind TransformGizmo drag hooks so SSBO sub-object transforms can follow gizmo.""" + self._bound_transform_gizmo = transform_gizmo + if not transform_gizmo: + return + hooks = { + "move": { + "drag_start": [self._on_ssbo_gizmo_drag_start], + "drag_move": [self._on_ssbo_gizmo_drag_move], + "drag_end": [self._on_ssbo_gizmo_drag_end], + }, + "rotate": { + "drag_start": [self._on_ssbo_gizmo_drag_start], + "drag_move": [self._on_ssbo_gizmo_drag_move], + "drag_end": [self._on_ssbo_gizmo_drag_end], + }, + "scale": { + "drag_start": [self._on_ssbo_gizmo_drag_start], + "drag_move": [self._on_ssbo_gizmo_drag_move], + "drag_end": [self._on_ssbo_gizmo_drag_end], + }, + } + try: + if hasattr(transform_gizmo, "set_event_hooks"): + transform_gizmo.set_event_hooks(hooks, replace=False) + print("[SSBOEditor] TransformGizmo hooks bound") + except Exception as e: + print(f"[SSBOEditor] bind transform gizmo failed: {e}") + + def _resolve_ssbo_selection_target(self, key): + """Resolve selected SSBO key to proxy node (preferred) or regular node.""" + self._ssbo_transform_active = False + self._ssbo_transform_snapshot = None + self._ssbo_selected_local_indices = [] + + if not self.controller or not key: + return "node", self.model + global_ids = self.controller.name_to_ids.get(key, []) + local_indices = self.controller.get_local_indices_from_global_ids(global_ids) + self._ssbo_selected_local_indices = local_indices + if local_indices: + print(f"[SSBOEditor] selection locals={len(local_indices)} key={key}") + center = self.controller.get_selection_center(local_indices) + proxy = self._ensure_ssbo_proxy(center) + return "proxy", proxy + target_np = self.controller.key_to_node.get(key) + if target_np is None or target_np.isEmpty(): + target_np = self.model + return "node", target_np + + def _ensure_ssbo_proxy(self, center): + if self._ssbo_gizmo_proxy is None or self._ssbo_gizmo_proxy.isEmpty(): + self._ssbo_gizmo_proxy = self.base.render.attach_new_node("ssbo_transform_proxy") + self._ssbo_gizmo_proxy.setTag("is_ssbo_proxy", "1") + self._ssbo_gizmo_proxy.set_pos(center) + self._ssbo_gizmo_proxy.set_hpr(0, 0, 0) + self._ssbo_gizmo_proxy.set_scale(1, 1, 1) + return self._ssbo_gizmo_proxy + + def _cleanup_ssbo_proxy(self): + if self._ssbo_gizmo_proxy and not self._ssbo_gizmo_proxy.isEmpty(): + self._ssbo_gizmo_proxy.removeNode() + self._ssbo_gizmo_proxy = None + + def _on_ssbo_gizmo_drag_start(self, payload): + try: + target = payload.get("target") if payload else None + if not target or target != self._ssbo_gizmo_proxy: + self._ssbo_transform_active = False + return + if not self.controller or not self._ssbo_selected_local_indices: + self._ssbo_transform_active = False + return + self._ssbo_transform_snapshot = self.controller.begin_transform_session( + self._ssbo_selected_local_indices + ) + self._ssbo_proxy_start = { + "pos": Vec3(target.getPos(self.base.render)), + "quat": Quat(target.getQuat(self.base.render)), + "scale": Vec3(target.getScale()), + } + self._ssbo_transform_active = True + print(f"[SSBOEditor] drag_start locals={len(self._ssbo_selected_local_indices)}") + except Exception as e: + self._ssbo_transform_active = False + print(f"[SSBOEditor] drag_start bridge failed: {e}") + + def _on_ssbo_gizmo_drag_move(self, payload): + try: + if not self._ssbo_transform_active: + return + target = payload.get("target") if payload else None + if not target or target != self._ssbo_gizmo_proxy: + return + start_pos = self._ssbo_proxy_start.get("pos") + start_quat = self._ssbo_proxy_start.get("quat") + start_scale = self._ssbo_proxy_start.get("scale") + if start_pos is None or start_quat is None or start_scale is None: + return + + curr_pos = Vec3(target.getPos(self.base.render)) + curr_quat = Quat(target.getQuat(self.base.render)) + curr_scale = Vec3(target.getScale()) + + delta_pos = curr_pos - start_pos + inv_start_quat = Quat(start_quat) + inv_start_quat.invertInPlace() + delta_quat = curr_quat * inv_start_quat + delta_scale = Vec3( + curr_scale.x / start_scale.x if abs(start_scale.x) > 1e-8 else 1.0, + curr_scale.y / start_scale.y if abs(start_scale.y) > 1e-8 else 1.0, + curr_scale.z / start_scale.z if abs(start_scale.z) > 1e-8 else 1.0, + ) + + self.controller.apply_transform_session( + self._ssbo_transform_snapshot, + delta_pos, + delta_quat, + delta_scale, + ) + except Exception as e: + print(f"[SSBOEditor] drag_move bridge failed: {e}") + + def _on_ssbo_gizmo_drag_end(self, payload): + try: + if self._ssbo_transform_active: + print(f"[SSBOEditor] drag_end locals={len(self._ssbo_selected_local_indices)}") + self._ssbo_transform_active = False + self._ssbo_transform_snapshot = None + except Exception as e: + print(f"[SSBOEditor] drag_end bridge failed: {e}") + + def _fallback_legacy_pick(self, mx, my): + """Fallback to legacy ray picking when SSBO misses.""" + try: + if not hasattr(self.base, "event_handler") or not self.base.event_handler: + return + win_w, win_h = self.base.win.getSize() + x = (mx + 1.0) * 0.5 * win_w + y = (1.0 - my) * 0.5 * win_h + self.base.event_handler.mousePressEventLeft({"x": x, "y": y}) + except Exception as e: + print(f"[SSBOEditor] legacy fallback pick failed: {e}") + + def _try_start_gizmo_drag(self, mouse_x=None, mouse_y=None): + """Try to start gizmo drag using the existing SelectionSystem pipeline.""" + try: + new_transform = getattr(self.base, "newTransform", None) + if ( + new_transform is not None and + mouse_x is not None and + mouse_y is not None and + self._is_mouse_on_new_gizmo(new_transform, mouse_x, mouse_y) + ): + return True + selection = getattr(self.base, "selection", None) + if not selection or not selection.gizmo: + return False + win_w, win_h = self.base.win.getSize() + mpos = self.base.mouseWatcherNode.get_mouse() + x = (mpos.x + 1.0) * 0.5 * win_w + y = (1.0 - mpos.y) * 0.5 * win_h + + axis = selection.gizmoHighlightAxis or selection.checkGizmoClick(x, y) + if axis: + selection.startGizmoDrag(axis, x, y) + return True + except Exception as e: + print(f"[SSBOEditor] gizmo drag start failed: {e}") + return False + + def _is_mouse_on_new_gizmo(self, new_transform, mouse_x, mouse_y): + """Refresh and query hover state for TransformGizmo on current click position.""" + try: + mouse_pos = Point3(mouse_x, mouse_y, 0.0) + for gizmo_name in ("move_gizmo", "rotate_gizmo", "scale_gizmo"): + gizmo = getattr(new_transform, gizmo_name, None) + if not gizmo or not getattr(gizmo, "attached", False): + continue + hover_updater = getattr(gizmo, "_update_hover_highlight", None) + if callable(hover_updater): + hover_updater(mouse_pos) + return bool(getattr(new_transform, "is_hovering", False)) + except Exception as e: + print(f"[SSBOEditor] new gizmo hover check failed: {e}") + return False + + def focus_on_selected(self): + if self.selected_name and self.selected_ids: + first_id = self.selected_ids[0] + pos = self.controller.get_world_pos(first_id) + dist = 100 + self.base.camera.set_pos(pos.x, pos.y - dist, pos.z + dist * 0.5) + self.base.camera.look_at(pos) + + def draw_imgui(self): + if not self.controller: return + + imgui.set_next_window_pos((10, 10), imgui.Cond_.first_use_ever) + imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever) + + expanded, opened = imgui.begin("Scene Tree (Component)") + if expanded: + imgui.text(f"FPS: {globalClock.getAverageFrameRate():.1f}") + imgui.separator() + + changed, self.search_text = imgui.input_text("Search", self.search_text, 256) + + if imgui.begin_child("ObjectList", (0, 380), child_flags=imgui.ChildFlags_.borders): + if self.search_text != self.last_search_text: + self.last_search_text = self.search_text + search_lower = self.search_text.lower() + self.filtered_nodes = [] + for key in self.controller.node_list: + display = self.controller.display_names.get(key, key.split('/')[-1]) + if not search_lower or (search_lower in display.lower() or search_lower in key.lower()): + geom_count = len(self.controller.name_to_ids.get(key, [])) + self.filtered_nodes.append((key, display, geom_count)) + + # If list is empty initially (no search), show all + if not self.search_text and not self.filtered_nodes: + if len(self.filtered_nodes) != len(self.controller.node_list): + self.filtered_nodes = [(k, self.controller.display_names.get(k, k), len(self.controller.name_to_ids.get(k,[]))) for k in self.controller.node_list] + + count = len(self.filtered_nodes) + clipper = imgui.ListClipper() + clipper.begin(count) + while clipper.step(): + for i in range(clipper.display_start, clipper.display_end): + key, display, geom_count = self.filtered_nodes[i] + label = f"{display} ({geom_count})" + is_selected = (key == self.selected_name) + if imgui.selectable(label, is_selected)[0]: + self.select_node(key) + imgui.end_child() + + imgui.separator() + if self.selected_name: + imgui.text_colored((1, 0.8, 0.2, 1), f"Selected: {self.selected_name}") + if imgui.button("Focus (F)"): self.focus_on_selected() + imgui.end() + + # swap_transforms_task removed - motion blur disabled for performance + + def update_task(self, task): + dt = globalClock.getDt() + io = imgui.get_io() + + if io.want_capture_keyboard: return task.cont + + if self.selected_ids and self.controller: + speed = 50 * dt + acc = Vec3(0, 0, 0) + if self.keys.get('arrow_up'): acc.z += speed + if self.keys.get('arrow_down'): acc.z -= speed + if self.keys.get('arrow_left'): acc.x -= speed + if self.keys.get('arrow_right'): acc.x += speed + if self.keys.get('z'): acc.y += speed + if self.keys.get('x'): acc.y -= speed + + if acc.length_squared() > 0: + for idx in self.selected_ids: + self.controller.move_object(idx, acc) + + return task.cont diff --git a/ui/Builtin/Elements.py b/ui/Builtin/Elements.py new file mode 100644 index 00000000..031dc1d0 --- /dev/null +++ b/ui/Builtin/Elements.py @@ -0,0 +1,339 @@ +""" + + + +OUTDATED + +Do not use anymore. + + +""" + + +import colorsys + +from LUIObject import LUIObject +from LUISlider import LUISlider +from LUISprite import LUISprite +from LUIVerticalLayout import LUIVerticalLayout +from LUICallback import LUICallback +from LUILabel import LUILabel +from LUIFrame import LUIFrame +from LUIButton import LUIButton + +class LUISliderWithLabel(LUIObject, LUICallback): + + def __init__(self, parent=None, width=100.0, filled=False, min_value=0, max_value=1.0, precision=2, value=None): + LUIObject.__init__(self, x=0, y=0, w=width, h=0) + LUICallback.__init__(self) + + max_numbers_before = max(len(str(int(max_value))), len(str(int(min_value)))) + number_space_required = max_numbers_before + + if precision > 0: + number_space_required += 1 + precision + + pixels_per_number = 7 + self.precision = precision + + self.slider = LUISlider(self, width=width - pixels_per_number * number_space_required - 5, filled=filled, min_value=min_value, max_value=max_value, value=value) + self.label = LUILabel(parent=self, shadow=True, text=u"1.23") + self.label.right = 0 + self.label.top = self.label.height - self.slider.height + self.label.color = (1,1,1,0.5) + + self.slider.add_change_callback(self._on_slider_changed) + self.slider.add_change_callback(self._trigger_callback) + self._on_slider_changed(self.slider, self.slider.get_value()) + + if parent is not None: + self.parent = parent + + self.fit_to_children() + + def get_value(self): + return self.slider.get_value() + + def set_value(self, val): + self.slider.set_value(val) + + def _on_slider_changed(self, obj, value): + self.label.text = ("{:." + str(self.precision) + "f}").format(value) + +class LUIKeyMarker(LUIObject): + + def __init__(self, parent=None, key=u"A"): + LUIObject.__init__(self) + self.bgLeft = LUISprite(self, "Keymarker_Left", "skin") + self.bgMid = LUISprite(self, "Keymarker", "skin") + self.bgRight = LUISprite(self, "Keymarker_Right", "skin") + + self.label = LUILabel(parent=self, text=key, shadow=True) + self.label.centered = (True, True) + self.label.margin = (-3, 0, 0, -1) + self.margin = (-1, 0, 0, -1) + + self.set_key(key) + + if parent is not None: + self.parent = parent + + self.fit_to_children() + + def set_key(self, key): + self.label.set_text(key) + self.width = self.label.width + self.bgLeft.width + self.bgRight.width + 7 + self.bgMid.width = self.width - self.bgLeft.width - self.bgRight.width + self.bgMid.left = self.bgLeft.width + self.bgRight.left = self.bgMid.width + self.bgMid.left + + self.fit_to_children() + +class LUIKeyInstruction(LUIObject): + + def __init__(self, parent=None, key=u"A", instruction=u"Instruction"): + LUIObject.__init__(self) + self.marker = LUIKeyMarker(parent=self, key=key) + self.instructionLabel = LUILabel(parent=self, text=instruction, shadow=True) + self.instructionLabel.centered = (False, True) + self.instructionLabel.margin.top = -4 + self.set_key(key) + + def set_key(self, key): + self.marker.set_key(key) + self.instructionLabel.left = self.marker.width + 5 + self.fit_to_children() + + +class LUIColorpicker(LUIObject): + + def __init__(self, parent=None, color=None): + LUIObject.__init__(self, x=0, y=0, w=27, h=27) + + self.previewBg = LUISprite(self, "ColorpickerPreviewBg", "skin") + + self.filler = LUISprite(self, "blank", "skin") + self.filler.width = 21 + self.filler.height = 21 + self.filler.pos = (5, 5) + self.filler.color = (0.2,0.6,1.0,1.0) + + self.overlay = LUISprite(self, "ColorpickerPreviewOverlay", "skin") + self.overlay.pos = (2, 2) + self.overlay.bind("click", self._open_dialog) + + self.fit_to_children() + + self.popup = LUIColorpickerPopup(self) + self.popup.hide() + + if color is not None: + self.colorValue = color + else: + # My favourite color + self.colorValue = (0.2, 0.6, 1.0) + self.set_color_value(self.colorValue) + + self.popup.add_change_callback(self._on_popup_color_changed) + + if parent is not None: + self.parent = parent + + def _open_dialog(self, event): + if self.has_focus(): + self.blur() + else: + self.request_focus() + + def on_focus(self, event): + self.popup._load_rgb(self.colorValue) + self.popup.open_at(self, 14.0) + + def set_color_value(self, rgb): + self.colorValue = rgb + self.filler.color = rgb + + def get_color_value(self): + return self.colorValue + + def on_tick(self, event): + self.popup._update(event) + + def on_blur(self, event): + self.popup.close() + + def _on_popup_color_changed(self, popup, rgb): + self.set_color_value(rgb) + + def _on_popup_closed(self): + self.blur() + + +class LUIPopup(LUIFrame): + + def __init__(self, parent=None, width=200, height=200): + LUIFrame.__init__(self, parent=parent, width=width, height=height, padding=10, innerPadding=0) + self.topmost = True + self.borderSize = 33 + self.content.bind("click", self._on_content_click) + + def open_at(self, targetElement, distance): + self.show() + + targetPos = targetElement.get_abs_pos()+ targetElement.get_size() / 2 + + showAbove = targetPos.y > self.height - self.borderSize + showLeft = targetPos.x > self.width - self.borderSize + + relative = self.get_relative_pos(targetPos) + self.pos += relative + + if showLeft: + self.left -= self.width - self.borderSize + self.left += 25 + else: + self.left -= self.borderSize + self.left -= 25 + + if showAbove: + self.top -= distance + self.top -= self.height - self.borderSize + else: + self.top += distance + self.top -= self.borderSize + + + def _on_content_click(self, event): + pass + + def close(self): + self.hide() + +class LUIColorpickerPopup(LUIPopup, LUICallback): + def __init__(self, parent=None): + LUIPopup.__init__(self, parent=parent, width=240, height=146) + LUICallback.__init__(self) + + self.field = LUIObject(self.content, x=0, y=0, w=128, h=128) + + self.fieldBG = LUISprite(self.field, "blank", "skin") + self.fieldBG.size = (128, 128) + self.fieldBG.color = (0.2,0.6,1.0) + self.fieldFG = LUISprite(self.field, "ColorpickerFieldOverlay", "skin") + self.fieldFG.pos = (-2, 0) + + self.fieldBG.bind("mousedown", self._start_field_dragging) + self.fieldBG.bind("mouseup", self._stop_field_dragging) + + self.fieldHandle = LUISprite(self.field, "ColorpickerFieldHandle", "skin") + self.fieldHandle.bind("mousedown", self._start_field_dragging) + self.fieldHandle.bind("mouseup", self._stop_field_dragging) + + self.fieldDragging = False + + self.hueSlider = LUIObject(self.content, x=140, y=0, w=40, h=128) + self.hueSliderFG = LUISprite(self.hueSlider, "ColorpickerHueSlider", "skin") + + self.hueHandle = LUISprite(self.hueSlider, "ColorpickerHueHandle", "skin") + self.hueHandle.left = (self.hueSliderFG.width - self.hueHandle.width) / 2.0 + self.hueHandle.top = 50 + + self.hueDragging = False + self.hueSlider.bind("mousedown", self._start_hue_dragging) + self.hueSlider.bind("mouseup", self._stop_hue_dragging) + + self.labels = LUIVerticalLayout(self.content, width=40) + self.labels.pos = (177, 42) + + colors = [u"R", u"G", u"B"] + self.colorLabels = [] + + for color in colors: + label = LUILabel(text=color, shadow=True) + label.color = (1,1,1,0.3) + + valueLabel = LUILabel(text=u"255", shadow=True) + valueLabel.right = 0 + self.labels.add(label, valueLabel) + self.colorLabels.append(valueLabel) + + self.activeColor = LUIObject(self.content, x=177, y=0) + self.activeColorBG = LUISprite(self.activeColor, "blank", "skin") + self.activeColorFG = LUISprite(self.activeColor, "ColorpickerActiveColorOverlay", "skin") + + self.activeColorBG.size = (40, 40) + self.activeColorBG.pos = (2, 0) + self.activeColorBG.color = (0.2,0.6,1.0,1.0) + + self.closeButton = LUIButton(parent=self.content, text=u"Done", width=45, template="ButtonGreen") + self.closeButton.left = 177 + self.closeButton.top = 98 + self.closeButton.bind("click", self._close_popup) + + self._set_hue(0.5) + self._set_sat_val(0.5, 0.5) + + self.widget = parent + + def _load_rgb(self, rgb): + hsv = colorsys.rgb_to_hsv(*rgb) + self._set_hue(hsv[0]) + self._set_sat_val(hsv[1], hsv[2]) + + def _close_popup(self, event): + self.widget._on_popup_closed() + self.close() + + def _update(self, event): + if self.hueDragging: + offset = event.coordinates.y - self.hueSliderFG.abs_pos.y + offset /= 128.0 + offset = 1.0 - max(0.0, min(1.0, offset)) + self._set_hue(offset) + + if self.fieldDragging: + offset = event.coordinates - self.fieldBG.abs_pos + saturation = max(0.0, min(1.0, offset.x / 128.0)) + value = 1.0 - max(0.0, min(1.0, offset.y / 128.0)) + self._set_sat_val(saturation, value) + + self._update_color() + + def _set_sat_val(self, sat, val): + self.saturation = sat + self.valueValue = val + + self.fieldHandle.top = (1.0 - self.valueValue) * 128.0 - self.fieldHandle.height / 2.0 + self.fieldHandle.left = self.saturation * 128.0 - self.fieldHandle.width / 2.0 + + def _set_hue(self, hue): + self.hueValue = min(0.999, hue) + self.hueHandle.top = (1.0-hue) * 128.0 - self.hueHandle.height / 2 + self.fieldBG.color = colorsys.hsv_to_rgb(self.hueValue, 1, 1) + + def _update_color(self): + rgb = colorsys.hsv_to_rgb(self.hueValue, self.saturation, self.valueValue) + self.activeColorBG.color = rgb + + self.colorLabels[0].set_text(str(int(rgb[0]*255.0))) + self.colorLabels[1].set_text(str(int(rgb[1]*255.0))) + self.colorLabels[2].set_text(str(int(rgb[2]*255.0))) + + self._trigger_callback(rgb) + + def _start_field_dragging(self, event): + if not self.fieldDragging: + self.fieldDragging = True + + def _stop_field_dragging(self, event): + if self.fieldDragging: + self.fieldDragging = False + + def _start_hue_dragging(self, event): + if not self.hueDragging: + self.hueDragging = True + + def _stop_hue_dragging(self, event): + if self.hueDragging: + self.hueDragging = False + diff --git a/ui/Builtin/LUIBlockText.py b/ui/Builtin/LUIBlockText.py new file mode 100644 index 00000000..d74d3c8b --- /dev/null +++ b/ui/Builtin/LUIBlockText.py @@ -0,0 +1,100 @@ + +from panda3d.core import LVecBase2i +from LUIObject import LUIObject +from LUILabel import LUILabel +from LUIInitialState import LUIInitialState + +__all__ = ["LUIBlockText"] + + +class LUIBlockText(LUIObject): + + """ Small helper class to format labels into paragraphs. + Uses LUILabels internally """ + + def __init__(self, **kwargs): + """ Creates a new block of text. """ + LUIObject.__init__(self) + LUIInitialState.init(self, kwargs) + self._cursor = LVecBase2i(0) + self._last_size = 14 + + self.labels = [] + + + def clear(self): + """ Removes all text from this label and resets it to the initial state. + This will also detach the sub-labels from this label. """ + self._cursor.set(0, 0) + self.labels = [] + self.remove_all_children() + + + def newline(self, font_size=None): + """ Moves the cursor to the next line. The font size controls how much + the cursor will move. By default, the font size of the last added text + is used, or if no text was added yet, a size of 14.""" + self._cursor.x = 0 + if font_size is None: + font_size = self._last_size + self._cursor.y += font_size + 2 + + + def add(self, *args, **kwargs): + """ Appends a new text. The arguments are equal to the arguments of + LUILabel. The arguments shouldn't contain information about the + placement like top_left, or center_vertical, since the labels are + placed at explicit positions. """ + self._last_size = kwargs.get("font_size", 14) + label = LUILabel(parent=self, left=self._cursor.x, top=self._cursor.y, width=self.get_width(), + *args, **kwargs) + + self.labels.append(label) + + # This is a bit of a hack, we should use a horizontal layout, but we + # don't for performance reasons. + self._cursor.y += label.text_handle.height + + # After every paragraph, we add a new line. + self.newline() + + + def set_text(self, text): + """ Replaces the text with new text """ + self.clear() + self.add(text=text) + + + def update_height(self): + """ Updates the height of the element, adding a newline to the end of + every paragraph """ + top = 0 + for child in self.labels: + child.top = top + top += child._text.height + + # Newline + top += self._last_size + 2 + + + def set_wrap(self, wrap): + """ Sets text wrapping for the element. Wrapping breaks lines on + spaces, and breaks words if the word is longer than the line + length. """ + for child in self.children: + for c in child.children: + c.set_wordwrap(wrap) + + self.update_height() + + + def set_width(self, width): + """ Sets the width of this element, and turns on wrapping. """ + for child in self.children: + child.set_width(width) + + # Need to force an update to the text when the width changes. + for c in child.children: + c.set_wordwrap(True) + + self.update_height() diff --git a/ui/Builtin/LUIButton.py b/ui/Builtin/LUIButton.py new file mode 100644 index 00000000..6921efdc --- /dev/null +++ b/ui/Builtin/LUIButton.py @@ -0,0 +1,192 @@ + +from LUIObject import LUIObject +from LUILayouts import LUIHorizontalStretchedLayout +from LUILabel import LUILabel +from LUIInitialState import LUIInitialState + +__all__ = ["LUIButton"] + + +class LUIButton(LUIObject): + + """ Simple button, containing three sprites and a label. """ + + def __init__(self, text="Button", template="ButtonDefault", **kwargs): + """ Constructs a new button. The template controls which sprites to use. + If the template is "ButtonDefault" for example, the sprites + "ButtonDefault_Left", "ButtonDefault" and "ButtonDefault_Right" will + be used. The sprites used when the button is pressed should be named + "ButtonDefaultFocus_Left" and so on then. + + If an explicit width is set on the button, the button will stick to + that width, otherwise it will automatically resize to fit the label """ + LUIObject.__init__(self, x=0, y=0, solid=True) + self._template = template + self._layout = LUIHorizontalStretchedLayout( + parent=self, prefix=self._template, width="100%") + self._label = LUILabel(parent=self, text=text) + self._label.z_offset = 1 + self._label.center_vertical = True + self._label.margin = 0, 20, 0, 20 + self.margin.left = -1 + self._hovered = False + self._pressed = False + self._use_custom_texture = False + self._custom_texture = None + self._custom_uv = None + self._custom_texture_hover = None + self._custom_uv_hover = None + self._custom_texture_pressed = None + self._custom_uv_pressed = None + LUIInitialState.init(self, kwargs) + + def _apply_stretch_sizes(self): + """Ensure internal sprites stretch to the button size.""" + try: + layout = getattr(self, '_layout', None) + if layout is not None: + if hasattr(layout, 'width'): + layout.width = "100%" + if hasattr(layout, 'height'): + layout.height = "100%" + inner = getattr(layout, '_layout', None) + if inner is not None: + if hasattr(inner, 'width'): + inner.width = "100%" + if hasattr(inner, 'height'): + inner.height = "100%" + for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): + spr = getattr(layout, attr, None) + if spr is not None: + if hasattr(spr, 'height'): + spr.height = "100%" + if attr == '_sprite_mid' and hasattr(spr, 'width'): + spr.width = "100%" + except Exception: + pass + + def _apply_custom_texture(self, state="normal"): + """Apply custom texture based on state: normal/hover/pressed.""" + if not self._use_custom_texture: + return + + tex = None + uv = None + if state == "pressed" and self._custom_texture_pressed is not None: + tex = self._custom_texture_pressed + uv = self._custom_uv_pressed + elif state == "hover" and self._custom_texture_hover is not None: + tex = self._custom_texture_hover + uv = self._custom_uv_hover + else: + tex = self._custom_texture + uv = self._custom_uv + + if tex is None: + return + + layout = getattr(self, "_layout", None) + if layout is None: + return + for attr in ("_sprite_left", "_sprite_mid", "_sprite_right"): + spr = getattr(layout, attr, None) + if spr is None: + continue + try: + if hasattr(spr, "set_texture"): + spr.set_texture(tex, resize=False) + if uv and hasattr(spr, "set_uv_range"): + u0, v0, u1, v1 = uv + spr.set_uv_range(u0, v0, u1, v1) + except Exception: + pass + + # Hide left/right caps for single-image mode + try: + if hasattr(layout, "_sprite_left") and layout._sprite_left is not None: + layout._sprite_left.width = 0 + if hasattr(layout, "_sprite_right") and layout._sprite_right is not None: + layout._sprite_right.width = 0 + except Exception: + pass + + self._apply_stretch_sizes() + + def set_custom_textures(self, normal_tex, normal_uv=None, hover_tex=None, hover_uv=None, pressed_tex=None, pressed_uv=None): + """Set custom textures for normal/hover/pressed states.""" + self._use_custom_texture = True + self._custom_texture = normal_tex + self._custom_uv = normal_uv + self._custom_texture_hover = hover_tex + self._custom_uv_hover = hover_uv + self._custom_texture_pressed = pressed_tex + self._custom_uv_pressed = pressed_uv + if self._hovered: + self._apply_custom_texture("hover") + else: + self._apply_custom_texture("normal") + + def set_custom_texture(self, texture, uv=None): + """Use a single texture for the button background.""" + self.set_custom_textures(texture, uv, None, None, None, None) + + def clear_custom_texture(self): + """Restore default template textures.""" + self._use_custom_texture = False + self._custom_texture = None + self._custom_uv = None + self._custom_texture_hover = None + self._custom_uv_hover = None + self._custom_texture_pressed = None + self._custom_uv_pressed = None + try: + self._layout.prefix = self._template + self._apply_stretch_sizes() + except Exception: + pass + + @property + def text(self): + """ Returns the current label text of the button """ + return self._label.text + + @text.setter + def text(self, text): + """ Sets the label text of the button """ + self._label.text = text + + def on_mousedown(self, event): + """ Internal on_mousedown handler. Do not override """ + self._pressed = True + if self._use_custom_texture: + self._apply_custom_texture("pressed") + else: + self._layout.prefix = self._template + "Focus" + self._apply_stretch_sizes() + self._label.margin.top = 1 + + def on_mouseup(self, event): + """ Internal on_mouseup handler. Do not override """ + self._pressed = False + if self._use_custom_texture: + if self._hovered: + self._apply_custom_texture("hover") + else: + self._apply_custom_texture("normal") + else: + self._layout.prefix = self._template + self._apply_stretch_sizes() + self._label.margin.top = 0 + + def on_mouseover(self, event): + """ Internal mouseover handler """ + self._hovered = True + if self._use_custom_texture and not self._pressed: + self._apply_custom_texture("hover") + + def on_mouseout(self, event): + """ Internal mouseout handler """ + self._hovered = False + self._pressed = False + if self._use_custom_texture: + self._apply_custom_texture("normal") diff --git a/ui/Builtin/LUICanvas.py b/ui/Builtin/LUICanvas.py new file mode 100644 index 00000000..b6593006 --- /dev/null +++ b/ui/Builtin/LUICanvas.py @@ -0,0 +1,102 @@ +""" +LUICanvas - 类似Unity的Canvas容器,带有可视化边框 +用于组织和管理UI元素,所有UI组件都应该是Canvas的子节点 +""" + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUIInitialState import LUIInitialState + +__all__ = ["LUICanvas"] + + +class LUICanvas(LUIObject): + """ + Canvas容器,类似Unity的Canvas系统 + - 所有UI元素的父容器 + - 带有可视化的边框线框(类似Unity编辑器中的Canvas Gizmo) + - 可以设置边框颜色和宽度 + """ + + def __init__(self, border_color=(0.3, 0.7, 1.0, 0.3), border_width=2, **kwargs): + """ + 创建一个新的Canvas + + 参数: + border_color: 边框颜色 (r, g, b, a),默认为蓝色半透明 + border_width: 边框宽度,默认为2像素(使用平面模式时此参数无效) + """ + LUIObject.__init__(self) + + self._border_width = border_width + self._border_color = border_color + + # 创建一个透明平面覆盖整个Canvas + self._border_plane = LUISprite(self, "blank", "skin") + self._border_plane.pos = (0, 0) + self._border_plane.color = self._border_color + self._border_plane.z_offset = 0 + + # 先设置一个固定尺寸,确保可见 + self._border_plane.width = 1280 + self._border_plane.height = 720 + + # 应用初始状态 + LUIInitialState.init(self, kwargs) + + # 最后设置Canvas占满整个屏幕(这会触发setter更新边框) + self.set_size("100%", "100%") + + def _update_borders(self): + """更新边框平面的位置和大小""" + # 直接获取width和height属性值 + try: + width = self.width + height = self.height + + # 设置平面覆盖整个Canvas区域 + self._border_plane.pos = (0, 0) + self._border_plane.width = width + self._border_plane.height = height + except: + # 如果获取失败,使用默认值 + pass + + def set_border_color(self, color): + """设置边框颜色 (r, g, b, a)""" + self._border_color = color + self._border_plane.color = color + + def set_border_width(self, width): + """设置边框宽度(平面模式下无效)""" + self._border_width = width + + def show_border(self): + """显示边框""" + self._border_plane.show() + + def hide_border(self): + """隐藏边框""" + self._border_plane.hide() + + @property + def width(self): + """获取宽度""" + return LUIObject.width.fget(self) + + @width.setter + def width(self, value): + """设置宽度并更新边框""" + LUIObject.width.fset(self, value) + self._update_borders() + + @property + def height(self): + """获取高度""" + return LUIObject.height.fget(self) + + @height.setter + def height(self, value): + """设置高度并更新边框""" + LUIObject.height.fset(self, value) + self._update_borders() diff --git a/ui/Builtin/LUICheckbox.py b/ui/Builtin/LUICheckbox.py new file mode 100644 index 00000000..11fdbc7c --- /dev/null +++ b/ui/Builtin/LUICheckbox.py @@ -0,0 +1,83 @@ + +from __future__ import division + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUILabel import LUILabel +from LUIInitialState import LUIInitialState + +__all__ = ["LUICheckbox"] + + +class LUICheckbox(LUIObject): + + """ This is a simple checkbox, including a Label. The checkbox can either + be checked or unchecked. """ + + def __init__(self, checked=False, label=u"Checkbox", **kwargs): + """ Constructs a new checkbox with the given label and state. By default, + the checkbox is not checked. """ + LUIObject.__init__(self, x=0, y=0, solid=True) + self._checked = checked + self._checkbox_sprite = LUISprite(self, "Checkbox_Default", "skin") + self._label = LUILabel(parent=self, text=label, margin=(0, 0, 0, 25), + center_vertical=True, alpha=0.4) + self._hovered = False + self._update_sprite() + LUIInitialState.init(self, kwargs) + + @property + def checked(self): + """ Returns True if the checkbox is currently checked """ + return self._checked + + @checked.setter + def checked(self, checked): + """ Sets the checkbox state """ + self._checked = checked + self._update_sprite() + + def toggle(self): + """ Toggles the checkbox state """ + self.checked = not self.checked + + @property + def label(self): + """ Returns a handle to the label, so it can be modified """ + return self._label + + @property + def sprite(self): + """ Returns a handle to the internal checkbox sprite """ + return self._checkbox_sprite + + def on_click(self, event): + """ Internal onclick handler. Do not override """ + self._checked = not self._checked + self.trigger_event("changed") + self._update_sprite() + + def on_mousedown(self, event): + """ Internal mousedown handler. """ + self._checkbox_sprite.color = (0.9, 0.9, 0.9, 1.0) + + def on_mouseup(self, event): + """ Internal on_mouseup handler. """ + self._checkbox_sprite.color = (1, 1, 1, 1) + + def on_mouseover(self, event): + """ Internal mouseover handler """ + self._hovered = True + self._update_sprite() + + def on_mouseout(self, event): + """ Internal mouseout handler """ + self._hovered = False + self._update_sprite() + + def _update_sprite(self): + """ Internal method to update the sprites """ + img = "Checkbox_Checked" if self._checked else "Checkbox_Default" + if self._hovered: + img += "Hover" + self._checkbox_sprite.set_texture(img, "skin") diff --git a/ui/Builtin/LUIFormattedLabel.py b/ui/Builtin/LUIFormattedLabel.py new file mode 100644 index 00000000..fad7df3e --- /dev/null +++ b/ui/Builtin/LUIFormattedLabel.py @@ -0,0 +1,47 @@ + +from panda3d.core import LVecBase2i +from LUIObject import LUIObject +from LUILabel import LUILabel +from LUIInitialState import LUIInitialState + +__all__ = ["LUIFormattedLabel"] + + +class LUIFormattedLabel(LUIObject): + + """ Small helper class to build a text consisting of different formatted + parts of text. Uses LUILabels internally """ + + def __init__(self, **kwargs): + """ Creates a new formatted label. """ + LUIObject.__init__(self) + LUIInitialState.init(self, kwargs) + self._cursor = LVecBase2i(0) + self._last_size = 14 + + def clear(self): + """ Removes all text from this label and resets it to the initial state. + This will also detach the sub-labels from this label. """ + self._cursor.set(0, 0) + self.remove_all_children() + + def newline(self, font_size=None): + """ Moves the cursor to the next line. The font size controls how much + the cursor will move. By default, the font size of the last added text + is used, or if no text was added yet, a size of 14.""" + self._cursor.x = 0 + if font_size is None: + font_size = self._last_size + self._cursor.y += font_size + 2 + + def add(self, *args, **kwargs): + """ Appends a new text. The arguments are equal to the arguments of + LUILabel. The arguments shouldn't contain information about the + placement like top_left, or center_vertical, since the labels are + placed at explicit positions. """ + self._last_size = kwargs.get("font_size", 14) + label = LUILabel(parent=self, left=self._cursor.x, top=self._cursor.y, + *args, **kwargs) + # This is a bit of a hack, we should use a horizontal layout, but we + # don't for performance reasons. + self._cursor.x += label.text_handle.width diff --git a/ui/Builtin/LUIFrame.py b/ui/Builtin/LUIFrame.py new file mode 100644 index 00000000..4408b5c7 --- /dev/null +++ b/ui/Builtin/LUIFrame.py @@ -0,0 +1,66 @@ + +from __future__ import print_function + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUILayouts import LUICornerLayout +from LUIInitialState import LUIInitialState +from LUIScrollableRegion import LUIScrollableRegion + +__all__ = ["LUIFrame"] + + +class LUIFrame(LUIObject): + + """ A container which can store multiple ui-elements. If you don't want a + border/background, you should use an empty LUIObject as container instead. + """ + + FS_sunken = 1 + FS_raised = 2 + + def __init__(self, inner_padding=5, scrollable=False, style=FS_raised, + **kwargs): + """ Creates a new frame with the given options and style. If scrollable + is True, the contents of the frame will scroll if they don't fit into + the frame height. inner_padding only has effect if scrollable is True. + You can call fit_to_children() to make the frame fit automatically to + it's contents.""" + LUIObject.__init__(self) + + # Each *style* has a different border size (size of the shadow). The + # border size shouldn't get calculated to the actual framesize, so we + # are determining it first and then substracting it. + # TODO: We could do this automatically, determined by the sprite size + # probably? + self._border_size = 0 + self.padding = 10 + self.solid = True + prefix = "" + + if style == LUIFrame.FS_raised: + temp = LUISprite(self, "Frame_Left", "skin") + self._border_size = temp.width + self.remove_child(temp) + prefix = "Frame_" + elif style == LUIFrame.FS_sunken: + self._border_size = 0 + prefix = "SunkenFrame_" + else: + raise Exception("Unkown LUIFrame style: " + style) + + self._scrollable = scrollable + self._layout = LUICornerLayout(parent=self, image_prefix=prefix) + self._layout.margin = -(self.padding.top + self._border_size) + if self._scrollable: + self._content = LUIObject(self) + self._content.size = (self.width, self.height) + self._content.pos = (self._border_size, self._border_size) + self._scroll_content = LUIScrollableRegion( + self._content, + width=self.width - 2 * self.padding.left, + height=self.height - 2 * self.padding.left, + padding=inner_padding) + self.content_node = self._scroll_content.content_node + + LUIInitialState.init(self, kwargs) diff --git a/ui/Builtin/LUIHorizontalLayout.py b/ui/Builtin/LUIHorizontalLayout.py new file mode 100644 index 00000000..3a887a2c --- /dev/null +++ b/ui/Builtin/LUIHorizontalLayout.py @@ -0,0 +1,20 @@ +""" + +This is a wrapper file. It contains no actual implementation + +""" + +from panda3d.lui import LUIHorizontalLayout as _LUIHorizontalLayout +from LUIInitialState import LUIInitialState + +__all__ = ["LUIHorizontalLayout"] + + +class LUIHorizontalLayout(_LUIHorizontalLayout): + """ This is a wrapper class for the C++ LUIHorizontalLayout class, to be + able to use it in a more convenient way. It leverages LUIInitialState + to be able to pass arbitrary keyword arguments. """ + + def __init__(self, parent=None, spacing=0.0, **kwargs): + _LUIHorizontalLayout.__init__(self, parent, spacing) + LUIInitialState.init(self, kwargs) diff --git a/ui/Builtin/LUIInitialState.py b/ui/Builtin/LUIInitialState.py new file mode 100644 index 00000000..cf3eb590 --- /dev/null +++ b/ui/Builtin/LUIInitialState.py @@ -0,0 +1,41 @@ + +__all__ = ["LUIInitialState"] + + +class LUIInitialState: + + """ Small helper class to pass keyword arguments to the LUI-objects. It takes + all keyword arguments of a given call, and calls obj. = for + each keyword. It usually is called at the end of the __init__ method. """ + + def __init__(self): + raise Exception("LUIInitialState is a static class") + + # Some properties have alternative names, under which they can be accessed. + __MAPPINGS = { + "x": "left", + "y": "top", + "w": "width", + "h": "height" + } + + @classmethod + def init(cls, obj, kwargs): + """ Applies all keyword arguments as properties. For example, passing + dict({"left": 10, "top": 3, "color": (0.2, 0.6, 1.0)}) results in + behaviour similar to: + + element.left = 10 + element.top = 3 + element.color = 0.2, 0.6, 1.0 + + Calling this method allows setting arbitrary properties in + constructors, without having to specify each possible keyword argument. + """ + for arg_name, arg_val in kwargs.items(): + arg_name = cls.__MAPPINGS.get(arg_name, arg_name) + if hasattr(obj, arg_name): + setattr(obj, arg_name, arg_val) + else: + raise AttributeError("{0} has no attribute {1}".format( + obj.__class__.__name__, arg_name)) diff --git a/ui/Builtin/LUIInputField.py b/ui/Builtin/LUIInputField.py new file mode 100644 index 00000000..b3863c2f --- /dev/null +++ b/ui/Builtin/LUIInputField.py @@ -0,0 +1,219 @@ +import re + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUILabel import LUILabel +from LUIInitialState import LUIInitialState +from LUILayouts import LUIHorizontalStretchedLayout + +__all__ = ["LUIInputField"] + + +class LUIInputField(LUIObject): + + """ Simple input field, accepting text input. This input field supports + entering text and navigating. Selecting text is (currently) not supported. + + The input field also supports various keyboard shortcuts: + + [pos1] Move to the beginning of the text + [end] Move to the end of the text + [arrow_left] Move one character to the left + [arrow_right] Move one character to the right + [ctrl] + [arrow_left] Move to the left, skipping over words + [ctrl] + [arrow_right] Move to the right, skipping over words + [escape] Un-focus input element + + """ + + re_skip = re.compile("\W*\w+\W") + + def __init__(self, parent=None, width=200, placeholder=u"Enter some text ..", value=u"", **kwargs): + """ Constructs a new input field. An input field always needs a width specified """ + LUIObject.__init__(self, x=0, y=0, solid=True) + self.set_width(width) + self._layout = LUIHorizontalStretchedLayout(parent=self, prefix="InputField", width="100%") + + # Container for the text + self._text_content = LUIObject(self) + self._text_content.margin = (5, 7, 5, 7) + self._text_content.clip_bounds = (0,0,0,0) + self._text_content.set_size("100%", "100%") + + # Scroller for the text, so we can move right and left + self._text_scroller = LUIObject(parent=self._text_content) + self._text_scroller.center_vertical = True + self._text = LUILabel(parent=self._text_scroller, text="") + + # Cursor for the current position + self._cursor = LUISprite(self._text_scroller, "blank", "skin", x=0, y=0, w=2, h=15) + self._cursor.color = (0.5, 0.5, 0.5) + self._cursor.margin.top = 2 + self._cursor.z_offset = 20 + self._cursor_index = 0 + self._cursor.hide() + self._value = value + + # Placeholder text, shown when out of focus and no value exists + self._placeholder = LUILabel(parent=self._text_content, text=placeholder, shadow=False, + center_vertical=True, alpha=0.2) + + # Various states + self._tickrate = 1.0 + self._tickstart = 0.0 + + self._render_text() + + if parent is not None: + self.parent = parent + + LUIInitialState.init(self, kwargs) + + @property + def value(self): + """ Returns the value of the input field """ + return self._value + + @value.setter + def value(self, new_value): + """ Sets the value of the input field """ + self._value = new_value + self._render_text() + self.trigger_event("changed", self._value) + + def clear(self): + """ Clears the input value """ + self.value = u"" + + @property + def cursor_pos(self): + """ Set the cursor position """ + return self._cursor_index + + @cursor_pos.setter + def cursor_pos(self, pos): + """ Set the cursor position """ + if pos >= 0: + self._cursor_index = max(0, min(len(self._value), pos)) + else: + self._cursor_index = max(len(self._value) + pos + 1, 0) + self._reset_cursor_tick() + self._render_text() + + def on_tick(self, event): + """ Tick handler, gets executed every frame """ + frame_time = globalClock.get_frame_time() - self._tickstart + show_cursor = frame_time % self._tickrate < 0.5 * self._tickrate + if show_cursor: + self._cursor.color = (0.5, 0.5, 0.5, 1) + else: + self._cursor.color = (1, 1, 1, 0) + + def on_click(self, event): + """ Internal on click handler """ + self.request_focus() + + def on_mousedown(self, event): + """ Internal mousedown handler """ + local_x_offset = self._text.text_handle.get_relative_pos(event.coordinates).x + self.cursor_pos = self._text.text_handle.get_char_index(local_x_offset) + + def _reset_cursor_tick(self): + """ Internal method to reset the cursor tick """ + self._tickstart = globalClock.get_frame_time() + + def on_focus(self, event): + """ Internal focus handler """ + self._cursor.show() + self._placeholder.hide() + self._reset_cursor_tick() + self._layout.color = (0.9, 0.9, 0.9, 1) + + def on_keydown(self, event): + """ Internal keydown handler. Processes the special keys, and if none are + present, redirects the event """ + key_name = event.message + if key_name == "backspace": + self._value = self._value[:max(0, self._cursor_index - 1)] + self._value[self._cursor_index:] + self.cursor_pos -= 1 + self.trigger_event("changed", self._value) + elif key_name == "delete": + post_value = self._value[min(len(self._value), self._cursor_index + 1):] + self._value = self._value[:self._cursor_index] + post_value + self.cursor_pos = self._cursor_index + self.trigger_event("changed", self._value) + elif key_name == "arrow_left": + if event.get_modifier_state("alt") or event.get_modifier_state("ctrl"): + self.cursor_skip_left() + else: + self.cursor_pos -= 1 + elif key_name == "arrow_right": + if event.get_modifier_state("alt") or event.get_modifier_state("ctrl"): + self.cursor_skip_right() + else: + self.cursor_pos += 1 + elif key_name == "escape": + self.blur() + elif key_name == "home": + self.cursor_pos = 0 + elif key_name == "end": + self.cursor_pos = len(self.value) + + self.trigger_event(key_name, self._value) + + def on_keyrepeat(self, event): + """ Internal keyrepeat handler """ + self.on_keydown(event) + + def on_textinput(self, event): + """ Internal textinput handler """ + self._value = self._value[:self._cursor_index] + event.message + \ + self._value[self._cursor_index:] + self.cursor_pos = self._cursor_index + len(event.message) + self.trigger_event("changed", self._value) + + def on_blur(self, event): + """ Internal blur handler """ + self._cursor.hide() + if len(self._value) < 1: + self._placeholder.show() + + self._layout.color = (1, 1, 1, 1) + + def _render_text(self): + """ Internal method to render the text """ + self._text.set_text(self._value) + self._cursor.left = self._text.left + \ + self._text.text_handle.get_char_pos(self._cursor_index) + 1 + max_left = self.width - 15 + + if self._value: + self._placeholder.hide() + else: + if not self.focused: + self._placeholder.show() + + # Scroll if the cursor is outside of the clip bounds + rel_pos = self.get_relative_pos(self._cursor.get_abs_pos()).x + if rel_pos >= max_left: + self._text_scroller.left = min(0, max_left - self._cursor.left) + if rel_pos <= 0: + self._text_scroller.left = min(0, - self._cursor.left - rel_pos) + + def cursor_skip_left(self): + """ Moves the cursor to the left, skipping the previous word """ + left_hand_str = ''.join(reversed(self.value[0:self.cursor_pos])) + match = self.re_skip.match(left_hand_str) + if match is not None: + self.cursor_pos -= match.end() - 1 + else: + self.cursor_pos = 0 + + def cursor_skip_right(self): + """ Moves the cursor to the right, skipping the next word """ + right_hand_str = self.value[self.cursor_pos:] + match = self.re_skip.match(right_hand_str) + if match is not None: + self.cursor_pos += match.end() - 1 + else: + self.cursor_pos = len(self.value) diff --git a/ui/Builtin/LUIInputHandler.py b/ui/Builtin/LUIInputHandler.py new file mode 100644 index 00000000..3b5e2baa --- /dev/null +++ b/ui/Builtin/LUIInputHandler.py @@ -0,0 +1,8 @@ +""" + +This is a wrapper file. It contains no actual implementation + +""" + +from panda3d.lui import LUIInputHandler as __LUIInputHandler +LUIInputHandler = __LUIInputHandler diff --git a/ui/Builtin/LUILabel.py b/ui/Builtin/LUILabel.py new file mode 100644 index 00000000..0eb25a1b --- /dev/null +++ b/ui/Builtin/LUILabel.py @@ -0,0 +1,77 @@ + +from panda3d.lui import LUIText +from LUIObject import LUIObject +from LUIInitialState import LUIInitialState + +__all__ = ["LUILabel"] + +class LUILabel(LUIObject): + + """ A simple label, displaying text. """ + + # Default variables which can be overridden by skins + DEFAULT_COLOR = (0.9, 0.9, 0.9, 1) + DEFAULT_USE_SHADOW = True + + def __init__(self, text="Label", shadow=None, font_size=14, font="label", color=None, wordwrap=False, **kwargs): + """ Creates a new label. If shadow is True, a small text shadow will be + rendered below the actual text. """ + LUIObject.__init__(self) + LUIInitialState.init(self, kwargs) + self._text = LUIText( + self, + text, + font, + font_size, + 0, + 0, + wordwrap + ) + self._text.z_offset = 1 + if color is None: + self.color = LUILabel.DEFAULT_COLOR + else: + self.color = color + if shadow is None: + shadow = LUILabel.DEFAULT_USE_SHADOW + self._have_shadow = shadow + if self._have_shadow: + self._shadow_text = LUIText( + self, + text, + font, + font_size, + 0, + 0, + wordwrap + ) + self._shadow_text.top = 1 + self._shadow_text.color = (0,0,0,0.6) + + def get_text_handle(self): + """ Returns a handle to the internal used LUIText object """ + return self._text + + text_handle = property(get_text_handle) + + def get_text(self): + """ Returns the current text of the label """ + return self._text.text + + def set_text(self, text): + """ Sets the text of the label """ + self._text.text = text + if self._have_shadow: + self._shadow_text.text = text + + text = property(get_text, set_text) + + def get_color(self): + """ Returns the current color of the label's text """ + return self._text.color + + def set_color(self, color): + """ Sets the color of the label's text """ + self._text.color = color + + color = property(get_color, set_color) diff --git a/ui/Builtin/LUILayouts.py b/ui/Builtin/LUILayouts.py new file mode 100644 index 00000000..2210450d --- /dev/null +++ b/ui/Builtin/LUILayouts.py @@ -0,0 +1,105 @@ + +from __future__ import print_function, division + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUIHorizontalLayout import LUIHorizontalLayout + +from LUIInitialState import LUIInitialState + +__all__ = ["LUICornerLayout", "LUIHorizontalStretchedLayout"] + +class LUICornerLayout(LUIObject): + + """ This is a layout which is used to combine 9 sprites to a single sprite, + e.g. used for box shadow or frames.""" + + # List of all sprite identifiers required for the layout + _MODES = ["TR", "Top", "TL", "Right", "Mid", "Left", "BR", "Bottom", "BL"] + + def __init__(self, image_prefix="", **kwargs): + """ Creates a new layout, using the image_prefix as prefix. """ + LUIObject.__init__(self) + self.set_size("100%", "100%") + self._prefix = image_prefix + self._parts = {} + for i in self._MODES: + self._parts[i] = LUISprite(self, "blank", "skin") + self._update_layout() + LUIInitialState.init(self, kwargs) + + def _update_layout(self): + """ Updates the layouts components. """ + for i in self._MODES: + self._parts[i].set_texture(self._prefix + i, "skin", resize=True) + + # Top and Left + self._parts["Top"].width = "100%" + self._parts["Top"].margin = (0, self._parts["TR"].width, 0, self._parts["TL"].width) + + self._parts["Left"].height = "100%" + self._parts["Left"].margin = (self._parts["TL"].height, 0, self._parts["BL"].height, 0) + + # Mid + self._parts["Mid"].set_size("100%", "100%") + self._parts["Mid"].margin = (self._parts["Top"].height, self._parts["Right"].width, + self._parts["Bottom"].height, self._parts["Left"].width) + + # Bottom and Right + self._parts["Bottom"].width = "100%" + self._parts["Bottom"].margin = (0, self._parts["BR"].width, 0, self._parts["BL"].width) + self._parts["Bottom"].bottom = 0 + + self._parts["Right"].height = "100%" + self._parts["Right"].margin = (self._parts["TR"].height, 0, self._parts["BR"].width, 0) + self._parts["Right"].right = 0 + + # Corners + self._parts["TL"].top_left = 0, 0 + self._parts["TR"].top_right = 0, 0 + self._parts["BL"].bottom_left = 0, 0 + self._parts["BR"].bottom_right = 0, 0 + + def set_prefix(self, prefix): + """ Changes the texture of the layout """ + self._prefix = prefix + self._update_layout() + + def get_prefix(self): + """ Returns the layouts texture prefix """ + return self._prefix + + prefix = property(get_prefix, set_prefix) + + +class LUIHorizontalStretchedLayout(LUIObject): + + """ A layout which takes 3 sprites, a left sprite, a right sprite, and a + middle sprite. While the left and right sprites remain untouched, the middle + one will be stretched to fit the layout """ + + def __init__(self, parent=None, prefix="ButtonDefault", **kwargs): + LUIObject.__init__(self) + self._layout = LUIHorizontalLayout(self, spacing=0) + self._layout.width = "100%" + self._sprite_left = LUISprite(self._layout.cell(), "blank", "skin") + self._sprite_mid = LUISprite(self._layout.cell('*'), "blank", "skin") + self._sprite_right = LUISprite(self._layout.cell(), "blank", "skin") + if parent is not None: + self.parent = parent + self.prefix = prefix + LUIInitialState.init(self, kwargs) + + def set_prefix(self, prefix): + """ Sets the layout prefix, this controls which sprites will be used """ + self._sprite_left.set_texture(prefix + "_Left", "skin") + self._sprite_mid.set_texture(prefix, "skin") + self._sprite_right.set_texture(prefix + "_Right", "skin") + self._sprite_mid.width = "100%" + self._prefix = prefix + + def get_prefix(self): + """ Returns the layout prefix """ + return self._prefix + + prefix = property(get_prefix, set_prefix) diff --git a/ui/Builtin/LUIObject.py b/ui/Builtin/LUIObject.py new file mode 100644 index 00000000..c666efe2 --- /dev/null +++ b/ui/Builtin/LUIObject.py @@ -0,0 +1,18 @@ +""" + +This is a wrapper file. It contains no actual implementation + +""" + +from panda3d.lui import LUIObject as _LUIObject +from LUIInitialState import LUIInitialState + +__all__ = ["LUIObject"] + +class LUIObject(_LUIObject): + """ This is a wrapper class for the C++ LUIObject class, to be able to + use it in a more convenient way """ + + def __init__(self, *args, **kwargs): + _LUIObject.__init__(self, *args) + LUIInitialState.init(self, kwargs) diff --git a/ui/Builtin/LUIProgressbar.py b/ui/Builtin/LUIProgressbar.py new file mode 100644 index 00000000..8f744eaf --- /dev/null +++ b/ui/Builtin/LUIProgressbar.py @@ -0,0 +1,72 @@ + +from LUIObject import LUIObject +from LUISprite import LUISprite + +from LUILayouts import LUIHorizontalStretchedLayout +from LUILabel import LUILabel + +class LUIProgressbar(LUIObject): + + """ A simple progress bar """ + + def __init__(self, parent=None, width=200, value=50, show_label=True): + """ Constructs a new progress bar. If show_label is True, a label indicating + the current progress is shown """ + LUIObject.__init__(self) + self.set_width(width) + + self._bg_layout = LUIHorizontalStretchedLayout( + parent=self, prefix="ProgressbarBg", width="100%") + + self._fg_left = LUISprite(self, "ProgressbarFg_Left", "skin") + self._fg_mid = LUISprite(self, "ProgressbarFg", "skin") + self._fg_right = LUISprite(self, "ProgressbarFg_Right", "skin") + self._fg_finish = LUISprite(self, "ProgressbarFg_Finish", "skin") + + self._show_label = show_label + self._progress_pixel = 0 + self._fg_finish.right = 0 + + if self._show_label: + self._progress_label = LUILabel(parent=self, text=u"33 %") + self._progress_label.centered = (True, True) + + self.set_value(value) + self._update_progress() + + if parent is not None: + self.parent = parent + + def get_value(self): + """ Returns the current value of the progress bar """ + return (self._progress_pixel / self.width * 100.0) + + def set_value(self, val): + """ Sets the value of the progress bar """ + val = max(0, min(100, val)) + self._progress_pixel = int(val / 100.0 * self.width) + self._update_progress() + + value = property(get_value, set_value) + + def _update_progress(self): + """ Internal method to update the progressbar """ + self._fg_finish.hide() + + if self._progress_pixel <= self._fg_left.width + self._fg_right.width: + self._fg_mid.hide() + self._fg_right.left = self._fg_left.width + else: + self._fg_mid.show() + self._fg_mid.left = self._fg_left.width + self._fg_mid.width = self._progress_pixel - self._fg_right.width - self._fg_left.width + self._fg_right.left = self._fg_mid.left + self._fg_mid.width + + if self._progress_pixel >= self.width - self._fg_right.width: + self._fg_finish.show() + self._fg_finish.right = - (self.width - self._progress_pixel) + self._fg_finish.clip_bounds = (0, self.width - self._progress_pixel, 0, 0) + + if self._show_label: + percentage = self._progress_pixel / self.width * 100.0 + self._progress_label.set_text("{} %".format(int(percentage))) diff --git a/ui/Builtin/LUIRadiobox.py b/ui/Builtin/LUIRadiobox.py new file mode 100644 index 00000000..9d7d8bf2 --- /dev/null +++ b/ui/Builtin/LUIRadiobox.py @@ -0,0 +1,91 @@ + +from __future__ import division + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUIInitialState import LUIInitialState +from LUILabel import LUILabel + +class LUIRadiobox(LUIObject): + + """ A radiobox which can be used in combination with a LUIRadioboxGroup """ + + def __init__(self, parent=None, group=None, value=None, active=False, label=u"Radiobox", **kwargs): + """ Constructs a new radiobox. group should be a handle to a LUIRadioboxGroup. + value will be the value returned by group.value, in case the box was + selected. By default, the radiobox is not active. """ + assert group is not None, "LUIRadiobox needs a LUIRadioboxGroup!" + LUIObject.__init__(self, x=0, y=0, solid=True) + self._sprite = LUISprite(self, "Radiobox_Default", "skin") + self._label = LUILabel(parent=self, text=label, margin=(0, 0, 0, 23), + center_vertical=True) + self._value = value + self._active = False + self._hovered = False + self._group = group + self._group.register_box(self) + if active: + self.set_active() + if parent: + self.parent = parent + LUIInitialState.init(self, kwargs) + + def on_click(self, event): + """ Internal onclick handler. Do not override. """ + self.set_active() + + def on_mouseover(self, event): + """ Internal mouseover handler """ + self._hovered = True + self._update_sprite() + + def on_mouseout(self, event): + """ Internal mouseout handler """ + self._hovered = False + self._update_sprite() + + def set_active(self): + """ Internal function to set the radiobox active """ + if self._group is not None: + self._group.set_active_box(self) + else: + self._update_state(True) + + def get_value(self): + """ Returns the value of the radiobox """ + return self._value + + def set_value(self, value): + """ Sets the value of the radiobox """ + self._value = value + + value = property(get_value, set_value) + + def get_label(self): + """ Returns a handle to the label, so it can be modified (e.g. change + its text) """ + return self._label + + label = property(get_label) + + def _update_state(self, active): + """ Internal method to update the state of the radiobox. Called by the + LUIRadioboxGroup """ + self._active = active + self.trigger_event("changed") + self._update_sprite() + + def on_mousedown(self, event): + """ Internal onmousedown handler. Do not override. """ + self._sprite.color = (0.86,0.86,0.86,1.0) + + def on_mouseup(self, event): + """ Internal onmouseup handler. Do not override. """ + self._sprite.color = (1,1,1,1) + + def _update_sprite(self): + """ Internal function to update the sprite of the radiobox """ + img = "Radiobox_Active" if self._active else "Radiobox_Default" + if self._hovered: + img += "Hover" + self._sprite.set_texture(img, "skin") diff --git a/ui/Builtin/LUIRadioboxGroup.py b/ui/Builtin/LUIRadioboxGroup.py new file mode 100644 index 00000000..94117c79 --- /dev/null +++ b/ui/Builtin/LUIRadioboxGroup.py @@ -0,0 +1,41 @@ + +from LUIObject import LUIObject + +class LUIRadioboxGroup(LUIObject): + + """ Simple helper to group a bunch of LUIRadiobox and ensure only one is + checked at one timem """ + + def __init__(self): + """ Constructs a new group without any radioboxes inside """ + self._boxes = [] + self._selected_box = None + + def register_box(self, box): + """ Registers a box to the collection """ + if box not in self._boxes: + self._boxes.append(box) + + def set_active_box(self, active_box): + """ Internal function to set the active box """ + for box in self._boxes: + if box is not active_box: + box._update_state(False) + else: + box._update_state(True) + self._selected_box = active_box + + def get_active_box(self): + """ Returns the current selected box """ + return self._selected_box + + active_box = property(get_active_box, set_active_box) + + def get_active_value(self): + """ Returns the value of the current selected box (or None if none is + selected) """ + if self._selected_box is None: + return None + return self._selected_box.get_value() + + active_value = property(get_active_value) diff --git a/ui/Builtin/LUIRegion.py b/ui/Builtin/LUIRegion.py new file mode 100644 index 00000000..b6bbb3d4 --- /dev/null +++ b/ui/Builtin/LUIRegion.py @@ -0,0 +1,8 @@ +""" + +This is a wrapper file. It contains no actual implementation + +""" + +from panda3d.lui import LUIRegion as __LUIRegion +LUIRegion = __LUIRegion diff --git a/ui/Builtin/LUIRoot.py b/ui/Builtin/LUIRoot.py new file mode 100644 index 00000000..79f83f96 --- /dev/null +++ b/ui/Builtin/LUIRoot.py @@ -0,0 +1,8 @@ +""" + +This is a wrapper file. It contains no actual implementation + +""" + +from panda3d.lui import LUIRoot as __LUIRoot +LUIRoot = __LUIRoot diff --git a/ui/Builtin/LUIScrollableRegion.py b/ui/Builtin/LUIScrollableRegion.py new file mode 100644 index 00000000..08105cae --- /dev/null +++ b/ui/Builtin/LUIScrollableRegion.py @@ -0,0 +1,155 @@ + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUIInitialState import LUIInitialState +from LUILayouts import LUIHorizontalStretchedLayout + +class LUIScrollableRegion(LUIObject): + + """ Scrollable region, reparent elements to the .content_node to make them + scroll. """ + + def __init__(self, parent=None, width=100, height=100, padding=10, **kwargs): + LUIObject.__init__(self) + self.set_size(width, height) + self._content_parent = LUIObject(self) + self._content_parent.set_size("100%", "100%") + self._content_parent.clip_bounds = (0,0,0,0) + + self._content_clip = LUIObject(self._content_parent, x=padding, y=padding) + self._content_clip.set_size("100%", "100%") + + self._content_scroller = LUIObject(self._content_clip) + self._content_scroller.width = "100%" + + + self._scrollbar = LUIObject(self, x=0, y=0, w=20) + self._scrollbar.height = "100%" + self._scrollbar.right = -10 + + self._scrollbar_bg = LUISprite(self._scrollbar, "blank", "skin") + self._scrollbar_bg.color = (1,1,1,0.05) + self._scrollbar_bg.set_size(3, "100%") + self._scrollbar_bg.center_horizontal = True + + # Handle + self._scrollbar_handle = LUIObject(self._scrollbar, x=5, y=0, w=10) + self._scroll_handle_top = LUISprite(self._scrollbar_handle, "ScrollbarHandle_Top", "skin") + self._scroll_handle_mid = LUISprite(self._scrollbar_handle, "ScrollbarHandle", "skin") + self._scroll_handle_bottom = LUISprite(self._scrollbar_handle, "ScrollbarHandle_Bottom", "skin") + + self._scrollbar_handle.solid = True + self._scrollbar.solid = True + + self._scrollbar_handle.bind("mousedown", self._start_scrolling) + self._scrollbar_handle.bind("mouseup", self._stop_scrolling) + self._scrollbar.bind("mousedown", self._on_bar_click) + self._scrollbar.bind("mouseup", self._stop_scrolling) + + self._handle_dragging = False + self._drag_start_y = 0 + + self._scroll_top_position = 0 + self._content_height = 400 + + # Scroll shadow + self._scroll_shadow_top = LUIHorizontalStretchedLayout(parent=self, prefix="ScrollShadowTop", width="100%") + self._scroll_shadow_bottom = LUIHorizontalStretchedLayout(parent=self, prefix="ScrollShadowBottom", width="100%") + self._scroll_shadow_bottom.bottom = 0 + + self._handle_height = 100 + + if parent is not None: + self.parent = parent + + LUIInitialState.init(self, kwargs) + self.content_node = self._content_scroller + taskMgr.doMethodLater(0.05, lambda task: self._update(), "update_scrollbar") + + def _on_bar_click(self, event): + """ Internal handler when the user clicks on the scroll bar """ + self._scroll_to_bar_pixels(event.coordinates.y - self._scrollbar.abs_pos.y - self._handle_height / 2.0) + self._update() + self._start_scrolling(event) + + def _start_scrolling(self, event): + """ Internal method when we start scrolling """ + self.request_focus() + if not self._handle_dragging: + self._drag_start_y = event.coordinates.y + self._handle_dragging = True + + def _stop_scrolling(self, event): + """ Internal handler when we should stop scrolling """ + if self._handle_dragging: + self._handle_dragging = False + self.blur() + + def _scroll_to_bar_pixels(self, pixels): + """ Internal method to convert from pixels to a relative position """ + offset = pixels * self._content_height / self.height + self._scroll_top_position = offset + self._scroll_top_position = max(0, min(self._content_height - self._content_clip.height, self._scroll_top_position)) + + def on_tick(self, event): + """ Internal on tick handler """ + if self._handle_dragging: + scroll_abs_pos = self._scrollbar.abs_pos + clamped_coord_y = max(scroll_abs_pos.y, min(scroll_abs_pos.y + self.height, event.coordinates.y)) + offset = clamped_coord_y - self._drag_start_y + self._drag_start_y = clamped_coord_y + self._scroll_to_bar_pixels(self._scroll_top_position/self._content_height*self.height + offset) + self._update() + + def _set_handle_height(self, height): + """ Internal method to set the scrollbar height """ + self._scroll_handle_mid.top = float(self._scroll_handle_top.height) + + self._scroll_handle_mid.height = max(0.0, height - self._scroll_handle_top.height - self._scroll_handle_bottom.height) + self._scroll_handle_bottom.top = self._scroll_handle_mid.height + self._scroll_handle_mid.top + self._handle_height = height + + def _update(self): + """ Internal method to update the scroll bar """ + self._content_height = max(1, self._content_scroller.get_height() + 20) + self._content_scroller.top = -self._scroll_top_position + scrollbar_height = max(0.1, min(1.0, self._content_clip.height / self._content_height)) + scrollbar_height_px = scrollbar_height * self.height + + self._set_handle_height(scrollbar_height_px) + self._scrollbar_handle.top = self._scroll_top_position / self._content_height * self.height + + top_alpha = max(0.0, min(1.0, self._scroll_top_position / 50.0)) + bottom_alpha = max(0.0, min(1.0, (self._content_height - self._scroll_top_position - self._content_clip.height) / 50.0 )) + self._scroll_shadow_top.color = (1,1,1,top_alpha) + self._scroll_shadow_bottom.color = (1,1,1,bottom_alpha) + + if self._content_height <= self.height: + self._scrollbar_handle.hide() + else: + self._scrollbar_handle.show() + + def on_element_added(self): + taskMgr.doMethodLater(0.05, lambda task: self._update(), "update_layout") + + def get_scroll_percentage(self): + """ Returns the current scroll height in percentage from 0 to 1 """ + return self._scroll_top_position / max(1, self._content_height - self._content_clip.height) + + def set_scroll_percentage(self, percentage): + """ Sets the scroll position in percentage, 0 means top and 1 means bottom """ + percentage = max(0.0, min(1.0, percentage)) + pixels = max(0.0, self._content_height - self._content_clip.height) * percentage + self._scroll_top_position = pixels + self._update() + + scroll_percentage = property(get_scroll_percentage, set_scroll_percentage) + + def scroll_to_bottom(self): + """ Scrolls to the bottom of the frame """ + taskMgr.doMethodLater(0.07, lambda task: self.set_scroll_percentage(1.0), "scroll_to_bottom") + + def scroll_to_top(self): + """ Scrolls to the top of the frame """ + taskMgr.doMethodLater(0.07, lambda task: self.set_scroll_percentage(0.0), "scroll_to_top") + diff --git a/ui/Builtin/LUISelectbox.py b/ui/Builtin/LUISelectbox.py new file mode 100644 index 00000000..f2fa4a77 --- /dev/null +++ b/ui/Builtin/LUISelectbox.py @@ -0,0 +1,207 @@ + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUILabel import LUILabel +from LUILayouts import LUICornerLayout, LUIHorizontalStretchedLayout +from LUIInitialState import LUIInitialState + +from functools import partial + +__all__ = ["LUISelectbox"] + +class LUISelectbox(LUIObject): + + """ Selectbox widget, showing several options whereas the user can select + only one. """ + + def __init__(self, width=200, options=None, selected_option=None, **kwargs): + """ Constructs a new selectbox with a given width """ + LUIObject.__init__(self, x=0, y=0, w=width+4, solid=True) + LUIInitialState.init(self, kwargs) + + # The selectbox has a small border, to correct this we move it + self.margin.left = -2 + + self._bg_layout = LUIHorizontalStretchedLayout(parent=self, prefix="Selectbox", width="100%") + + self._label_container = LUIObject(self, x=10, y=0) + self._label_container.set_size("100%", "100%") + self._label_container.clip_bounds = (0,0,0,0) + self._label = LUILabel(parent=self._label_container, text=u"Select an option ..") + self._label.center_vertical = True + + self._drop_menu = LUISelectdrop(parent=self, width=width) + self._drop_menu.top = self._bg_layout._sprite_right.height - 7 + self._drop_menu.topmost = True + + self._drop_open = False + self._drop_menu.hide() + + self._options = [] + self._current_option_id = None + + if options is not None: + self._options = options + + self._select_option(selected_option) + + def get_selected_option(self): + """ Returns the selected option """ + return self._current_option_id + + def set_selected_option(self, option_id): + """ Sets the selected option """ + raise NotImplementedError() + + selected_option = property(get_selected_option, set_selected_option) + + def _render_options(self): + """ Internal method to render all available options """ + self._drop_menu._render_options(self._options) + + def get_options(self): + """ Returns the list of options """ + return self._options + + def set_options(self, options): + """ Sets the list of options, options should be a list containing entries + whereas each entry is a tuple in the format (option_id, option_label). + The option ID can be an arbitrary object, and will not get modified. """ + self._options = options + self._current_option_id = None + self._render_options() + + options = property(get_options, set_options) + + def _select_option(self, opt_id): + """ Internal method to select an option """ + self._label.alpha = 1.0 + for elem_opt_id, opt_val in self._options: + if opt_id == elem_opt_id: + self._label.text = opt_val + self._current_option_id = opt_id + return + self._label.alpha = 0.3 + + # def on_mouseover(self, event): + # """ Internal handle when the select-knob was hovered """ + # self._bg_layout.color = (0.9,0.9,0.9,1.0) + + # def on_mouseout(self, event): + # """ Internal handle when the select-knob was no longer hovered """ + # self._bg_layout.color = (1,1,1,1.0) + + def on_click(self, event): + """ On-Click handler """ + self.request_focus() + if self._drop_open: + self._close_drop() + else: + self._open_drop() + + def on_mousedown(self, event): + """ Mousedown handler """ + self._bg_layout.alpha = 0.9 + + def on_mouseup(self, event): + """ Mouseup handler """ + self._bg_layout.alpha = 1 + + def on_blur(self, event): + """ Internal handler when the selectbox lost focus """ + if not self._drop_menu.focused: + self._close_drop() + + def _open_drop(self): + """ Internal method to show the dropdown menu """ + if not self._drop_open: + self._render_options() + self._drop_menu.show() + self.request_focus() + self._drop_open = True + + def _close_drop(self): + """ Internal method to close the dropdown menu """ + if self._drop_open: + self._drop_menu.hide() + self._drop_open = False + + def _on_option_selected(self, opt_id): + """ Internal method when an option got selected """ + self._select_option(opt_id) + self._close_drop() + + +class LUISelectdrop(LUIObject): + + """ Internal class used by the selectbox, representing the dropdown menu """ + + def __init__(self, parent, width=200): + LUIObject.__init__(self, x=0, y=0, w=width, h=1, solid=True) + + self._layout = LUICornerLayout(parent=self, image_prefix="Selectdrop_", + width=width + 10, height=100) + self._layout.margin.left = -3 + + self._opener = LUISprite(self, "SelectboxOpen_Right", "skin") + self._opener.right = -4 + self._opener.top = -25 + self._opener.z_offset = 3 + + self._container = LUIObject(self._layout, 0, 0, 0, 0) + self._container.width = self.width + self._container.clip_bounds = (0,0,0,0) + self._container.left = 5 + self._container.solid = True + self._container.bind("mousedown", lambda *args: self.request_focus()) + + self._selectbox = parent + self._option_focus = False + self.parent = self._selectbox + + def _on_opt_over(self, event): + """ Inernal handler when an option got hovered """ + event.sender.color = (0,0,0,0.1) + + def _on_opt_out(self, event): + """ Inernal handler when an option got no longer hovered """ + event.sender.color = (0,0,0,0) + + def _on_opt_click(self, opt_id, event): + """ Internal handler when an option got clicked """ + self._selectbox._on_option_selected(opt_id) + + def _render_options(self, options): + """ Internal method to update the options """ + num_visible_options = min(30, len(options)) + offset_top = 6 + self._layout.height = num_visible_options * 30 + offset_top + 11 + self._container.height = num_visible_options * 30 + offset_top + 1 + self._container.remove_all_children() + + current_y = offset_top + for opt_id, opt_val in options: + opt_container = LUIObject(self._container, x=0, y=current_y, w=self._container.width - 30, h=30) + + opt_bg = LUISprite(opt_container, "blank", "skin") + opt_bg.width = self._container.width + opt_bg.height = opt_container.height + opt_bg.color = (0,0,0,0) + opt_bg.bind("mouseover", self._on_opt_over) + opt_bg.bind("mouseout", self._on_opt_out) + opt_bg.bind("mousedown", lambda *args: self.request_focus()) + opt_bg.bind("click", partial(self._on_opt_click, opt_id)) + opt_bg.solid = True + + opt_label = LUILabel(parent=opt_container, text=opt_val) + opt_label.top = 8 + opt_label.left = 8 + + if opt_id == self._selectbox.selected_option: + opt_label.color = (0.6, 0.9, 0.4, 1.0) + + divider = LUISprite(opt_container, "SelectdropDivider", "skin") + divider.top = 30 - divider.height / 2 + divider.width = self._container.width + + current_y += 30 diff --git a/ui/Builtin/LUISkin.py b/ui/Builtin/LUISkin.py new file mode 100644 index 00000000..0f0e774b --- /dev/null +++ b/ui/Builtin/LUISkin.py @@ -0,0 +1,53 @@ + +import os +from os.path import join + +from panda3d.core import Filename +from panda3d.lui import LUIFontPool, LUIAtlasPool + +class LUISkin: + + """ Abstract class, each skin derives from this class """ + + skin_location = "" + + def __init__(self): + pass + + def load(self): + """ Skins should override this. Each skin should at least provide the fonts + 'default' and 'label', and at least one atlas named 'skin' """ + raise NotImplementedError() + + def get_resource(self, pth): + """ Turns a relative path into an absolute one, using the skin_location """ + return Filename.from_os_specific(join(self.skin_location, pth)).get_fullpath() + + +class LUIDefaultSkin(LUISkin): + + """ The default skin which comes with LUI """ + + skin_location = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../Skins/Default/") + + def __init__(self): + pass + + def load(self): + LUIFontPool.get_global_ptr().register_font( + "default", loader.loadFont(self.get_resource("font/SourceSansPro-Semibold.ttf"))) + + labelFont = loader.loadFont(self.get_resource("font/SourceSansPro-Semibold.ttf")) + labelFont.setPixelsPerUnit(32) + + LUIFontPool.get_global_ptr().register_font( + "label", labelFont) + + headerFont = loader.loadFont(self.get_resource("font/SourceSansPro-Light.ttf")) + headerFont.setPixelsPerUnit(80) + + LUIFontPool.get_global_ptr().register_font("header", headerFont) + + LUIAtlasPool.get_global_ptr().load_atlas("skin", + self.get_resource("res/atlas.txt"), + self.get_resource("res/atlas.png")) diff --git a/ui/Builtin/LUISlider.py b/ui/Builtin/LUISlider.py new file mode 100644 index 00000000..ca534362 --- /dev/null +++ b/ui/Builtin/LUISlider.py @@ -0,0 +1,214 @@ + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUIInitialState import LUIInitialState +from LUILayouts import LUIHorizontalStretchedLayout + +class LUISlider(LUIObject): + + """ Slider which can be used to control values """ + KNOB_Y_OFFSET = -5.0 + + def __init__(self, parent=None, filled=True, min_value=0.0, max_value=1.0, width=100.0, value=None, **kwargs): + """ Constructs a new slider. If filled is True, the part behind the knob + will be solid """ + LUIObject.__init__(self, x=0, y=0, solid=True) + self.set_width(width) + self._knob = LUISprite(self, "SliderKnob", "skin") + self._knob.z_offset = 2 + self._knob.solid = True + + # Construct the background + self._slider_bg = LUIHorizontalStretchedLayout(parent=self, prefix="SliderBg", center_vertical=True, width="100%", margin=(-1, 0, 0, 0)) + + self._filled = filled + self._min_value = min_value + self._max_value = max_value + + self._side_margin = self._knob.width / 4 + self._effective_width = self.width - 2 * self._side_margin + + if self._filled: + self._slider_fill = LUIObject(self) + self._fill_left = LUISprite(self._slider_fill, "SliderBgFill_Left", "skin") + self._fill_mid = LUISprite(self._slider_fill, "SliderBgFill", "skin") + self._fill_mid.left = self._fill_left.width + self._slider_fill.z_offset = 1 + self._slider_fill.center_vertical = True + self._slider_fill.width = "100%" + self._slider_fill.height = "100%" + + if parent is not None: + self.parent = parent + + # Handle various events + self._knob.bind("mousedown", self._start_drag) + self._knob.bind("mousemove", self._update_drag) + self._knob.bind("mouseup", self._stop_drag) + self._knob.bind("keydown", self._on_keydown) + self._knob.bind("blur", self._stop_drag) + self._knob.bind("keyrepeat", self._on_keydown) + + self._drag_start_pos = None + self._dragging = False + self._drag_start_val = 0 + self.current_val = 10 + + # Set initial value + if value is None: + self.set_value( (self._min_value + self._max_value) / 2.0 ) + else: + self.set_value(value) + + self._update_knob() + + LUIInitialState.init(self, kwargs) + self._refresh_layout() + + def on_click(self, event): + """ Internal on click handler """ + # I don't like this behaviour + # relative_pos = self.get_relative_pos(event.coordinates) + # if not self._dragging: + # self._set_current_val(relative_pos.x) + + def _update_knob(self): + """ Internal method to update the slider knob """ + self._knob.left = self.current_val - (self._knob.width / 2) + self._side_margin + if self._filled: + knob_center = self._knob.left + (self._knob.width / 2) + fill_len = max(0, knob_center - self._fill_left.width) + self._fill_mid.width = fill_len + if hasattr(self._slider_fill, 'left'): + self._slider_fill.left = 0 + + def _refresh_layout(self): + """Recalculate slider layout after size changes.""" + # Guard for early init before internal parts are created + if not hasattr(self, "_knob"): + return + try: + # Use half knob width so knob center aligns with bar ends + self._side_margin = self._knob.width / 2 + except Exception: + self._side_margin = 0 + try: + self._effective_width = max(1, self.width - (self._knob.width if hasattr(self, "_knob") else 0)) + except Exception: + self._effective_width = 1 + + if hasattr(self, "_slider_bg"): + if hasattr(self._slider_bg, 'width'): + self._slider_bg.width = "100%" + if hasattr(self._slider_bg, 'height'): + self._slider_bg.height = "100%" + if hasattr(self._slider_bg, 'center_vertical'): + self._slider_bg.center_vertical = True + if hasattr(self._knob, 'center_vertical'): + self._knob.center_vertical = True + # Explicit vertical centering for knob to avoid mismatch after resize + try: + base_h = self.height + base_top = 0 + if hasattr(self, "_slider_bg") and self._slider_bg is not None: + bg_h = getattr(self._slider_bg, "height", None) + if bg_h is not None and bg_h != "100%": + base_h = bg_h + base_top = getattr(self._slider_bg, "top", 0) or 0 + if hasattr(self._knob, "height"): + # Visual offset compensates for the skin's knob pivot + self._knob.top = (base_h - self._knob.height) / 2.0 + self.KNOB_Y_OFFSET + except Exception: + pass + if self._filled and hasattr(self, '_slider_fill'): + self._slider_fill.center_vertical = True + try: + base_h = self.height + base_top = 0 + if hasattr(self, "_slider_bg") and self._slider_bg is not None: + bg_h = getattr(self._slider_bg, "height", None) + if bg_h is not None and bg_h != "100%": + base_h = bg_h + base_top = getattr(self._slider_bg, "top", 0) or 0 + if hasattr(self._slider_fill, 'height'): + self._slider_fill.top = base_top + (base_h - self._slider_fill.height) / 2.0 + self.KNOB_Y_OFFSET + except Exception: + pass + if hasattr(self._slider_fill, 'width'): + self._slider_fill.width = "100%" + if hasattr(self._slider_fill, 'height'): + self._slider_fill.height = "100%" + + try: + self.current_val = max(0, min(self.current_val, self._effective_width)) + except Exception: + pass + self._update_knob() + + def set_width(self, width): + """Set slider width and refresh layout.""" + self.width = width + if hasattr(self, "_slider_bg"): + self._refresh_layout() + + def set_height(self, height): + """Set slider height and refresh layout.""" + self.height = height + if hasattr(self, "_slider_bg"): + self._refresh_layout() + + def _set_current_val(self, pixels): + """ Internal method to set the current value in pixels """ + pixels = max(0, min(self._effective_width, pixels)) + self.current_val = pixels + self.trigger_event("changed") + self._update_knob() + + def _start_drag(self, event): + """ Internal drag start handler """ + self._knob.request_focus() + if not self._dragging: + self._drag_start_pos = event.coordinates + self._dragging = True + self._drag_start_val = self.current_val + self._knob.color = (0.8,0.8,0.8,1.0) + + def set_value(self, value): + """ Sets the value of the slider, should be between minimum and maximum. """ + scaled = (float(value) - float(self._min_value)) \ + / (float(self._max_value) - float(self._min_value)) \ + * self._effective_width + self._set_current_val(scaled) + + def get_value(self): + """ Returns the current value of the slider """ + return (self.current_val / float(self._effective_width)) \ + * (float(self._max_value) - float(self._min_value)) \ + + self._min_value + + value = property(get_value, set_value) + + def _on_keydown(self, event): + """ Internal keydown handler """ + if event.message == "arrow_right": + self._set_current_val(self.current_val + 2) + elif event.message == "arrow_left": + self._set_current_val(self.current_val - 2) + elif event.message == "escape": + self.current_val = self._drag_start_val + self._stop_drag(event) + self._update_knob() + + def _update_drag(self, event): + """ Internal drag handler """ + if self._dragging: + dragOffset = event.coordinates.x - self._drag_start_pos.x + finalValue = self._drag_start_val + dragOffset + self._set_current_val(finalValue) + + def _stop_drag(self, event): + """ Internal drag stop handelr """ + self._drag_start_pos = None + self._dragging = False + self._drag_start_val = self.current_val + self._knob.color = (1,1,1,1) diff --git a/ui/Builtin/LUISprite.py b/ui/Builtin/LUISprite.py new file mode 100644 index 00000000..b04b4229 --- /dev/null +++ b/ui/Builtin/LUISprite.py @@ -0,0 +1,18 @@ +""" + +This is a wrapper file. It contains no actual implementation + +""" + +from panda3d.lui import LUISprite as _LUISprite +from LUIInitialState import LUIInitialState + +__all__ = ["LUISprite"] + +class LUISprite(_LUISprite): + """ This is a wrapper class for the C++ LUISprite class, to be able to + use it in a more convenient way """ + + def __init__(self, *args, **kwargs): + _LUISprite.__init__(self, *args) + LUIInitialState.init(self, kwargs) diff --git a/ui/Builtin/LUISpriteButton.py b/ui/Builtin/LUISpriteButton.py new file mode 100644 index 00000000..348a197a --- /dev/null +++ b/ui/Builtin/LUISpriteButton.py @@ -0,0 +1,30 @@ + +from LUIObject import LUIObject +from LUISprite import LUISprite +from LUIInitialState import LUIInitialState + +class LUISpriteButton(LUIObject): + + """ Simple button that uses only two images: Default and focus. """ + + def __init__(self, template="ButtonDefault", **kwargs): + LUIObject.__init__(self, x=0, y=0, solid=True) + self._template = template + self._button_sprite = LUISprite(self, template, "skin") + if 'width' in kwargs: + self._button_sprite.width = kwargs['width'] + if 'height' in kwargs: + self._button_sprite.height = kwargs['height'] + LUIInitialState.init(self, kwargs) + + def on_mousedown(self, event): + """ Internal on_mousedown handler. Do not override """ + self._button_sprite.set_texture(self.template + "Focus", "skin", resize=False) + + def on_mouseup(self, event): + """ Internal on_mouseup handler. Do not override """ + self._button_sprite.set_texture(self.template, "skin", resize=False) + + def on_click(self, event): + """ Internal onclick handler. Do not override """ + self.trigger_event("changed") diff --git a/ui/Builtin/LUITabbedFrame.py b/ui/Builtin/LUITabbedFrame.py new file mode 100644 index 00000000..4e210f58 --- /dev/null +++ b/ui/Builtin/LUITabbedFrame.py @@ -0,0 +1,87 @@ +from LUIFrame import LUIFrame +from LUILabel import LUILabel +from LUIObject import LUIObject +from LUIVerticalLayout import LUIVerticalLayout +from LUIHorizontalLayout import LUIHorizontalLayout + +class LUITabbedFrame(LUIFrame): + def __init__(self, **kwargs): + super(LUITabbedFrame, self).__init__(**kwargs) + + # The main window layout + bar_spacing = kwargs.get('bar_spacing', 3) + self.root_layout = LUIVerticalLayout(parent = self, + spacing = bar_spacing) + self.root_layout.height = "100%" + self.root_layout.width = "100%" + self.root_layout.margin = 0 + + # The header bar + header_spacing = kwargs.get('header_spacing', 3) + self.header_bar = LUIHorizontalLayout(parent = self.root_layout.cell("?"), + spacing = header_spacing) + self.root_layout.add(self.header_bar, "?") + self.header_to_frame = {} + self.current_frame = None + + # The main window contents + self.main_frame = LUIObject() + self.main_frame.height = "100%" + self.main_frame.width = "100%" + self.main_frame.margin = 0 + # self.main_frame.padding = 0 + self.root_layout.add(self.main_frame, "*") + self.bind("expose", self.on_expose) + self.bind("unexpose", self.on_unexpose) + + def add(self, header, frame): + # header + if isinstance(header, str): + header = LUILabel(text = header) + self.header_bar.add(header, "?") + self.header_to_frame[header] = frame + header.solid = True + header.bind("click", self._change_to_tab) + # Frame + frame.parent = self.main_frame + frame.width = "100%" + frame.height = "100%" + # Put frame in front + if self.current_frame is None: + self.current_frame = frame + self.current_frame.show() + else: + frame.hide() + return header + + def _find_header_index(self, header): + for idx, child in enumerate(self.header_bar.children): + if any([grandchild == header for grandchild in child.children]): + break + else: + raise ValueError("Given object is not a header bar item.") + return idx + + def remove(self, header): + idx = self._find_header_index(header) + self.header_bar.remove_cell(idx) + frame = self.header_to_frame[header] + frame.parent = None + del self.header_to_frame[header] + if self.current_frame == frame: + self.current_frame = None + + def _change_to_tab(self, lui_event): + header = lui_event.sender + if self.current_frame is not None: + self.current_frame.trigger_event("unexpose") + self.current_frame.hide() + self.current_frame = self.header_to_frame[header] + self.current_frame.show() + self.current_frame.trigger_event("expose") + + def on_expose(self, event): + self.current_frame.trigger_event("expose") + + def on_unexpose(self, event): + self.current_frame.trigger_event("unexpose") diff --git a/ui/Builtin/LUIVerticalLayout.py b/ui/Builtin/LUIVerticalLayout.py new file mode 100644 index 00000000..fd15f81c --- /dev/null +++ b/ui/Builtin/LUIVerticalLayout.py @@ -0,0 +1,20 @@ +""" + +This is a wrapper file. It contains no actual implementation + +""" + +from panda3d.lui import LUIVerticalLayout as _LUIVerticalLayout +from LUIInitialState import LUIInitialState + +__all__ = ["LUIVerticalLayout"] + + +class LUIVerticalLayout(_LUIVerticalLayout): + """ This is a wrapper class for the C++ LUIVerticalLayout class, to be + able to use it in a more convenient way. It leverages LUIInitialState + to be able to pass arbitrary keyword arguments. """ + + def __init__(self, parent=None, spacing=0.0, **kwargs): + _LUIVerticalLayout.__init__(self, parent, spacing) + LUIInitialState.init(self, kwargs) diff --git a/ui/Builtin/RectTransform.py b/ui/Builtin/RectTransform.py new file mode 100644 index 00000000..e1d98028 --- /dev/null +++ b/ui/Builtin/RectTransform.py @@ -0,0 +1,85 @@ +"""RectTransform - 一个轻量级的 Unity-like RectTransform 实现(Python 层 MVP) + +提供 anchors/pivot/anchored_position/size_delta 的数据结构和 +把相对锚点映射到像素矩形的计算方法。 + +此实现不修改底层 C++ LUI,而是在 Python 层计算并把结果应用到 +`LUIObject` 的 `left/top/width/height` 属性上。 +""" +from typing import Tuple + +class RectTransform(object): + def __init__(self, + anchor_min=(0.5, 0.5), + anchor_max=(0.5, 0.5), + pivot=(0.5, 0.5), + anchored_position=(0.0, 0.0), + size_delta=(100.0, 30.0)): + # 值域: anchor/pivot 为 0..1,anchored_position/size_delta 为像素 + self.anchor_min = tuple(anchor_min) + self.anchor_max = tuple(anchor_max) + self.pivot = tuple(pivot) + self.anchored_position = tuple(anchored_position) + self.size_delta = tuple(size_delta) + + def compute_pixel_rect(self, parent_rect: Tuple[float, float, float, float]): + """ + 将 RectTransform 转换为相对于父节点的像素矩形 (left, top, width, height) + + parent_rect: (parent_left, parent_top, parent_width, parent_height) + """ + p_left, p_top, p_w, p_h = parent_rect + + a_min_x = self.anchor_min[0] + a_min_y = self.anchor_min[1] + a_max_x = self.anchor_max[0] + a_max_y = self.anchor_max[1] + + # 计算锚框的像素坐标(left/top 和 right/bottom) + anchor_left = p_left + a_min_x * p_w + anchor_right = p_left + a_max_x * p_w + anchor_top = p_top + a_min_y * p_h + anchor_bottom = p_top + a_max_y * p_h + + # 宽高 + if abs(a_max_x - a_min_x) < 1e-6: + # 固定锚点 -> 使用 size_delta.x + width = float(self.size_delta[0]) + # anchored_position.x 表示相对于锚点框的位置(像素) + left = anchor_left + float(self.anchored_position[0]) - self.pivot[0] * width + else: + # 拉伸到锚框再加上 size_delta.x + width = (anchor_right - anchor_left) + float(self.size_delta[0]) + # anchored_position.x 表示锚框内的像素偏移 + left = anchor_left + float(self.anchored_position[0]) + + if abs(a_max_y - a_min_y) < 1e-6: + height = float(self.size_delta[1]) + # 注意:engine 中 top 增大表示向下,所以 anchored_position.y 保持像素系 + top = anchor_top + float(self.anchored_position[1]) - self.pivot[1] * height + else: + height = (anchor_bottom - anchor_top) + float(self.size_delta[1]) + top = anchor_top + float(self.anchored_position[1]) + + return (left, top, width, height) + + def apply_to_lui(self, lui_obj, parent_rect: Tuple[float, float, float, float]): + """计算并应用到给定的 LUIObject(设置 left/top/width/height)""" + left, top, width, height = self.compute_pixel_rect(parent_rect) + try: + lui_obj.left = left + lui_obj.top = top + lui_obj.width = width + lui_obj.height = height + except Exception: + # 忽略不能设置的属性,调用者应保证对象支持这些属性 + pass + + def to_dict(self): + return { + 'anchor_min': tuple(self.anchor_min), + 'anchor_max': tuple(self.anchor_max), + 'pivot': tuple(self.pivot), + 'anchored_position': tuple(self.anchored_position), + 'size_delta': tuple(self.size_delta), + } diff --git a/ui/Builtin/__init__.py b/ui/Builtin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/Skins/Default/GenerateAtlas.bat b/ui/Skins/Default/GenerateAtlas.bat new file mode 100644 index 00000000..3ae0bb00 --- /dev/null +++ b/ui/Skins/Default/GenerateAtlas.bat @@ -0,0 +1,5 @@ +@echo off + +cd res +ppython ../../../Misc/LUIAtlasGen.py +pause diff --git a/ui/Skins/Default/__init__.py b/ui/Skins/Default/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/Skins/Default/font/SourceSansPro-Black.ttf b/ui/Skins/Default/font/SourceSansPro-Black.ttf new file mode 100644 index 00000000..0243842f Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-Black.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-BlackIt.ttf b/ui/Skins/Default/font/SourceSansPro-BlackIt.ttf new file mode 100644 index 00000000..167045f9 Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-BlackIt.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-Bold.ttf b/ui/Skins/Default/font/SourceSansPro-Bold.ttf new file mode 100644 index 00000000..be46652b Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-Bold.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-BoldIt.ttf b/ui/Skins/Default/font/SourceSansPro-BoldIt.ttf new file mode 100644 index 00000000..fd5c4f39 Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-BoldIt.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-ExtraLight.ttf b/ui/Skins/Default/font/SourceSansPro-ExtraLight.ttf new file mode 100644 index 00000000..182165ca Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-ExtraLight.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-ExtraLightIt.ttf b/ui/Skins/Default/font/SourceSansPro-ExtraLightIt.ttf new file mode 100644 index 00000000..5846588e Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-ExtraLightIt.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-It.ttf b/ui/Skins/Default/font/SourceSansPro-It.ttf new file mode 100644 index 00000000..c689cd29 Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-It.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-Light.ttf b/ui/Skins/Default/font/SourceSansPro-Light.ttf new file mode 100644 index 00000000..dcff4e92 Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-Light.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-LightIt.ttf b/ui/Skins/Default/font/SourceSansPro-LightIt.ttf new file mode 100644 index 00000000..0c3451f8 Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-LightIt.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-Regular.ttf b/ui/Skins/Default/font/SourceSansPro-Regular.ttf new file mode 100644 index 00000000..a011dff9 Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-Regular.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-Semibold.ttf b/ui/Skins/Default/font/SourceSansPro-Semibold.ttf new file mode 100644 index 00000000..dbf946a5 Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-Semibold.ttf differ diff --git a/ui/Skins/Default/font/SourceSansPro-SemiboldIt.ttf b/ui/Skins/Default/font/SourceSansPro-SemiboldIt.ttf new file mode 100644 index 00000000..d1bc7f0a Binary files /dev/null and b/ui/Skins/Default/font/SourceSansPro-SemiboldIt.ttf differ diff --git a/ui/Skins/Default/res/ButtonDefault.png b/ui/Skins/Default/res/ButtonDefault.png new file mode 100644 index 00000000..b3e08cb6 Binary files /dev/null and b/ui/Skins/Default/res/ButtonDefault.png differ diff --git a/ui/Skins/Default/res/ButtonDefaultFocus.png b/ui/Skins/Default/res/ButtonDefaultFocus.png new file mode 100644 index 00000000..3dee2478 Binary files /dev/null and b/ui/Skins/Default/res/ButtonDefaultFocus.png differ diff --git a/ui/Skins/Default/res/ButtonDefaultFocus_Left.png b/ui/Skins/Default/res/ButtonDefaultFocus_Left.png new file mode 100644 index 00000000..8702ed6a Binary files /dev/null and b/ui/Skins/Default/res/ButtonDefaultFocus_Left.png differ diff --git a/ui/Skins/Default/res/ButtonDefaultFocus_Right.png b/ui/Skins/Default/res/ButtonDefaultFocus_Right.png new file mode 100644 index 00000000..c60bccce Binary files /dev/null and b/ui/Skins/Default/res/ButtonDefaultFocus_Right.png differ diff --git a/ui/Skins/Default/res/ButtonDefault_Left.png b/ui/Skins/Default/res/ButtonDefault_Left.png new file mode 100644 index 00000000..34155844 Binary files /dev/null and b/ui/Skins/Default/res/ButtonDefault_Left.png differ diff --git a/ui/Skins/Default/res/ButtonDefault_Right.png b/ui/Skins/Default/res/ButtonDefault_Right.png new file mode 100644 index 00000000..60a8e179 Binary files /dev/null and b/ui/Skins/Default/res/ButtonDefault_Right.png differ diff --git a/ui/Skins/Default/res/ButtonGreen.png b/ui/Skins/Default/res/ButtonGreen.png new file mode 100644 index 00000000..e2a42a4c Binary files /dev/null and b/ui/Skins/Default/res/ButtonGreen.png differ diff --git a/ui/Skins/Default/res/ButtonGreenFocus.png b/ui/Skins/Default/res/ButtonGreenFocus.png new file mode 100644 index 00000000..d340eb79 Binary files /dev/null and b/ui/Skins/Default/res/ButtonGreenFocus.png differ diff --git a/ui/Skins/Default/res/ButtonGreenFocus_Left.png b/ui/Skins/Default/res/ButtonGreenFocus_Left.png new file mode 100644 index 00000000..2b9e8d39 Binary files /dev/null and b/ui/Skins/Default/res/ButtonGreenFocus_Left.png differ diff --git a/ui/Skins/Default/res/ButtonGreenFocus_Right.png b/ui/Skins/Default/res/ButtonGreenFocus_Right.png new file mode 100644 index 00000000..5d5196eb Binary files /dev/null and b/ui/Skins/Default/res/ButtonGreenFocus_Right.png differ diff --git a/ui/Skins/Default/res/ButtonGreen_Left.png b/ui/Skins/Default/res/ButtonGreen_Left.png new file mode 100644 index 00000000..3291d7b6 Binary files /dev/null and b/ui/Skins/Default/res/ButtonGreen_Left.png differ diff --git a/ui/Skins/Default/res/ButtonGreen_Right.png b/ui/Skins/Default/res/ButtonGreen_Right.png new file mode 100644 index 00000000..46e16de6 Binary files /dev/null and b/ui/Skins/Default/res/ButtonGreen_Right.png differ diff --git a/ui/Skins/Default/res/Checkbox_Checked.png b/ui/Skins/Default/res/Checkbox_Checked.png new file mode 100644 index 00000000..d0822806 Binary files /dev/null and b/ui/Skins/Default/res/Checkbox_Checked.png differ diff --git a/ui/Skins/Default/res/Checkbox_CheckedHover.png b/ui/Skins/Default/res/Checkbox_CheckedHover.png new file mode 100644 index 00000000..fd9eadd0 Binary files /dev/null and b/ui/Skins/Default/res/Checkbox_CheckedHover.png differ diff --git a/ui/Skins/Default/res/Checkbox_Default.png b/ui/Skins/Default/res/Checkbox_Default.png new file mode 100644 index 00000000..f8ff0126 Binary files /dev/null and b/ui/Skins/Default/res/Checkbox_Default.png differ diff --git a/ui/Skins/Default/res/Checkbox_DefaultHover.png b/ui/Skins/Default/res/Checkbox_DefaultHover.png new file mode 100644 index 00000000..26194435 Binary files /dev/null and b/ui/Skins/Default/res/Checkbox_DefaultHover.png differ diff --git a/ui/Skins/Default/res/ColorpickerActiveColorOverlay.png b/ui/Skins/Default/res/ColorpickerActiveColorOverlay.png new file mode 100644 index 00000000..05274097 Binary files /dev/null and b/ui/Skins/Default/res/ColorpickerActiveColorOverlay.png differ diff --git a/ui/Skins/Default/res/ColorpickerFieldHandle.png b/ui/Skins/Default/res/ColorpickerFieldHandle.png new file mode 100644 index 00000000..f6109c05 Binary files /dev/null and b/ui/Skins/Default/res/ColorpickerFieldHandle.png differ diff --git a/ui/Skins/Default/res/ColorpickerFieldOverlay.png b/ui/Skins/Default/res/ColorpickerFieldOverlay.png new file mode 100644 index 00000000..89289a38 Binary files /dev/null and b/ui/Skins/Default/res/ColorpickerFieldOverlay.png differ diff --git a/ui/Skins/Default/res/ColorpickerHueHandle.png b/ui/Skins/Default/res/ColorpickerHueHandle.png new file mode 100644 index 00000000..287020c4 Binary files /dev/null and b/ui/Skins/Default/res/ColorpickerHueHandle.png differ diff --git a/ui/Skins/Default/res/ColorpickerHueSlider.png b/ui/Skins/Default/res/ColorpickerHueSlider.png new file mode 100644 index 00000000..bb73fead Binary files /dev/null and b/ui/Skins/Default/res/ColorpickerHueSlider.png differ diff --git a/ui/Skins/Default/res/ColorpickerPreviewBg.png b/ui/Skins/Default/res/ColorpickerPreviewBg.png new file mode 100644 index 00000000..7c5759af Binary files /dev/null and b/ui/Skins/Default/res/ColorpickerPreviewBg.png differ diff --git a/ui/Skins/Default/res/ColorpickerPreviewOverlay.png b/ui/Skins/Default/res/ColorpickerPreviewOverlay.png new file mode 100644 index 00000000..0d691ff2 Binary files /dev/null and b/ui/Skins/Default/res/ColorpickerPreviewOverlay.png differ diff --git a/ui/Skins/Default/res/Frame_BL.png b/ui/Skins/Default/res/Frame_BL.png new file mode 100644 index 00000000..31c67058 Binary files /dev/null and b/ui/Skins/Default/res/Frame_BL.png differ diff --git a/ui/Skins/Default/res/Frame_BR.png b/ui/Skins/Default/res/Frame_BR.png new file mode 100644 index 00000000..62b03819 Binary files /dev/null and b/ui/Skins/Default/res/Frame_BR.png differ diff --git a/ui/Skins/Default/res/Frame_Bottom.png b/ui/Skins/Default/res/Frame_Bottom.png new file mode 100644 index 00000000..00235803 Binary files /dev/null and b/ui/Skins/Default/res/Frame_Bottom.png differ diff --git a/ui/Skins/Default/res/Frame_Left.png b/ui/Skins/Default/res/Frame_Left.png new file mode 100644 index 00000000..1d781ff0 Binary files /dev/null and b/ui/Skins/Default/res/Frame_Left.png differ diff --git a/ui/Skins/Default/res/Frame_Mid.png b/ui/Skins/Default/res/Frame_Mid.png new file mode 100644 index 00000000..7a96c24a Binary files /dev/null and b/ui/Skins/Default/res/Frame_Mid.png differ diff --git a/ui/Skins/Default/res/Frame_Right.png b/ui/Skins/Default/res/Frame_Right.png new file mode 100644 index 00000000..fa9355e1 Binary files /dev/null and b/ui/Skins/Default/res/Frame_Right.png differ diff --git a/ui/Skins/Default/res/Frame_TL.png b/ui/Skins/Default/res/Frame_TL.png new file mode 100644 index 00000000..aa188525 Binary files /dev/null and b/ui/Skins/Default/res/Frame_TL.png differ diff --git a/ui/Skins/Default/res/Frame_TR.png b/ui/Skins/Default/res/Frame_TR.png new file mode 100644 index 00000000..08b46bbb Binary files /dev/null and b/ui/Skins/Default/res/Frame_TR.png differ diff --git a/ui/Skins/Default/res/Frame_Top.png b/ui/Skins/Default/res/Frame_Top.png new file mode 100644 index 00000000..cec2bf28 Binary files /dev/null and b/ui/Skins/Default/res/Frame_Top.png differ diff --git a/ui/Skins/Default/res/HorizontalListDivider.png b/ui/Skins/Default/res/HorizontalListDivider.png new file mode 100644 index 00000000..6e48500d Binary files /dev/null and b/ui/Skins/Default/res/HorizontalListDivider.png differ diff --git a/ui/Skins/Default/res/InputField.png b/ui/Skins/Default/res/InputField.png new file mode 100644 index 00000000..db58301e Binary files /dev/null and b/ui/Skins/Default/res/InputField.png differ diff --git a/ui/Skins/Default/res/InputField_Left.png b/ui/Skins/Default/res/InputField_Left.png new file mode 100644 index 00000000..8ecd16ab Binary files /dev/null and b/ui/Skins/Default/res/InputField_Left.png differ diff --git a/ui/Skins/Default/res/InputField_Right.png b/ui/Skins/Default/res/InputField_Right.png new file mode 100644 index 00000000..e9a1b430 Binary files /dev/null and b/ui/Skins/Default/res/InputField_Right.png differ diff --git a/ui/Skins/Default/res/Keymarker.png b/ui/Skins/Default/res/Keymarker.png new file mode 100644 index 00000000..e5723fca Binary files /dev/null and b/ui/Skins/Default/res/Keymarker.png differ diff --git a/ui/Skins/Default/res/Keymarker_Left.png b/ui/Skins/Default/res/Keymarker_Left.png new file mode 100644 index 00000000..0fd4ce2f Binary files /dev/null and b/ui/Skins/Default/res/Keymarker_Left.png differ diff --git a/ui/Skins/Default/res/Keymarker_Right.png b/ui/Skins/Default/res/Keymarker_Right.png new file mode 100644 index 00000000..f6c3ca93 Binary files /dev/null and b/ui/Skins/Default/res/Keymarker_Right.png differ diff --git a/ui/Skins/Default/res/ListDivider.png b/ui/Skins/Default/res/ListDivider.png new file mode 100644 index 00000000..6e48500d Binary files /dev/null and b/ui/Skins/Default/res/ListDivider.png differ diff --git a/ui/Skins/Default/res/Popup_BL.png b/ui/Skins/Default/res/Popup_BL.png new file mode 100644 index 00000000..b268f115 Binary files /dev/null and b/ui/Skins/Default/res/Popup_BL.png differ diff --git a/ui/Skins/Default/res/Popup_BR.png b/ui/Skins/Default/res/Popup_BR.png new file mode 100644 index 00000000..648c791d Binary files /dev/null and b/ui/Skins/Default/res/Popup_BR.png differ diff --git a/ui/Skins/Default/res/Popup_Bottom.png b/ui/Skins/Default/res/Popup_Bottom.png new file mode 100644 index 00000000..690ff311 Binary files /dev/null and b/ui/Skins/Default/res/Popup_Bottom.png differ diff --git a/ui/Skins/Default/res/Popup_Left.png b/ui/Skins/Default/res/Popup_Left.png new file mode 100644 index 00000000..d87d6988 Binary files /dev/null and b/ui/Skins/Default/res/Popup_Left.png differ diff --git a/ui/Skins/Default/res/Popup_Mid.png b/ui/Skins/Default/res/Popup_Mid.png new file mode 100644 index 00000000..cb210d7d Binary files /dev/null and b/ui/Skins/Default/res/Popup_Mid.png differ diff --git a/ui/Skins/Default/res/Popup_Right.png b/ui/Skins/Default/res/Popup_Right.png new file mode 100644 index 00000000..595d8620 Binary files /dev/null and b/ui/Skins/Default/res/Popup_Right.png differ diff --git a/ui/Skins/Default/res/Popup_TL.png b/ui/Skins/Default/res/Popup_TL.png new file mode 100644 index 00000000..38155924 Binary files /dev/null and b/ui/Skins/Default/res/Popup_TL.png differ diff --git a/ui/Skins/Default/res/Popup_TR.png b/ui/Skins/Default/res/Popup_TR.png new file mode 100644 index 00000000..10ba6b7f Binary files /dev/null and b/ui/Skins/Default/res/Popup_TR.png differ diff --git a/ui/Skins/Default/res/Popup_Top.png b/ui/Skins/Default/res/Popup_Top.png new file mode 100644 index 00000000..15ebd9d5 Binary files /dev/null and b/ui/Skins/Default/res/Popup_Top.png differ diff --git a/ui/Skins/Default/res/ProgressbarBg.png b/ui/Skins/Default/res/ProgressbarBg.png new file mode 100644 index 00000000..324a30bb Binary files /dev/null and b/ui/Skins/Default/res/ProgressbarBg.png differ diff --git a/ui/Skins/Default/res/ProgressbarBg_Left.png b/ui/Skins/Default/res/ProgressbarBg_Left.png new file mode 100644 index 00000000..35ea1af3 Binary files /dev/null and b/ui/Skins/Default/res/ProgressbarBg_Left.png differ diff --git a/ui/Skins/Default/res/ProgressbarBg_Right.png b/ui/Skins/Default/res/ProgressbarBg_Right.png new file mode 100644 index 00000000..4a250d5f Binary files /dev/null and b/ui/Skins/Default/res/ProgressbarBg_Right.png differ diff --git a/ui/Skins/Default/res/ProgressbarFg.png b/ui/Skins/Default/res/ProgressbarFg.png new file mode 100644 index 00000000..d0e93499 Binary files /dev/null and b/ui/Skins/Default/res/ProgressbarFg.png differ diff --git a/ui/Skins/Default/res/ProgressbarFg_Finish.png b/ui/Skins/Default/res/ProgressbarFg_Finish.png new file mode 100644 index 00000000..e7c5e433 Binary files /dev/null and b/ui/Skins/Default/res/ProgressbarFg_Finish.png differ diff --git a/ui/Skins/Default/res/ProgressbarFg_Left.png b/ui/Skins/Default/res/ProgressbarFg_Left.png new file mode 100644 index 00000000..924f3f3a Binary files /dev/null and b/ui/Skins/Default/res/ProgressbarFg_Left.png differ diff --git a/ui/Skins/Default/res/ProgressbarFg_Right.png b/ui/Skins/Default/res/ProgressbarFg_Right.png new file mode 100644 index 00000000..355a7ef1 Binary files /dev/null and b/ui/Skins/Default/res/ProgressbarFg_Right.png differ diff --git a/ui/Skins/Default/res/Radiobox_Active.png b/ui/Skins/Default/res/Radiobox_Active.png new file mode 100644 index 00000000..08596180 Binary files /dev/null and b/ui/Skins/Default/res/Radiobox_Active.png differ diff --git a/ui/Skins/Default/res/Radiobox_ActiveHover.png b/ui/Skins/Default/res/Radiobox_ActiveHover.png new file mode 100644 index 00000000..fd0f0a1e Binary files /dev/null and b/ui/Skins/Default/res/Radiobox_ActiveHover.png differ diff --git a/ui/Skins/Default/res/Radiobox_Default.png b/ui/Skins/Default/res/Radiobox_Default.png new file mode 100644 index 00000000..25a0a2be Binary files /dev/null and b/ui/Skins/Default/res/Radiobox_Default.png differ diff --git a/ui/Skins/Default/res/Radiobox_DefaultHover.png b/ui/Skins/Default/res/Radiobox_DefaultHover.png new file mode 100644 index 00000000..1fef48b3 Binary files /dev/null and b/ui/Skins/Default/res/Radiobox_DefaultHover.png differ diff --git a/ui/Skins/Default/res/ScrollShadowBottom.png b/ui/Skins/Default/res/ScrollShadowBottom.png new file mode 100644 index 00000000..ab3e37b1 Binary files /dev/null and b/ui/Skins/Default/res/ScrollShadowBottom.png differ diff --git a/ui/Skins/Default/res/ScrollShadowBottom_Left.png b/ui/Skins/Default/res/ScrollShadowBottom_Left.png new file mode 100644 index 00000000..929a34af Binary files /dev/null and b/ui/Skins/Default/res/ScrollShadowBottom_Left.png differ diff --git a/ui/Skins/Default/res/ScrollShadowBottom_Right.png b/ui/Skins/Default/res/ScrollShadowBottom_Right.png new file mode 100644 index 00000000..7363bf62 Binary files /dev/null and b/ui/Skins/Default/res/ScrollShadowBottom_Right.png differ diff --git a/ui/Skins/Default/res/ScrollShadowTop.png b/ui/Skins/Default/res/ScrollShadowTop.png new file mode 100644 index 00000000..55f42507 Binary files /dev/null and b/ui/Skins/Default/res/ScrollShadowTop.png differ diff --git a/ui/Skins/Default/res/ScrollShadowTop_Left.png b/ui/Skins/Default/res/ScrollShadowTop_Left.png new file mode 100644 index 00000000..e4d93dbd Binary files /dev/null and b/ui/Skins/Default/res/ScrollShadowTop_Left.png differ diff --git a/ui/Skins/Default/res/ScrollShadowTop_Right.png b/ui/Skins/Default/res/ScrollShadowTop_Right.png new file mode 100644 index 00000000..bb52bcdb Binary files /dev/null and b/ui/Skins/Default/res/ScrollShadowTop_Right.png differ diff --git a/ui/Skins/Default/res/ScrollbarHandle.png b/ui/Skins/Default/res/ScrollbarHandle.png new file mode 100644 index 00000000..64559565 Binary files /dev/null and b/ui/Skins/Default/res/ScrollbarHandle.png differ diff --git a/ui/Skins/Default/res/ScrollbarHandle_Bottom.png b/ui/Skins/Default/res/ScrollbarHandle_Bottom.png new file mode 100644 index 00000000..f475f5d6 Binary files /dev/null and b/ui/Skins/Default/res/ScrollbarHandle_Bottom.png differ diff --git a/ui/Skins/Default/res/ScrollbarHandle_Top.png b/ui/Skins/Default/res/ScrollbarHandle_Top.png new file mode 100644 index 00000000..62faa394 Binary files /dev/null and b/ui/Skins/Default/res/ScrollbarHandle_Top.png differ diff --git a/ui/Skins/Default/res/Selectbox.png b/ui/Skins/Default/res/Selectbox.png new file mode 100644 index 00000000..e3ae52e4 Binary files /dev/null and b/ui/Skins/Default/res/Selectbox.png differ diff --git a/ui/Skins/Default/res/SelectboxActive.png b/ui/Skins/Default/res/SelectboxActive.png new file mode 100644 index 00000000..c1fe8b66 Binary files /dev/null and b/ui/Skins/Default/res/SelectboxActive.png differ diff --git a/ui/Skins/Default/res/SelectboxActive_Left.png b/ui/Skins/Default/res/SelectboxActive_Left.png new file mode 100644 index 00000000..511c2d46 Binary files /dev/null and b/ui/Skins/Default/res/SelectboxActive_Left.png differ diff --git a/ui/Skins/Default/res/SelectboxOpen_Right.png b/ui/Skins/Default/res/SelectboxOpen_Right.png new file mode 100644 index 00000000..020a3601 Binary files /dev/null and b/ui/Skins/Default/res/SelectboxOpen_Right.png differ diff --git a/ui/Skins/Default/res/Selectbox_Left.png b/ui/Skins/Default/res/Selectbox_Left.png new file mode 100644 index 00000000..1bf301f3 Binary files /dev/null and b/ui/Skins/Default/res/Selectbox_Left.png differ diff --git a/ui/Skins/Default/res/Selectbox_Right.png b/ui/Skins/Default/res/Selectbox_Right.png new file mode 100644 index 00000000..05e88d9c Binary files /dev/null and b/ui/Skins/Default/res/Selectbox_Right.png differ diff --git a/ui/Skins/Default/res/SelectdropDivider.png b/ui/Skins/Default/res/SelectdropDivider.png new file mode 100644 index 00000000..c83b68e6 Binary files /dev/null and b/ui/Skins/Default/res/SelectdropDivider.png differ diff --git a/ui/Skins/Default/res/Selectdrop_BL.png b/ui/Skins/Default/res/Selectdrop_BL.png new file mode 100644 index 00000000..0cd0b316 Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_BL.png differ diff --git a/ui/Skins/Default/res/Selectdrop_BR.png b/ui/Skins/Default/res/Selectdrop_BR.png new file mode 100644 index 00000000..9a39f01d Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_BR.png differ diff --git a/ui/Skins/Default/res/Selectdrop_Bottom.png b/ui/Skins/Default/res/Selectdrop_Bottom.png new file mode 100644 index 00000000..1cff174e Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_Bottom.png differ diff --git a/ui/Skins/Default/res/Selectdrop_Left.png b/ui/Skins/Default/res/Selectdrop_Left.png new file mode 100644 index 00000000..d223578f Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_Left.png differ diff --git a/ui/Skins/Default/res/Selectdrop_Mid.png b/ui/Skins/Default/res/Selectdrop_Mid.png new file mode 100644 index 00000000..71ff34ce Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_Mid.png differ diff --git a/ui/Skins/Default/res/Selectdrop_Right.png b/ui/Skins/Default/res/Selectdrop_Right.png new file mode 100644 index 00000000..5958be42 Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_Right.png differ diff --git a/ui/Skins/Default/res/Selectdrop_TL.png b/ui/Skins/Default/res/Selectdrop_TL.png new file mode 100644 index 00000000..52f12048 Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_TL.png differ diff --git a/ui/Skins/Default/res/Selectdrop_TR.png b/ui/Skins/Default/res/Selectdrop_TR.png new file mode 100644 index 00000000..e091f82b Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_TR.png differ diff --git a/ui/Skins/Default/res/Selectdrop_Top.png b/ui/Skins/Default/res/Selectdrop_Top.png new file mode 100644 index 00000000..d97b68aa Binary files /dev/null and b/ui/Skins/Default/res/Selectdrop_Top.png differ diff --git a/ui/Skins/Default/res/SliderBg.png b/ui/Skins/Default/res/SliderBg.png new file mode 100644 index 00000000..520e6278 Binary files /dev/null and b/ui/Skins/Default/res/SliderBg.png differ diff --git a/ui/Skins/Default/res/SliderBgFill.png b/ui/Skins/Default/res/SliderBgFill.png new file mode 100644 index 00000000..8a941040 Binary files /dev/null and b/ui/Skins/Default/res/SliderBgFill.png differ diff --git a/ui/Skins/Default/res/SliderBgFill_Left.png b/ui/Skins/Default/res/SliderBgFill_Left.png new file mode 100644 index 00000000..eef58909 Binary files /dev/null and b/ui/Skins/Default/res/SliderBgFill_Left.png differ diff --git a/ui/Skins/Default/res/SliderBg_Left.png b/ui/Skins/Default/res/SliderBg_Left.png new file mode 100644 index 00000000..d53ab941 Binary files /dev/null and b/ui/Skins/Default/res/SliderBg_Left.png differ diff --git a/ui/Skins/Default/res/SliderBg_Right.png b/ui/Skins/Default/res/SliderBg_Right.png new file mode 100644 index 00000000..cecf516d Binary files /dev/null and b/ui/Skins/Default/res/SliderBg_Right.png differ diff --git a/ui/Skins/Default/res/SliderKnob.png b/ui/Skins/Default/res/SliderKnob.png new file mode 100644 index 00000000..f7ba432f Binary files /dev/null and b/ui/Skins/Default/res/SliderKnob.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_BL.png b/ui/Skins/Default/res/SunkenFrame_BL.png new file mode 100644 index 00000000..2557ef97 Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_BL.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_BR.png b/ui/Skins/Default/res/SunkenFrame_BR.png new file mode 100644 index 00000000..58f43acb Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_BR.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_Bottom.png b/ui/Skins/Default/res/SunkenFrame_Bottom.png new file mode 100644 index 00000000..c5e6b43b Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_Bottom.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_Left.png b/ui/Skins/Default/res/SunkenFrame_Left.png new file mode 100644 index 00000000..e5ded006 Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_Left.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_Mid.png b/ui/Skins/Default/res/SunkenFrame_Mid.png new file mode 100644 index 00000000..53b37c07 Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_Mid.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_Right.png b/ui/Skins/Default/res/SunkenFrame_Right.png new file mode 100644 index 00000000..e11ea865 Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_Right.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_TL.png b/ui/Skins/Default/res/SunkenFrame_TL.png new file mode 100644 index 00000000..3d9c89f2 Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_TL.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_TR.png b/ui/Skins/Default/res/SunkenFrame_TR.png new file mode 100644 index 00000000..d65b5093 Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_TR.png differ diff --git a/ui/Skins/Default/res/SunkenFrame_Top.png b/ui/Skins/Default/res/SunkenFrame_Top.png new file mode 100644 index 00000000..aa396ebf Binary files /dev/null and b/ui/Skins/Default/res/SunkenFrame_Top.png differ diff --git a/ui/Skins/Default/res/atlas.png b/ui/Skins/Default/res/atlas.png new file mode 100644 index 00000000..df6ecf33 Binary files /dev/null and b/ui/Skins/Default/res/atlas.png differ diff --git a/ui/Skins/Default/res/atlas.txt b/ui/Skins/Default/res/atlas.txt new file mode 100644 index 00000000..8ecaa545 --- /dev/null +++ b/ui/Skins/Default/res/atlas.txt @@ -0,0 +1,101 @@ +ColorpickerFieldOverlay 0 0 132 132 +ColorpickerHueSlider 132 0 24 132 +ColorpickerActiveColorOverlay 156 0 44 44 +Frame_BL 200 0 36 36 +Frame_BR 200 36 36 36 +Frame_TL 156 44 36 36 +Frame_TR 192 72 36 36 +Popup_BL 156 80 36 36 +Popup_BR 192 108 36 36 +Popup_TL 156 116 36 36 +Popup_TR 0 132 36 36 +Frame_Bottom 36 132 31 36 +Frame_Left 67 132 36 31 +Frame_Right 103 132 36 31 +Frame_Top 192 144 31 36 +Popup_Bottom 223 144 31 36 +Popup_Left 139 152 36 31 +Popup_Right 67 163 36 31 +Popup_Top 103 163 31 36 +ColorpickerHueHandle 0 168 50 22 +SelectboxOpen_Right 175 180 32 32 +Selectbox_Right 207 180 32 32 +ColorpickerPreviewBg 134 183 31 31 +ColorpickerPreviewOverlay 228 72 27 27 +ScrollShadowBottom_Left 236 0 18 18 +ScrollShadowBottom_Right 236 18 18 18 +ScrollShadowTop_Left 236 36 18 18 +ScrollShadowTop_Right 236 54 18 18 +SliderKnob 228 99 18 18 +Checkbox_Checked 228 117 17 17 +Checkbox_CheckedHover 139 132 17 17 +Checkbox_Default 175 152 17 17 +Checkbox_DefaultHover 50 168 17 17 +Radiobox_Active 239 180 17 17 +Radiobox_Default 50 185 17 17 +ColorpickerFieldHandle 0 190 16 17 +Radiobox_ActiveHover 16 190 16 17 +Radiobox_DefaultHover 32 190 16 17 +ProgressbarFg_Finish 67 194 13 20 +ProgressbarFg_Right 245 117 11 20 +Keymarker_Left 192 44 6 27 +Keymarker_Right 165 183 6 27 +ButtonDefaultFocus_Left 80 194 5 32 +ButtonDefaultFocus_Right 85 194 5 32 +ButtonDefault_Left 90 194 5 32 +ButtonDefault_Right 95 194 5 32 +ButtonGreenFocus_Left 239 197 5 32 +ButtonGreenFocus_Right 244 197 5 32 +ButtonGreen_Left 171 183 4 32 +ButtonGreen_Right 249 197 4 32 +SelectboxActive_Left 100 199 4 32 +Selectbox_Left 104 199 4 32 +SunkenFrame_Left 108 199 5 24 +SunkenFrame_Right 113 199 5 24 +SunkenFrame_Bottom 228 137 23 5 +SunkenFrame_Top 0 207 23 5 +InputField_Left 253 197 3 29 +InputField_Right 118 199 3 29 +Selectdrop_BL 246 99 9 9 +Selectdrop_BR 246 108 9 9 +ScrollbarHandle_Top 175 169 11 6 +ProgressbarFg_Left 134 163 3 20 +ProgressbarBg_Left 121 199 3 19 +ProgressbarBg_Right 124 199 3 19 +ScrollbarHandle_Bottom 175 175 11 5 +ButtonDefault 254 0 1 32 +ButtonDefaultFocus 255 0 1 32 +ButtonGreen 254 32 1 32 +ButtonGreenFocus 255 32 1 32 +Selectbox 255 64 1 32 +SelectboxActive 254 137 1 32 +SliderBgFill_Left 186 169 4 8 +Frame_Mid 255 137 1 31 +Popup_Mid 48 190 1 31 +Selectdrop_TL 127 199 6 5 +Selectdrop_TR 49 202 6 5 +InputField 133 199 1 29 +Keymarker 198 44 1 27 +SunkenFrame_BL 55 202 5 5 +SunkenFrame_BR 60 202 5 5 +SunkenFrame_TL 127 204 5 5 +SunkenFrame_TR 23 207 5 5 +SunkenFrame_Mid 199 44 1 24 +SliderBg_Left 251 137 3 7 +SliderBg_Right 28 207 3 7 +ProgressbarFg 255 96 1 20 +ProgressbarBg 137 163 1 19 +ScrollShadowBottom 138 163 1 18 +ScrollShadowTop 65 202 1 18 +ScrollbarHandle 228 134 11 1 +SliderBgFill 254 64 1 8 +SliderBg 255 168 1 7 +Selectdrop_Bottom 190 169 1 6 +Selectdrop_Left 192 71 6 1 +Selectdrop_Right 239 134 6 1 +Selectdrop_Top 191 169 1 5 +SelectdropDivider 199 68 1 2 +blank 199 70 1 1 +HorizontalListDivider 198 71 1 1 +ListDivider 199 71 1 1 +Selectdrop_Mid 255 116 1 1 diff --git a/ui/Skins/Default/res/blank.png b/ui/Skins/Default/res/blank.png new file mode 100644 index 00000000..fc4efe0f Binary files /dev/null and b/ui/Skins/Default/res/blank.png differ diff --git a/ui/Skins/Metro/GenerateAtlas.bat b/ui/Skins/Metro/GenerateAtlas.bat new file mode 100644 index 00000000..3ae0bb00 --- /dev/null +++ b/ui/Skins/Metro/GenerateAtlas.bat @@ -0,0 +1,5 @@ +@echo off + +cd res +ppython ../../../Misc/LUIAtlasGen.py +pause diff --git a/ui/Skins/Metro/LUIMetroSkin.py b/ui/Skins/Metro/LUIMetroSkin.py new file mode 100644 index 00000000..5c454ee2 --- /dev/null +++ b/ui/Skins/Metro/LUIMetroSkin.py @@ -0,0 +1,35 @@ + +from panda3d.lui import LUIFontPool, LUIAtlasPool +from panda3d.core import Filename +import os +from os.path import join + +from LUISkin import LUISkin +from LUILabel import LUILabel + +class LUIMetroSkin(LUISkin): + + """ Simple Metro / Flat UI skin """ + + skin_location = os.path.dirname(os.path.abspath(__file__)) + + def load(self): + LUIFontPool.get_global_ptr().register_font( + "default", loader.loadFont(self.get_resource("font/Roboto-Medium.ttf"))) + + label_font = loader.loadFont(self.get_resource("font/Roboto-Medium.ttf")) + label_font.set_pixels_per_unit(32) + LUIFontPool.get_global_ptr().register_font("label", label_font) + + headerFont = loader.loadFont(self.get_resource("font/Roboto-Light.ttf")) + headerFont.set_pixels_per_unit(80) + + LUIFontPool.get_global_ptr().register_font("header", headerFont) + + LUIAtlasPool.get_global_ptr().load_atlas("skin", + self.get_resource("res/atlas.txt"), + self.get_resource("res/atlas.png")) + + # Label color + # LUILabel.DEFAULT_COLOR = (0.0, 0.0, 0.0, 0.6) + # LUILabel.DEFAULT_USE_SHADOW = False diff --git a/ui/Skins/Metro/__init__.py b/ui/Skins/Metro/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/Skins/Metro/border.png b/ui/Skins/Metro/border.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/border.png differ diff --git a/ui/Skins/Metro/copy_frames.py b/ui/Skins/Metro/copy_frames.py new file mode 100644 index 00000000..eb2d29f7 --- /dev/null +++ b/ui/Skins/Metro/copy_frames.py @@ -0,0 +1,41 @@ + +import os +from shutil import copyfile as copy +os.chdir("res/") + +for f in os.listdir("."): + if f.startswith("Popup_") or f.startswith("Frame_") or f.startswith("SunkenFrame_"): + copy("../flat.png" if "_Mid" in f else "../border.png", f) + + +copy("ButtonDefault.png", "ButtonDefaultFocus.png") +copy("ButtonDefault.png", "ButtonDefaultFocus_Left.png") +copy("ButtonDefault.png", "ButtonDefaultFocus_Right.png") +copy("ButtonDefault.png", "ButtonDefault_Left.png") +copy("ButtonDefault.png", "ButtonDefault_Right.png") + +copy("ButtonGreen.png", "ButtonGreenFocus.png") +copy("ButtonGreen.png", "ButtonGreenFocus_Left.png") +copy("ButtonGreen.png", "ButtonGreenFocus_Right.png") +copy("ButtonGreen.png", "ButtonGreen_Left.png") +copy("ButtonGreen.png", "ButtonGreen_Right.png") + +copy("Selectbox.png", "Selectbox_Left.png") +copy("Selectbox.png", "SelectboxActive.png") +copy("Selectbox.png", "SelectboxActive_Left.png") + +copy("ProgressbarFg.png", "ProgressbarFg_Right.png") +copy("ProgressbarFg.png", "ProgressbarFg_Left.png") +copy("ProgressbarFg.png", "ProgressbarFg_Finish.png") +copy("ProgressbarBg.png", "ProgressbarBg_Right.png") +copy("ProgressbarBg.png", "ProgressbarBg_Left.png") + +copy("SliderBgFill.png", "SliderBgFill_Left.png") +copy("SliderBg.png", "SliderBg_Left.png") +copy("SliderBg.png", "SliderBg_Right.png") + +copy("InputField.png", "InputField_Left.png") +copy("InputField.png", "InputField_Right.png") + +for align in "TR TL BR BL Top Right Bottom Left".split(): + copy("Selectdrop_Mid.png", "Selectdrop_" + align + ".png") diff --git a/ui/Skins/Metro/flat.png b/ui/Skins/Metro/flat.png new file mode 100644 index 00000000..77a13c5b Binary files /dev/null and b/ui/Skins/Metro/flat.png differ diff --git a/ui/Skins/Metro/font/Roboto-Bold.ttf b/ui/Skins/Metro/font/Roboto-Bold.ttf new file mode 100644 index 00000000..aaf374d2 Binary files /dev/null and b/ui/Skins/Metro/font/Roboto-Bold.ttf differ diff --git a/ui/Skins/Metro/font/Roboto-LICENSE.txt b/ui/Skins/Metro/font/Roboto-LICENSE.txt new file mode 100644 index 00000000..817dcace --- /dev/null +++ b/ui/Skins/Metro/font/Roboto-LICENSE.txt @@ -0,0 +1,178 @@ +Font data copyright Google 2012 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + “License” shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + “Licensor” shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + “Legal Entity” shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + “control” means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + “You” (or “Your”) shall mean an individual or Legal Entity + exercising permissions granted by this License. + + “Source” form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + “Object” form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + “Work” shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + “Derivative Works” shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + “Contribution” shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, “submitted” + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as “Not a Contribution.” + + “Contributor” shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a “NOTICE” text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an “AS IS” BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/ui/Skins/Metro/font/Roboto-Light.ttf b/ui/Skins/Metro/font/Roboto-Light.ttf new file mode 100644 index 00000000..664e1b2f Binary files /dev/null and b/ui/Skins/Metro/font/Roboto-Light.ttf differ diff --git a/ui/Skins/Metro/font/Roboto-Medium.ttf b/ui/Skins/Metro/font/Roboto-Medium.ttf new file mode 100644 index 00000000..87983419 Binary files /dev/null and b/ui/Skins/Metro/font/Roboto-Medium.ttf differ diff --git a/ui/Skins/Metro/font/Roboto-Thin.ttf b/ui/Skins/Metro/font/Roboto-Thin.ttf new file mode 100644 index 00000000..d262d144 Binary files /dev/null and b/ui/Skins/Metro/font/Roboto-Thin.ttf differ diff --git a/ui/Skins/Metro/res/ButtonDefault.png b/ui/Skins/Metro/res/ButtonDefault.png new file mode 100644 index 00000000..8baf82e9 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonDefault.png differ diff --git a/ui/Skins/Metro/res/ButtonDefaultFocus.png b/ui/Skins/Metro/res/ButtonDefaultFocus.png new file mode 100644 index 00000000..8baf82e9 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonDefaultFocus.png differ diff --git a/ui/Skins/Metro/res/ButtonDefaultFocus_Left.png b/ui/Skins/Metro/res/ButtonDefaultFocus_Left.png new file mode 100644 index 00000000..8baf82e9 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonDefaultFocus_Left.png differ diff --git a/ui/Skins/Metro/res/ButtonDefaultFocus_Right.png b/ui/Skins/Metro/res/ButtonDefaultFocus_Right.png new file mode 100644 index 00000000..8baf82e9 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonDefaultFocus_Right.png differ diff --git a/ui/Skins/Metro/res/ButtonDefault_Left.png b/ui/Skins/Metro/res/ButtonDefault_Left.png new file mode 100644 index 00000000..8baf82e9 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonDefault_Left.png differ diff --git a/ui/Skins/Metro/res/ButtonDefault_Right.png b/ui/Skins/Metro/res/ButtonDefault_Right.png new file mode 100644 index 00000000..8baf82e9 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonDefault_Right.png differ diff --git a/ui/Skins/Metro/res/ButtonGreen.png b/ui/Skins/Metro/res/ButtonGreen.png new file mode 100644 index 00000000..e4d1fbf5 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonGreen.png differ diff --git a/ui/Skins/Metro/res/ButtonGreenFocus.png b/ui/Skins/Metro/res/ButtonGreenFocus.png new file mode 100644 index 00000000..e4d1fbf5 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonGreenFocus.png differ diff --git a/ui/Skins/Metro/res/ButtonGreenFocus_Left.png b/ui/Skins/Metro/res/ButtonGreenFocus_Left.png new file mode 100644 index 00000000..e4d1fbf5 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonGreenFocus_Left.png differ diff --git a/ui/Skins/Metro/res/ButtonGreenFocus_Right.png b/ui/Skins/Metro/res/ButtonGreenFocus_Right.png new file mode 100644 index 00000000..e4d1fbf5 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonGreenFocus_Right.png differ diff --git a/ui/Skins/Metro/res/ButtonGreen_Left.png b/ui/Skins/Metro/res/ButtonGreen_Left.png new file mode 100644 index 00000000..e4d1fbf5 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonGreen_Left.png differ diff --git a/ui/Skins/Metro/res/ButtonGreen_Right.png b/ui/Skins/Metro/res/ButtonGreen_Right.png new file mode 100644 index 00000000..e4d1fbf5 Binary files /dev/null and b/ui/Skins/Metro/res/ButtonGreen_Right.png differ diff --git a/ui/Skins/Metro/res/Checkbox_Checked.png b/ui/Skins/Metro/res/Checkbox_Checked.png new file mode 100644 index 00000000..cec474b2 Binary files /dev/null and b/ui/Skins/Metro/res/Checkbox_Checked.png differ diff --git a/ui/Skins/Metro/res/Checkbox_CheckedHover.png b/ui/Skins/Metro/res/Checkbox_CheckedHover.png new file mode 100644 index 00000000..cec474b2 Binary files /dev/null and b/ui/Skins/Metro/res/Checkbox_CheckedHover.png differ diff --git a/ui/Skins/Metro/res/Checkbox_Default.png b/ui/Skins/Metro/res/Checkbox_Default.png new file mode 100644 index 00000000..46ab47da Binary files /dev/null and b/ui/Skins/Metro/res/Checkbox_Default.png differ diff --git a/ui/Skins/Metro/res/Checkbox_DefaultHover.png b/ui/Skins/Metro/res/Checkbox_DefaultHover.png new file mode 100644 index 00000000..46ab47da Binary files /dev/null and b/ui/Skins/Metro/res/Checkbox_DefaultHover.png differ diff --git a/ui/Skins/Metro/res/ColorpickerActiveColorOverlay.png b/ui/Skins/Metro/res/ColorpickerActiveColorOverlay.png new file mode 100644 index 00000000..05274097 Binary files /dev/null and b/ui/Skins/Metro/res/ColorpickerActiveColorOverlay.png differ diff --git a/ui/Skins/Metro/res/ColorpickerFieldHandle.png b/ui/Skins/Metro/res/ColorpickerFieldHandle.png new file mode 100644 index 00000000..f6109c05 Binary files /dev/null and b/ui/Skins/Metro/res/ColorpickerFieldHandle.png differ diff --git a/ui/Skins/Metro/res/ColorpickerFieldOverlay.png b/ui/Skins/Metro/res/ColorpickerFieldOverlay.png new file mode 100644 index 00000000..89289a38 Binary files /dev/null and b/ui/Skins/Metro/res/ColorpickerFieldOverlay.png differ diff --git a/ui/Skins/Metro/res/ColorpickerHueHandle.png b/ui/Skins/Metro/res/ColorpickerHueHandle.png new file mode 100644 index 00000000..287020c4 Binary files /dev/null and b/ui/Skins/Metro/res/ColorpickerHueHandle.png differ diff --git a/ui/Skins/Metro/res/ColorpickerHueSlider.png b/ui/Skins/Metro/res/ColorpickerHueSlider.png new file mode 100644 index 00000000..bb73fead Binary files /dev/null and b/ui/Skins/Metro/res/ColorpickerHueSlider.png differ diff --git a/ui/Skins/Metro/res/ColorpickerPreviewBg.png b/ui/Skins/Metro/res/ColorpickerPreviewBg.png new file mode 100644 index 00000000..7c5759af Binary files /dev/null and b/ui/Skins/Metro/res/ColorpickerPreviewBg.png differ diff --git a/ui/Skins/Metro/res/ColorpickerPreviewOverlay.png b/ui/Skins/Metro/res/ColorpickerPreviewOverlay.png new file mode 100644 index 00000000..0d691ff2 Binary files /dev/null and b/ui/Skins/Metro/res/ColorpickerPreviewOverlay.png differ diff --git a/ui/Skins/Metro/res/Draft3.psd b/ui/Skins/Metro/res/Draft3.psd new file mode 100644 index 00000000..97d94ada Binary files /dev/null and b/ui/Skins/Metro/res/Draft3.psd differ diff --git a/ui/Skins/Metro/res/Frame_BL.png b/ui/Skins/Metro/res/Frame_BL.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Frame_BL.png differ diff --git a/ui/Skins/Metro/res/Frame_BR.png b/ui/Skins/Metro/res/Frame_BR.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Frame_BR.png differ diff --git a/ui/Skins/Metro/res/Frame_Bottom.png b/ui/Skins/Metro/res/Frame_Bottom.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Frame_Bottom.png differ diff --git a/ui/Skins/Metro/res/Frame_Left.png b/ui/Skins/Metro/res/Frame_Left.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Frame_Left.png differ diff --git a/ui/Skins/Metro/res/Frame_Mid.png b/ui/Skins/Metro/res/Frame_Mid.png new file mode 100644 index 00000000..77a13c5b Binary files /dev/null and b/ui/Skins/Metro/res/Frame_Mid.png differ diff --git a/ui/Skins/Metro/res/Frame_Right.png b/ui/Skins/Metro/res/Frame_Right.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Frame_Right.png differ diff --git a/ui/Skins/Metro/res/Frame_TL.png b/ui/Skins/Metro/res/Frame_TL.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Frame_TL.png differ diff --git a/ui/Skins/Metro/res/Frame_TR.png b/ui/Skins/Metro/res/Frame_TR.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Frame_TR.png differ diff --git a/ui/Skins/Metro/res/Frame_Top.png b/ui/Skins/Metro/res/Frame_Top.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Frame_Top.png differ diff --git a/ui/Skins/Metro/res/HorizontalListDivider.png b/ui/Skins/Metro/res/HorizontalListDivider.png new file mode 100644 index 00000000..95007b9b Binary files /dev/null and b/ui/Skins/Metro/res/HorizontalListDivider.png differ diff --git a/ui/Skins/Metro/res/InputField.png b/ui/Skins/Metro/res/InputField.png new file mode 100644 index 00000000..c03ab634 Binary files /dev/null and b/ui/Skins/Metro/res/InputField.png differ diff --git a/ui/Skins/Metro/res/InputField_Left.png b/ui/Skins/Metro/res/InputField_Left.png new file mode 100644 index 00000000..c03ab634 Binary files /dev/null and b/ui/Skins/Metro/res/InputField_Left.png differ diff --git a/ui/Skins/Metro/res/InputField_Right.png b/ui/Skins/Metro/res/InputField_Right.png new file mode 100644 index 00000000..c03ab634 Binary files /dev/null and b/ui/Skins/Metro/res/InputField_Right.png differ diff --git a/ui/Skins/Metro/res/Keymarker.png b/ui/Skins/Metro/res/Keymarker.png new file mode 100644 index 00000000..e5723fca Binary files /dev/null and b/ui/Skins/Metro/res/Keymarker.png differ diff --git a/ui/Skins/Metro/res/Keymarker_Left.png b/ui/Skins/Metro/res/Keymarker_Left.png new file mode 100644 index 00000000..0fd4ce2f Binary files /dev/null and b/ui/Skins/Metro/res/Keymarker_Left.png differ diff --git a/ui/Skins/Metro/res/Keymarker_Right.png b/ui/Skins/Metro/res/Keymarker_Right.png new file mode 100644 index 00000000..f6c3ca93 Binary files /dev/null and b/ui/Skins/Metro/res/Keymarker_Right.png differ diff --git a/ui/Skins/Metro/res/ListDivider.png b/ui/Skins/Metro/res/ListDivider.png new file mode 100644 index 00000000..6e48500d Binary files /dev/null and b/ui/Skins/Metro/res/ListDivider.png differ diff --git a/ui/Skins/Metro/res/Popup_BL.png b/ui/Skins/Metro/res/Popup_BL.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Popup_BL.png differ diff --git a/ui/Skins/Metro/res/Popup_BR.png b/ui/Skins/Metro/res/Popup_BR.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Popup_BR.png differ diff --git a/ui/Skins/Metro/res/Popup_Bottom.png b/ui/Skins/Metro/res/Popup_Bottom.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Popup_Bottom.png differ diff --git a/ui/Skins/Metro/res/Popup_Left.png b/ui/Skins/Metro/res/Popup_Left.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Popup_Left.png differ diff --git a/ui/Skins/Metro/res/Popup_Mid.png b/ui/Skins/Metro/res/Popup_Mid.png new file mode 100644 index 00000000..77a13c5b Binary files /dev/null and b/ui/Skins/Metro/res/Popup_Mid.png differ diff --git a/ui/Skins/Metro/res/Popup_Right.png b/ui/Skins/Metro/res/Popup_Right.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Popup_Right.png differ diff --git a/ui/Skins/Metro/res/Popup_TL.png b/ui/Skins/Metro/res/Popup_TL.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Popup_TL.png differ diff --git a/ui/Skins/Metro/res/Popup_TR.png b/ui/Skins/Metro/res/Popup_TR.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Popup_TR.png differ diff --git a/ui/Skins/Metro/res/Popup_Top.png b/ui/Skins/Metro/res/Popup_Top.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/Popup_Top.png differ diff --git a/ui/Skins/Metro/res/ProgressbarBg.png b/ui/Skins/Metro/res/ProgressbarBg.png new file mode 100644 index 00000000..0e6c65b0 Binary files /dev/null and b/ui/Skins/Metro/res/ProgressbarBg.png differ diff --git a/ui/Skins/Metro/res/ProgressbarBg_Left.png b/ui/Skins/Metro/res/ProgressbarBg_Left.png new file mode 100644 index 00000000..0e6c65b0 Binary files /dev/null and b/ui/Skins/Metro/res/ProgressbarBg_Left.png differ diff --git a/ui/Skins/Metro/res/ProgressbarBg_Right.png b/ui/Skins/Metro/res/ProgressbarBg_Right.png new file mode 100644 index 00000000..0e6c65b0 Binary files /dev/null and b/ui/Skins/Metro/res/ProgressbarBg_Right.png differ diff --git a/ui/Skins/Metro/res/ProgressbarFg.png b/ui/Skins/Metro/res/ProgressbarFg.png new file mode 100644 index 00000000..62b2190b Binary files /dev/null and b/ui/Skins/Metro/res/ProgressbarFg.png differ diff --git a/ui/Skins/Metro/res/ProgressbarFg_Finish.png b/ui/Skins/Metro/res/ProgressbarFg_Finish.png new file mode 100644 index 00000000..62b2190b Binary files /dev/null and b/ui/Skins/Metro/res/ProgressbarFg_Finish.png differ diff --git a/ui/Skins/Metro/res/ProgressbarFg_Left.png b/ui/Skins/Metro/res/ProgressbarFg_Left.png new file mode 100644 index 00000000..62b2190b Binary files /dev/null and b/ui/Skins/Metro/res/ProgressbarFg_Left.png differ diff --git a/ui/Skins/Metro/res/ProgressbarFg_Right.png b/ui/Skins/Metro/res/ProgressbarFg_Right.png new file mode 100644 index 00000000..62b2190b Binary files /dev/null and b/ui/Skins/Metro/res/ProgressbarFg_Right.png differ diff --git a/ui/Skins/Metro/res/Radiobox_Active.png b/ui/Skins/Metro/res/Radiobox_Active.png new file mode 100644 index 00000000..ecffab49 Binary files /dev/null and b/ui/Skins/Metro/res/Radiobox_Active.png differ diff --git a/ui/Skins/Metro/res/Radiobox_ActiveHover.png b/ui/Skins/Metro/res/Radiobox_ActiveHover.png new file mode 100644 index 00000000..ecffab49 Binary files /dev/null and b/ui/Skins/Metro/res/Radiobox_ActiveHover.png differ diff --git a/ui/Skins/Metro/res/Radiobox_Default.png b/ui/Skins/Metro/res/Radiobox_Default.png new file mode 100644 index 00000000..10647eb7 Binary files /dev/null and b/ui/Skins/Metro/res/Radiobox_Default.png differ diff --git a/ui/Skins/Metro/res/Radiobox_DefaultHover.png b/ui/Skins/Metro/res/Radiobox_DefaultHover.png new file mode 100644 index 00000000..10647eb7 Binary files /dev/null and b/ui/Skins/Metro/res/Radiobox_DefaultHover.png differ diff --git a/ui/Skins/Metro/res/ScrollShadowBottom.png b/ui/Skins/Metro/res/ScrollShadowBottom.png new file mode 100644 index 00000000..ab3e37b1 Binary files /dev/null and b/ui/Skins/Metro/res/ScrollShadowBottom.png differ diff --git a/ui/Skins/Metro/res/ScrollShadowBottom_Left.png b/ui/Skins/Metro/res/ScrollShadowBottom_Left.png new file mode 100644 index 00000000..929a34af Binary files /dev/null and b/ui/Skins/Metro/res/ScrollShadowBottom_Left.png differ diff --git a/ui/Skins/Metro/res/ScrollShadowBottom_Right.png b/ui/Skins/Metro/res/ScrollShadowBottom_Right.png new file mode 100644 index 00000000..7363bf62 Binary files /dev/null and b/ui/Skins/Metro/res/ScrollShadowBottom_Right.png differ diff --git a/ui/Skins/Metro/res/ScrollShadowTop.png b/ui/Skins/Metro/res/ScrollShadowTop.png new file mode 100644 index 00000000..55f42507 Binary files /dev/null and b/ui/Skins/Metro/res/ScrollShadowTop.png differ diff --git a/ui/Skins/Metro/res/ScrollShadowTop_Left.png b/ui/Skins/Metro/res/ScrollShadowTop_Left.png new file mode 100644 index 00000000..e4d93dbd Binary files /dev/null and b/ui/Skins/Metro/res/ScrollShadowTop_Left.png differ diff --git a/ui/Skins/Metro/res/ScrollShadowTop_Right.png b/ui/Skins/Metro/res/ScrollShadowTop_Right.png new file mode 100644 index 00000000..bb52bcdb Binary files /dev/null and b/ui/Skins/Metro/res/ScrollShadowTop_Right.png differ diff --git a/ui/Skins/Metro/res/ScrollbarHandle.png b/ui/Skins/Metro/res/ScrollbarHandle.png new file mode 100644 index 00000000..64559565 Binary files /dev/null and b/ui/Skins/Metro/res/ScrollbarHandle.png differ diff --git a/ui/Skins/Metro/res/ScrollbarHandle_Bottom.png b/ui/Skins/Metro/res/ScrollbarHandle_Bottom.png new file mode 100644 index 00000000..f475f5d6 Binary files /dev/null and b/ui/Skins/Metro/res/ScrollbarHandle_Bottom.png differ diff --git a/ui/Skins/Metro/res/ScrollbarHandle_Top.png b/ui/Skins/Metro/res/ScrollbarHandle_Top.png new file mode 100644 index 00000000..62faa394 Binary files /dev/null and b/ui/Skins/Metro/res/ScrollbarHandle_Top.png differ diff --git a/ui/Skins/Metro/res/Selectbox.png b/ui/Skins/Metro/res/Selectbox.png new file mode 100644 index 00000000..4f2fa117 Binary files /dev/null and b/ui/Skins/Metro/res/Selectbox.png differ diff --git a/ui/Skins/Metro/res/SelectboxActive.png b/ui/Skins/Metro/res/SelectboxActive.png new file mode 100644 index 00000000..4f2fa117 Binary files /dev/null and b/ui/Skins/Metro/res/SelectboxActive.png differ diff --git a/ui/Skins/Metro/res/SelectboxActive_Left.png b/ui/Skins/Metro/res/SelectboxActive_Left.png new file mode 100644 index 00000000..4f2fa117 Binary files /dev/null and b/ui/Skins/Metro/res/SelectboxActive_Left.png differ diff --git a/ui/Skins/Metro/res/SelectboxOpen_Right.png b/ui/Skins/Metro/res/SelectboxOpen_Right.png new file mode 100644 index 00000000..58986300 Binary files /dev/null and b/ui/Skins/Metro/res/SelectboxOpen_Right.png differ diff --git a/ui/Skins/Metro/res/Selectbox_Left.png b/ui/Skins/Metro/res/Selectbox_Left.png new file mode 100644 index 00000000..4f2fa117 Binary files /dev/null and b/ui/Skins/Metro/res/Selectbox_Left.png differ diff --git a/ui/Skins/Metro/res/Selectbox_Right.png b/ui/Skins/Metro/res/Selectbox_Right.png new file mode 100644 index 00000000..d5d32ab0 Binary files /dev/null and b/ui/Skins/Metro/res/Selectbox_Right.png differ diff --git a/ui/Skins/Metro/res/SelectdropDivider.png b/ui/Skins/Metro/res/SelectdropDivider.png new file mode 100644 index 00000000..95007b9b Binary files /dev/null and b/ui/Skins/Metro/res/SelectdropDivider.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_BL.png b/ui/Skins/Metro/res/Selectdrop_BL.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_BL.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_BR.png b/ui/Skins/Metro/res/Selectdrop_BR.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_BR.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_Bottom.png b/ui/Skins/Metro/res/Selectdrop_Bottom.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_Bottom.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_Left.png b/ui/Skins/Metro/res/Selectdrop_Left.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_Left.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_Mid.png b/ui/Skins/Metro/res/Selectdrop_Mid.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_Mid.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_Right.png b/ui/Skins/Metro/res/Selectdrop_Right.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_Right.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_TL.png b/ui/Skins/Metro/res/Selectdrop_TL.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_TL.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_TR.png b/ui/Skins/Metro/res/Selectdrop_TR.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_TR.png differ diff --git a/ui/Skins/Metro/res/Selectdrop_Top.png b/ui/Skins/Metro/res/Selectdrop_Top.png new file mode 100644 index 00000000..264bb30d Binary files /dev/null and b/ui/Skins/Metro/res/Selectdrop_Top.png differ diff --git a/ui/Skins/Metro/res/SliderBg.png b/ui/Skins/Metro/res/SliderBg.png new file mode 100644 index 00000000..0e6c65b0 Binary files /dev/null and b/ui/Skins/Metro/res/SliderBg.png differ diff --git a/ui/Skins/Metro/res/SliderBgFill.png b/ui/Skins/Metro/res/SliderBgFill.png new file mode 100644 index 00000000..62b2190b Binary files /dev/null and b/ui/Skins/Metro/res/SliderBgFill.png differ diff --git a/ui/Skins/Metro/res/SliderBgFill_Left.png b/ui/Skins/Metro/res/SliderBgFill_Left.png new file mode 100644 index 00000000..62b2190b Binary files /dev/null and b/ui/Skins/Metro/res/SliderBgFill_Left.png differ diff --git a/ui/Skins/Metro/res/SliderBg_Left.png b/ui/Skins/Metro/res/SliderBg_Left.png new file mode 100644 index 00000000..0e6c65b0 Binary files /dev/null and b/ui/Skins/Metro/res/SliderBg_Left.png differ diff --git a/ui/Skins/Metro/res/SliderBg_Right.png b/ui/Skins/Metro/res/SliderBg_Right.png new file mode 100644 index 00000000..0e6c65b0 Binary files /dev/null and b/ui/Skins/Metro/res/SliderBg_Right.png differ diff --git a/ui/Skins/Metro/res/SliderKnob.png b/ui/Skins/Metro/res/SliderKnob.png new file mode 100644 index 00000000..507e900b Binary files /dev/null and b/ui/Skins/Metro/res/SliderKnob.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_BL.png b/ui/Skins/Metro/res/SunkenFrame_BL.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_BL.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_BR.png b/ui/Skins/Metro/res/SunkenFrame_BR.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_BR.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_Bottom.png b/ui/Skins/Metro/res/SunkenFrame_Bottom.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_Bottom.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_Left.png b/ui/Skins/Metro/res/SunkenFrame_Left.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_Left.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_Mid.png b/ui/Skins/Metro/res/SunkenFrame_Mid.png new file mode 100644 index 00000000..77a13c5b Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_Mid.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_Right.png b/ui/Skins/Metro/res/SunkenFrame_Right.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_Right.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_TL.png b/ui/Skins/Metro/res/SunkenFrame_TL.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_TL.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_TR.png b/ui/Skins/Metro/res/SunkenFrame_TR.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_TR.png differ diff --git a/ui/Skins/Metro/res/SunkenFrame_Top.png b/ui/Skins/Metro/res/SunkenFrame_Top.png new file mode 100644 index 00000000..54f15146 Binary files /dev/null and b/ui/Skins/Metro/res/SunkenFrame_Top.png differ diff --git a/ui/Skins/Metro/res/atlas.png b/ui/Skins/Metro/res/atlas.png new file mode 100644 index 00000000..732a2a63 Binary files /dev/null and b/ui/Skins/Metro/res/atlas.png differ diff --git a/ui/Skins/Metro/res/atlas.txt b/ui/Skins/Metro/res/atlas.txt new file mode 100644 index 00000000..c6dd47b4 --- /dev/null +++ b/ui/Skins/Metro/res/atlas.txt @@ -0,0 +1,101 @@ +ColorpickerFieldOverlay 0 0 132 132 +ColorpickerHueSlider 132 0 24 132 +ColorpickerActiveColorOverlay 156 0 44 44 +SelectboxOpen_Right 200 0 37 36 +Selectbox_Right 200 36 37 36 +ColorpickerHueHandle 156 72 50 22 +ColorpickerPreviewBg 206 72 31 31 +ColorpickerPreviewOverlay 156 44 27 27 +ScrollShadowBottom_Left 237 0 18 18 +ScrollShadowBottom_Right 237 18 18 18 +ScrollShadowTop_Left 237 36 18 18 +ScrollShadowTop_Right 237 54 18 18 +Checkbox_Checked 183 44 17 17 +Checkbox_CheckedHover 237 72 17 17 +Checkbox_Default 237 89 17 17 +Checkbox_DefaultHover 156 94 17 17 +Radiobox_Active 173 94 17 17 +Radiobox_ActiveHover 190 103 17 17 +Radiobox_Default 207 103 17 17 +Radiobox_DefaultHover 224 106 17 17 +ColorpickerFieldHandle 156 111 16 17 +Keymarker_Left 241 106 6 27 +Keymarker_Right 247 106 6 27 +SliderKnob 172 111 8 18 +ScrollbarHandle_Top 183 61 11 6 +ScrollbarHandle_Bottom 183 67 11 5 +InputField 255 0 1 39 +InputField_Left 255 39 1 39 +InputField_Right 254 72 1 39 +ButtonDefault 255 78 1 36 +ButtonDefaultFocus 253 106 1 36 +ButtonDefaultFocus_Left 180 111 1 36 +ButtonDefaultFocus_Right 181 111 1 36 +ButtonDefault_Left 182 111 1 36 +ButtonDefault_Right 183 111 1 36 +ButtonGreen 184 111 1 36 +ButtonGreenFocus 185 111 1 36 +ButtonGreenFocus_Left 186 111 1 36 +ButtonGreenFocus_Right 187 111 1 36 +ButtonGreen_Left 188 111 1 36 +ButtonGreen_Right 189 111 1 36 +Selectbox 254 111 1 36 +SelectboxActive 255 114 1 36 +SelectboxActive_Left 190 120 1 36 +Selectbox_Left 191 120 1 36 +Keymarker 192 120 1 27 +ScrollShadowBottom 193 120 1 18 +ScrollShadowTop 194 120 1 18 +ScrollbarHandle 156 71 11 1 +ProgressbarBg 194 61 1 5 +ProgressbarBg_Left 195 61 1 5 +ProgressbarBg_Right 196 61 1 5 +ProgressbarFg 197 61 1 5 +ProgressbarFg_Finish 198 61 1 5 +ProgressbarFg_Left 199 61 1 5 +ProgressbarFg_Right 194 66 1 5 +SliderBg 195 66 1 5 +SliderBgFill 196 66 1 5 +SliderBgFill_Left 197 66 1 5 +SliderBg_Left 198 66 1 5 +SliderBg_Right 199 66 1 5 +blank 167 71 1 1 +Frame_BL 168 71 1 1 +Frame_Bottom 169 71 1 1 +Frame_BR 170 71 1 1 +Frame_Left 171 71 1 1 +Frame_Mid 172 71 1 1 +Frame_Right 173 71 1 1 +Frame_TL 174 71 1 1 +Frame_Top 175 71 1 1 +Frame_TR 176 71 1 1 +HorizontalListDivider 177 71 1 1 +ListDivider 178 71 1 1 +Popup_BL 179 71 1 1 +Popup_Bottom 180 71 1 1 +Popup_BR 181 71 1 1 +Popup_Left 182 71 1 1 +Popup_Mid 194 71 1 1 +Popup_Right 195 71 1 1 +Popup_TL 196 71 1 1 +Popup_Top 197 71 1 1 +Popup_TR 198 71 1 1 +SelectdropDivider 199 71 1 1 +Selectdrop_BL 190 94 1 1 +Selectdrop_Bottom 191 94 1 1 +Selectdrop_BR 192 94 1 1 +Selectdrop_Left 193 94 1 1 +Selectdrop_Mid 194 94 1 1 +Selectdrop_Right 195 94 1 1 +Selectdrop_TL 196 94 1 1 +Selectdrop_Top 197 94 1 1 +Selectdrop_TR 198 94 1 1 +SunkenFrame_BL 199 94 1 1 +SunkenFrame_Bottom 200 94 1 1 +SunkenFrame_BR 201 94 1 1 +SunkenFrame_Left 202 94 1 1 +SunkenFrame_Mid 203 94 1 1 +SunkenFrame_Right 204 94 1 1 +SunkenFrame_TL 205 94 1 1 +SunkenFrame_Top 190 95 1 1 +SunkenFrame_TR 191 95 1 1 diff --git a/ui/Skins/Metro/res/blank.png b/ui/Skins/Metro/res/blank.png new file mode 100644 index 00000000..fc4efe0f Binary files /dev/null and b/ui/Skins/Metro/res/blank.png differ diff --git a/ui/Skins/__init__.py b/ui/Skins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/lui.pyd b/ui/lui.pyd new file mode 100644 index 00000000..0c4dd5d8 Binary files /dev/null and b/ui/lui.pyd differ diff --git a/ui/lui_function.py b/ui/lui_function.py new file mode 100644 index 00000000..788132db --- /dev/null +++ b/ui/lui_function.py @@ -0,0 +1,2556 @@ +import os +import sys +from pathlib import Path +import panda3d.core as p3d +from panda3d.core import NodePath, CardMaker +from imgui_bundle import imgui, imgui_ctx + +# Ensure ui directory is in path and lui is correctly patched +UI_DIR = Path(__file__).resolve().parent +if str(UI_DIR) not in sys.path: + sys.path.insert(0, str(UI_DIR)) + +# Add Builtin to path +BUILTIN_DIR = UI_DIR / "Builtin" +if str(BUILTIN_DIR) not in sys.path: + sys.path.insert(0, str(BUILTIN_DIR)) + +try: + import lui + # Monkey patch for Builtin components + import panda3d + panda3d.lui = lui + sys.modules["panda3d.lui"] = lui + + from Builtin.LUIRegion import LUIRegion + from Builtin.LUIInputHandler import LUIInputHandler + from Builtin.LUIButton import LUIButton + from Builtin.LUILabel import LUILabel + from Builtin.LUIInputField import LUIInputField + from Builtin.LUISlider import LUISlider + from Builtin.LUIFrame import LUIFrame + from Builtin.LUISkin import LUIDefaultSkin + from Builtin.LUISprite import LUISprite + from Builtin.LUIObject import LUIObject + from Builtin.LUICheckbox import LUICheckbox + from Builtin.LUIProgressbar import LUIProgressbar + from Builtin.LUISelectbox import LUISelectbox + from Builtin.LUIScrollableRegion import LUIScrollableRegion + from Builtin.LUITabbedFrame import LUITabbedFrame + from Builtin.LUIVerticalLayout import LUIVerticalLayout + from Builtin.LUIHorizontalLayout import LUIHorizontalLayout +except ImportError as e: + print(f"Error: Failed to import LUI: {e}") + lui = None + +class luiFunction: + + def create_canvas(manager, name=None): + """创建一个可视化的 UI 面板作为画布""" + manager.canvas_counter += 1 + canvas_name = name or f"{manager.canvas_counter}" + + # 获取窗口尺寸 + win_width = 800 + win_height = 600 + if hasattr(manager.world, 'win'): + win_width = manager.world.win.getXSize() + win_height = manager.world.win.getYSize() + + # 计算场景窗口区域(排除Hierarchy面板) + left_offset = 0 + if hasattr(manager.world, 'hierarchy_size'): + left_offset = manager.world.hierarchy_size.x + + target_width = win_width - left_offset + target_height = win_height + + canvas_panel = LUIObject(parent=manager.overlay_root, x=left_offset, y=0) + + # 显式设置尺寸 + canvas_panel.width = target_width + canvas_panel.height = target_height + canvas_panel.solid = True # 接收事件 + + # 创建背景 Sprite + bg = LUISprite(canvas_panel, "blank", "skin") + bg.width = target_width + bg.height = target_height + bg.color = (0.5, 0.5, 0.5, 1.0) # 半透明灰色背景,可见但不遮挡ImGui + # Canvas背景在LUI内部的底层 + if hasattr(bg, 'set_sort'): + bg.set_sort(-1) # LUI内部的底层 + + # Canvas面板使用默认层级(继承自LUI Region的sort=5) + # 不需要额外设置sort,会自动继承LUI Region的层级 + canvas_panel.solid = True # 接收事件,用于Canvas交互 + + # 创建Canvas的NodePath节点用于层级树显示 + canvas_node = NodePath(canvas_name) + canvas_node.reparent_to(manager.world.render) + + # 存储Canvas数据 + canvas_data = { + 'name': canvas_name, + 'node': canvas_node, + 'panel': canvas_panel, + 'visible': True, + 'background': bg, + 'fill_mode': True, # 默认开启填充模式 + 'margins': { + 'left': 240, + 'right': 480, # 默认右侧面板宽度 + 'top': 89, + 'bottom': 220 # 默认底部面板高度 + } + } + + # 应用初始几何计算 (调用 manager 的方法) + # 注意:这里需要 manager 是 public 的或者我们访问 protected 方法 + if hasattr(manager, '_update_canvas_geometry'): + manager._update_canvas_geometry(canvas_data) + + # 添加面板标题 + title_text = f"Canvas: {canvas_name}" + title = LUILabel(text=title_text, font_size=16) + title.set_parent(canvas_panel) + title.set_pos(10, 10) # 内部 padding + if hasattr(title, 'set_color'): + title.set_color((0.8, 0.8, 0.8, 1.0)) + + # 存储标题引用以便后续修改 + canvas_data['title_label'] = title + + # 启用Canvas拖动 + if hasattr(manager, '_make_canvas_draggable'): + manager._make_canvas_draggable(canvas_panel) + + manager.canvases.append(canvas_data) + manager.current_canvas_index = len(manager.canvases) - 1 + manager.switch_canvas(manager.current_canvas_index) + + # 调试输出:确认Canvas层级 + print(f"✓ Created Canvas: {canvas_name}") + print(f" Canvas panel sort: {getattr(canvas_panel, 'sort', 'N/A')}") + print(f" Background sprite sort: {getattr(bg, 'sort', 'N/A')}") + + if hasattr(manager.world, 'updateSceneTree'): + manager.world.updateSceneTree() + return canvas_data + + def create_button(manager, text="Button", x=100, y=100, parent=None): + """Create a LUI Button""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + btn = LUIButton(parent=parent, text=text, x=x, y=y) + + # Ensure size is initialized so selection bounds match the actual button + default_w = 100 + default_h = 30 + try: + if not hasattr(btn, 'width') or btn.width is None or btn.width <= 0: + btn.width = default_w + if not hasattr(btn, 'height') or btn.height is None or btn.height <= 0: + btn.height = default_h + except Exception: + pass + + width = getattr(btn, 'width', default_w) or default_w + height = getattr(btn, 'height', default_h) or default_h + + + # 设置组件层级,在Canvas背景之上 + if hasattr(btn, 'set_sort'): + btn.set_sort(1) # 在Canvas背景之上 + + comp_data = { + 'type': 'Button', + 'object': btn, + 'widget': btn, + 'text': text, + 'left': x, + 'top': y, + 'width': float(width), + 'height': float(height), + 'canvas_index': manager.current_canvas_index, + 'name': f"Button_{len(manager.components)}", + 'color': (1.0, 1.0, 1.0, 1.0), + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + if hasattr(manager, '_setup_component_drag'): + manager._setup_component_drag(comp_data, comp_index) + + if hasattr(manager, '_add_to_scene_tree'): + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return btn + + def create_label(manager,text="Text", x=100, y=100, font_size=14, parent=None): + """Create a LUI Text""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + lbl = LUILabel(parent=parent, text=text, x=x, y=y, solid=True, font_size=font_size) + + # 设置组件层级,在Canvas背景之上 + if hasattr(lbl, 'set_sort'): + lbl.set_sort(1) # 在Canvas背景之上 + + comp_data = { + 'type': 'Text', + 'object': lbl, + 'text': text, + 'left': x, + 'top': y, + 'width': 80, + 'height': 20, + 'canvas_index': manager.current_canvas_index, + 'name': f"Text_{len(manager.components)}", + 'color': (1, 1, 1, 1), + 'font_size': font_size, + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + + # 添加到层级树 + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return lbl + + def create_input_field(manager,text="", x=100, y=100, width=200, parent=None): + """Create a LUI InputField""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + inf = LUIInputField(parent=parent, value=text, x=x, y=y, width=width) + + comp_data = { + 'type': 'InputField', + 'object': inf, + 'text': text, + 'left': x, + 'top': y, + 'width': width, + 'height': 24, + 'canvas_index': manager.current_canvas_index, + 'name': f"InputField_{len(manager.components)}", + 'value': text, + 'placeholder': "输入文本...", + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + + # 添加到层级树 + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return inf + + def create_slider(manager,x=100, y=100, width=200, parent=None): + """Create a LUI Slider""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + sld = LUISlider(parent=parent, x=x, y=y, width=width) + if hasattr(sld, 'set_height'): + sld.set_height(16) + if hasattr(sld, 'height'): + sld.height = 16 + + comp_data = { + 'type': 'Slider', + 'object': sld, + 'left': x, + 'top': y, + 'width': width, + 'height': 16, + 'canvas_index': manager.current_canvas_index, + 'name': f"Slider_{len(manager.components)}", + 'min_value': 0.0, + 'max_value': 100.0, + 'value': 50.0, + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + + # 添加到层级树 + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return sld + + def create_frame(manager,x=100, y=100, width=300, height=200, parent=None): + """Create a LUI Frame""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + frm = LUIFrame(parent=parent, x=x, y=y, width=width, height=height) + + comp_data = { + 'type': 'Frame', + 'object': frm, + 'left': x, + 'top': y, + 'width': width, + 'height': height, + 'canvas_index': manager.current_canvas_index, + 'name': f"Frame_{len(manager.components)}", + 'color': (0.7, 0.7, 0.7, 0.8), + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + + # 添加到层级树 + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return frm + + + + + def create_vertical_layout(manager, x=100, y=100, width=300, height=200, spacing=0.0, parent=None): + """Create a LUI Vertical Layout""" + if not manager.lui_enabled: return None + + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + container = LUIObject(parent=parent, x=x, y=y) + if hasattr(container, 'width'): + container.width = width + if hasattr(container, 'height'): + container.height = height + if hasattr(container, 'solid'): + container.solid = True + + layout = LUIVerticalLayout(parent=container, spacing=spacing) + + padding_left = 0.0 + padding_right = 0.0 + padding_top = 0.0 + padding_bottom = 0.0 + + inner_w = max(1.0, float(width) - (padding_left + padding_right)) + inner_h = max(1.0, float(height) - (padding_top + padding_bottom)) + + if hasattr(layout, 'left'): + layout.left = padding_left + if hasattr(layout, 'top'): + layout.top = padding_top + if hasattr(layout, 'width'): + layout.width = inner_w + if hasattr(layout, 'height'): + layout.height = inner_h + + comp_data = { + 'type': 'VerticalLayout', + 'object': container, + 'layout_obj': layout, + 'left': x, + 'top': y, + 'width': width, + 'height': height, + 'layout_spacing': spacing, + 'layout_padding_left': padding_left, + 'layout_padding_right': padding_right, + 'layout_padding_top': padding_top, + 'layout_padding_bottom': padding_bottom, + 'layout_align': 'start', + 'layout_wrap': True, + 'layout_line_spacing': 0.0, + 'canvas_index': manager.current_canvas_index, + 'name': f"VerticalLayout_{len(manager.components)}", + 'sort': 1, + 'draggable': True + } + + comp_index = len(manager.components) + if hasattr(manager, '_setup_component_drag'): + manager._setup_component_drag(comp_data, comp_index) + + if hasattr(manager, '_add_to_scene_tree'): + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return container + + def create_horizontal_layout(manager, x=100, y=100, width=300, height=200, spacing=0.0, parent=None): + """Create a LUI Horizontal Layout""" + if not manager.lui_enabled: return None + + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + container = LUIObject(parent=parent, x=x, y=y) + if hasattr(container, 'width'): + container.width = width + if hasattr(container, 'height'): + container.height = height + if hasattr(container, 'solid'): + container.solid = True + + layout = LUIHorizontalLayout(parent=container, spacing=spacing) + + padding_left = 0.0 + padding_right = 0.0 + padding_top = 0.0 + padding_bottom = 0.0 + + inner_w = max(1.0, float(width) - (padding_left + padding_right)) + inner_h = max(1.0, float(height) - (padding_top + padding_bottom)) + + if hasattr(layout, 'left'): + layout.left = padding_left + if hasattr(layout, 'top'): + layout.top = padding_top + if hasattr(layout, 'width'): + layout.width = inner_w + if hasattr(layout, 'height'): + layout.height = inner_h + + comp_data = { + 'type': 'HorizontalLayout', + 'object': container, + 'layout_obj': layout, + 'left': x, + 'top': y, + 'width': width, + 'height': height, + 'layout_spacing': spacing, + 'layout_padding_left': padding_left, + 'layout_padding_right': padding_right, + 'layout_padding_top': padding_top, + 'layout_padding_bottom': padding_bottom, + 'layout_align': 'start', + 'layout_wrap': True, + 'layout_line_spacing': 0.0, + 'canvas_index': manager.current_canvas_index, + 'name': f"HorizontalLayout_{len(manager.components)}", + 'sort': 1, + 'draggable': True + } + + comp_index = len(manager.components) + if hasattr(manager, '_setup_component_drag'): + manager._setup_component_drag(comp_data, comp_index) + + if hasattr(manager, '_add_to_scene_tree'): + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return container + + def create_checkbox(manager, label="Checkbox", x=100, y=100, parent=None): + """Create a LUI Checkbox""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + chk = LUICheckbox(parent=parent, label=label, x=x, y=y) + + comp_data = { + 'type': 'Checkbox', + 'object': chk, + 'text': label, + 'left': x, + 'top': y, + 'width': 120, + 'height': 20, + 'canvas_index': manager.current_canvas_index, + 'name': f"Checkbox_{len(manager.components)}", + 'checked': False, + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + + # 添加到层级树 + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return chk + + def create_plane(manager, x=100, y=100, width=100, height=100, color=(1,1,1,1), parent=None): + """Create a LUI Plane (using Sprite with blank texture)""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + # 创建LUIObject容器 + obj = LUIObject(parent=parent, x=x, y=y) + obj.width = width + obj.height = height + obj.solid = True # 重要:设置为solid才能接收鼠标事件 + + # 设置组件层级,在Canvas背景之上 (Plane) + if hasattr(obj, 'set_sort'): + obj.set_sort(1) # 在Canvas背景之上 + + # 创建Sprite作为平面背景 + spr = LUISprite(obj, "blank", "skin") + spr.width = width + spr.height = height + spr.color = color + spr.left = 0 + spr.top = 0 + + comp_data = { + 'type': 'Plane', + 'object': obj, + 'sprite': spr, + 'left': x, + 'top': y, + 'width': width, + 'height': height, + 'canvas_index': manager.current_canvas_index, + 'name': f"Plane_{len(manager.components)}", + 'color': color, + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + + # 添加到层级树 + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return obj + + def create_image(manager, texture_path="blank", x=100, y=100, width=100, height=100, parent=None): + """Create a LUI Image""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + # 创建LUIObject容器 + obj = LUIObject(parent=parent, x=x, y=y) + obj.width = width + obj.height = height + obj.solid = True # 重要:设置为solid才能接收鼠标事件 + + # 设置组件层级,在Canvas背景之上 (Image) + if hasattr(obj, 'set_sort'): + obj.set_sort(1) # 在Canvas背景之上 + + # 加载纹理 (使用皮肤中的'blank'或外部路径) + # 注意: LUISprite通常使用图集图像名称或纹理 + # 对于外部图像,可能需要加载纹理并使用不同的LUI元素 + # 或将其注册到图集中。LUISprite(parent, image, atlas) + + # 为了简单起见,我们使用默认皮肤sprite,它有'blank' + img_name = "blank" + spr = LUISprite(obj, img_name, "skin") + spr.width = width + spr.height = height + spr.left = 0 + spr.top = 0 + + # 设置默认图像颜色(浅灰色,表示这是一个图像占位符) + spr.color = (1.0, 1.0, 1.0, 1.0) + + comp_data = { + 'type': 'Image', + 'object': obj, + 'sprite': spr, + 'left': x, + 'top': y, + 'width': width, + 'height': height, + 'canvas_index': manager.current_canvas_index, + 'name': f"Image_{len(manager.components)}", + 'texture_path': texture_path, + 'color': (1.0, 1.0, 1.0, 1.0), + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + + # 添加到层级树 + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return obj + + def create_video(manager, video_path="", x=100, y=100, width=320, height=240, parent=None): + """Create a LUI Video Player component""" + if not manager.lui_enabled: return None + + # 如果没有指定parent,使用当前Canvas的panel作为parent + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + # 1. 创建容器对象 + obj = LUIObject(parent=parent, x=x, y=y) + obj.width = width + obj.height = height + obj.solid = True + + # 设置层级 + if hasattr(obj, 'set_sort'): + obj.set_sort(1) + + # 2. 准备视频纹理 + video_texture = None + if video_path: + try: + from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData + # Trim whitespace which could cause "file not found" errors + video_path = video_path.strip() + + # Allow network protocols + loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file") + + if "://" in video_path: + # 尝试更稳定的加载序列 + try: + video_src = MovieVideo.get(Filename(video_path)) + video_texture = MovieTexture("RemoteVideo") + if not video_texture.load(video_src): + video_texture = manager.world.loader.loadTexture(video_path) + except: + video_texture = manager.world.loader.loadTexture(video_path) + else: + video_texture = manager.world.loader.loadTexture(Filename.from_os_specific(video_path)) + + if video_texture: + print(f"✓ 视频加载成功: {video_path}") + # 尝试同步视频尺寸比例 + orig_w = video_texture.getOrigFileXSize() + orig_h = video_texture.getOrigFileYSize() + if orig_w > 0 and orig_h > 0: + ratio = orig_w / orig_h + # 保持宽度,调整高度 + new_height = width / ratio + obj.height = new_height + height = new_height # Update local var + except Exception as e: + print(f"⚠ 加载视频失败: {e}") + + + + + # 2.1 ??????????????????????????????????????? + + audio_sound = None + audio_path = "" + audio_from_video = False + if video_path and "://" not in video_path: + try: + audio_path = video_path + audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(video_path)) + if audio_sound: + audio_from_video = True + if hasattr(audio_sound, 'setLoop'): + audio_sound.setLoop(True) + if hasattr(audio_sound, 'setVolume'): + audio_sound.setVolume(1.0) + except Exception: + audio_sound = None + audio_path = "" + + # 3. 创建Sprite + if video_texture: + # 直接使用纹理创建,不使用Atlas + spr = LUISprite(obj, video_texture) + spr.color = (1, 1, 1, 1) # Video needs white to show original colors + else: + # 使用默认黑色背景 + spr = LUISprite(obj, "blank", "skin") + spr.color = (0.0, 0.0, 0.0, 1.0) + + spr.width = width + spr.height = height + + if video_texture: + obj.video_texture = video_texture # Prevent GC + + # Ensure full UVs + if hasattr(spr, 'set_uv_range'): + spr.set_uv_range(0, 0, 1, 1) + + # 如果支持,自动播放 + if hasattr(video_texture, 'play'): + # Stop first to reset state, critical for some video formats + if hasattr(video_texture, 'stop'): + video_texture.stop() + video_texture.play() + if hasattr(video_texture, 'setLoop'): + video_texture.setLoop(True) + if audio_sound: + try: + if hasattr(audio_sound, 'stop'): + audio_sound.stop() + if hasattr(audio_sound, 'play'): + audio_sound.play() + except Exception: + pass + + # 4. 创建简单的控制栏 (可选,这里先做个占位符) + # TODO: Play/Pause controls + + comp_data = { + 'type': 'Video', + 'object': obj, + 'sprite': spr, + 'left': x, + 'top': y, + 'width': width, + 'height': height, + 'canvas_index': manager.current_canvas_index, + 'name': f"Video_{len(manager.components)}", + 'video_path': video_path, + 'texture': video_texture, + 'audio': audio_sound, + 'audio_path': audio_path, + 'audio_from_video': audio_from_video, + 'volume': 1.0, + 'is_playing': True, + 'loop': True, + 'sort': 1 + } + + # 设置拖拽功能 + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + + # 添加到层级树 + manager._add_to_scene_tree(comp_data) + + manager.components.append(comp_data) + return obj + + def create_progressbar(manager, value=50, x=100, y=100, width=200, parent=None): + """Create a LUI Progressbar""" + if not manager.lui_enabled: return None + + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + pg = LUIProgressbar(parent=parent, width=width, value=value) + pg.left = x + pg.top = y + pg.solid = True # Ensure it receives input + + if hasattr(pg, 'set_sort'): + pg.set_sort(1) + + comp_data = { + 'type': 'Progressbar', + 'object': pg, + 'left': x, + 'top': y, + 'width': width, + 'height': 30, # default height + 'canvas_index': manager.current_canvas_index, + 'name': f"Progressbar_{len(manager.components)}", + 'value': value, + 'sort': 1 + } + + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + manager._add_to_scene_tree(comp_data) + manager.components.append(comp_data) + return pg + + def create_selectbox(manager, options=None, x=100, y=100, width=200, parent=None): + """Create a LUI Selectbox""" + if not manager.lui_enabled: return None + + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + if options is None: + options = [(0, "Option 1"), (1, "Option 2"), (2, "Option 3")] + + sb = LUISelectbox(width=width, options=options, selected_option=0) + sb.parent = parent + sb.left = x + sb.top = y + sb.solid = True # Ensure it receives input + + if hasattr(sb, 'set_sort'): + sb.set_sort(1) + + comp_data = { + 'type': 'Selectbox', + 'object': sb, + 'left': x, + 'top': y, + 'width': width, + 'height': 30, # default height + 'canvas_index': manager.current_canvas_index, + 'name': f"Selectbox_{len(manager.components)}", + 'options': options, + 'sort': 1 + } + + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + manager._add_to_scene_tree(comp_data) + manager.components.append(comp_data) + return sb + + def create_scrollable_region(manager, x=100, y=100, width=200, height=200, parent=None): + """Create a LUI Scrollable Region""" + if not manager.lui_enabled: return None + + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + sr = LUIScrollableRegion(parent=parent, width=width, height=height) + sr.left = x + sr.top = y + sr.solid = True # Ensure it receives input + + if hasattr(sr, 'set_sort'): + sr.set_sort(1) + + comp_data = { + 'type': 'ScrollableRegion', + 'object': sr, + 'left': x, + 'top': y, + 'width': width, + 'height': height, + 'canvas_index': manager.current_canvas_index, + 'name': f"ScrollableRegion_{len(manager.components)}", + 'sort': 1 + } + + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + manager._add_to_scene_tree(comp_data) + manager.components.append(comp_data) + return sr + + def create_tabbed_frame(manager, x=100, y=100, width=300, height=200, parent=None): + """Create a LUI Tabbed Frame""" + if not manager.lui_enabled: return None + + if parent is None and manager.current_canvas_index >= 0: + parent = manager.canvases[manager.current_canvas_index]['panel'] + elif parent is None: + parent = manager.overlay_root + + tf = LUITabbedFrame(parent=parent, width=width, height=height) + tf.left = x + tf.top = y + tf.solid = True # Ensure it receives input + + # Add default tabs + f1 = LUIFrame(width=width, height=height) + tf.add("Tab 1", f1) + f2 = LUIFrame(width=width, height=height) + tf.add("Tab 2", f2) + + if hasattr(tf, 'set_sort'): + tf.set_sort(1) + + comp_data = { + 'type': 'TabbedFrame', + 'object': tf, + 'left': x, + 'top': y, + 'width': width, + 'height': height, + 'canvas_index': manager.current_canvas_index, + 'name': f"TabbedFrame_{len(manager.components)}", + 'sort': 1 + } + + comp_index = len(manager.components) + manager._setup_component_drag(comp_data, comp_index) + manager._add_to_scene_tree(comp_data) + manager.components.append(comp_data) + return tf + + @staticmethod + def delete_component(manager, index): + """Delete a LUI component and update all references""" + if index < 0 or index >= len(manager.components): + return + + # 1. Identify all components to delete (recursive) + to_delete = {index} + queue = list(manager.components[index].get('children_indices', [])) + while queue: + curr = queue.pop(0) + if curr not in to_delete: + to_delete.add(curr) + if curr < len(manager.components): + queue.extend(manager.components[curr].get('children_indices', [])) + + # Cleanup any cached outlines for deleted components + if hasattr(manager, 'debug_outlines') and isinstance(manager.debug_outlines, dict): + for del_idx in list(to_delete): + borders = manager.debug_outlines.pop(del_idx, None) + if borders: + for b in borders: + try: + if hasattr(b, 'remove'): + b.remove() + else: + b.visible = False + except Exception: + pass + + # Sort indices descending to delete from end first + sorted_indices = sorted(list(to_delete), reverse=True) + + # Clear selection if affected + if manager.selected_index in to_delete: + manager.selected_index = -1 + if hasattr(manager, '_hide_resize_handles'): + manager._hide_resize_handles() + + # Process deletions + for i in sorted_indices: + if i >= len(manager.components): continue + + deleted_comp = manager.components.pop(i) + + # Cleanup physical objects + if 'object' in deleted_comp: + obj = deleted_comp['object'] + if hasattr(obj, 'parent') and obj.parent: + if hasattr(obj.parent, 'remove_child'): + obj.parent.remove_child(obj) + # Cleanup audio if any + audio = deleted_comp.get("audio") + if audio: + try: + if hasattr(audio, "stop"): audio.stop() + except Exception: + pass + + # Cleanup associated node handles if any + if hasattr(manager, 'resize_handles') and manager.resizing_handle and getattr(manager.resizing_handle, '_resize_index', -1) == i: + manager.resizing_handle = None + + print(f"✓ Deleted LUI component: {deleted_comp.get('name', 'Unknown')}") + + # Shift indices > i down by 1 + if manager.selected_index > i: + manager.selected_index -= 1 + + for c in manager.components: + # Update parent_index + p_idx = c.get('parent_index') + if p_idx is not None and p_idx > i: + c['parent_index'] = p_idx - 1 + elif p_idx == i: + c['parent_index'] = -1 + + # Update children_indices + if 'children_indices' in c: + new_children = [] + for child_idx in c['children_indices']: + if child_idx == i: continue + elif child_idx > i: new_children.append(child_idx - 1) + else: new_children.append(child_idx) + c['children_indices'] = new_children + + # Reindex remaining outlines after component indices shift + if hasattr(manager, 'debug_outlines') and isinstance(manager.debug_outlines, dict) and manager.debug_outlines: + deleted_sorted = sorted(to_delete) + new_outlines = {} + for old_idx, borders in manager.debug_outlines.items(): + shift = 0 + for d in deleted_sorted: + if d < old_idx: + shift += 1 + else: + break + new_outlines[old_idx - shift] = borders + manager.debug_outlines = new_outlines + + def _load_ui_texture(manager, file_path, name_hint="", max_size=1024): + """Load image as a standalone Panda3D Texture (avoid LUI atlas).""" + try: + panda_path = p3d.Filename.from_os_specific(file_path) + pnm = p3d.PNMImage() + if not pnm.read(panda_path): + print(f"⚠ Failed to read image: {file_path}") + return None + + # Ensure alpha exists to avoid unexpected black/transparent results. + if not pnm.hasAlpha(): + pnm.addAlpha() + pnm.alphaFill(1.0) + + orig_x = pnm.getXSize() + orig_y = pnm.getYSize() + + # Power-of-two resize for better compatibility and to avoid atlas issues. + def next_pow2(v): + v = max(1, int(v)) + return 1 << (v - 1).bit_length() + + pot_x = min(next_pow2(orig_x), max_size) + pot_y = min(next_pow2(orig_y), max_size) + + if pot_x != orig_x or pot_y != orig_y: + pnm_scaled = p3d.PNMImage(pot_x, pot_y, pnm.getNumChannels()) + pnm_scaled.quickFilterFrom(pnm) + pnm = pnm_scaled + print(f"⚠ Resized to POT: {orig_x}x{orig_y} -> {pot_x}x{pot_y}") + + tex = p3d.Texture() + safe_name = f"{name_hint}_{os.path.basename(file_path)}" if name_hint else os.path.basename(file_path) + tex.setName(safe_name) + tex.load(pnm) + tex.setKeepRamImage(True) + tex.setMinfilter(p3d.Texture.FT_linear) + tex.setMagfilter(p3d.Texture.FT_linear) + tex.setWrapU(p3d.Texture.WM_clamp) + tex.setWrapV(p3d.Texture.WM_clamp) + tex.setCompression(p3d.Texture.CM_off) + tex.setQualityLevel(p3d.Texture.QL_best) + + return tex + except Exception as e: + print(f"⚠ Texture load failed: {e}") + try: + panda_path = p3d.Filename.from_os_specific(file_path) + return manager.world.loader.loadTexture(panda_path) + except Exception as e2: + print(f"⚠ Texture fallback load failed: {e2}") + return None + + def _get_image_atlas_page(manager, size=2048): + """Get or create an atlas page for UI images.""" + if not hasattr(manager, "_image_atlas_pages"): + manager._image_atlas_pages = [] + + # Try existing pages first + for page in manager._image_atlas_pages: + if page.get("size") == size and not page.get("full", False): + return page + + # Create a new page + pnm = p3d.PNMImage(size, size, 4) + pnm.fill(0, 0, 0) + pnm.alphaFill(0.0) + + tex = p3d.Texture() + tex.setName(f"ui_atlas_{len(manager._image_atlas_pages)}_{size}") + tex.load(pnm) + tex.setKeepRamImage(True) + tex.setMinfilter(p3d.Texture.FT_linear) + tex.setMagfilter(p3d.Texture.FT_linear) + tex.setWrapU(p3d.Texture.WM_clamp) + tex.setWrapV(p3d.Texture.WM_clamp) + tex.setCompression(p3d.Texture.CM_off) + tex.setQualityLevel(p3d.Texture.QL_best) + + page = { + "size": size, + "pnm": pnm, + "tex": tex, + "cursor_x": 0, + "cursor_y": 0, + "row_h": 0, + "full": False, + } + manager._image_atlas_pages.append(page) + return page + + def _add_image_to_atlas(manager, file_path, atlas_size=2048): + """Add image to atlas and return (tex, u0, v0, u1, v1, w, h, orig_w, orig_h).""" + try: + panda_path = p3d.Filename.from_os_specific(file_path) + src = p3d.PNMImage() + if not src.read(panda_path): + print(f"⚠ Failed to read image: {file_path}") + return None + + if not src.hasAlpha(): + src.addAlpha() + src.alphaFill(1.0) + + w = src.getXSize() + h = src.getYSize() + + orig_w = w + orig_h = h + + # Downscale if too large for atlas + if w > atlas_size or h > atlas_size: + scale = min(atlas_size / max(1, w), atlas_size / max(1, h)) + new_w = max(1, int(w * scale)) + new_h = max(1, int(h * scale)) + scaled = p3d.PNMImage(new_w, new_h, src.getNumChannels()) + scaled.quickFilterFrom(src) + src = scaled + w, h = new_w, new_h + print(f"⚠ Downscaled for atlas: {file_path} -> {w}x{h}") + + page = manager.luiFunction._get_image_atlas_page(manager, size=atlas_size) + + # Shelf packing + if page["cursor_x"] + w > atlas_size: + page["cursor_x"] = 0 + page["cursor_y"] += page["row_h"] + page["row_h"] = 0 + + if page["cursor_y"] + h > atlas_size: + page["full"] = True + # Create a new page and try again + page = manager.luiFunction._get_image_atlas_page(manager, size=atlas_size) + if page["cursor_x"] + w > atlas_size or page["cursor_y"] + h > atlas_size: + print(f"⚠ Atlas full for image: {file_path}") + return None + + x = page["cursor_x"] + y = page["cursor_y"] + + page["pnm"].copySubImage(src, x, y, 0, 0, w, h) + page["tex"].load(page["pnm"]) + + page["cursor_x"] += w + page["row_h"] = max(page["row_h"], h) + + size = float(atlas_size) + u0 = x / size + v0 = y / size + u1 = (x + w) / size + v1 = (y + h) / size + + return (page["tex"], u0, v0, u1, v1, w, h, orig_w, orig_h) + except Exception as e: + print(f"⚠ Atlas insert failed: {e}") + return None + + def _draw_component_properties(manager, index): + """绘制组件属性编辑面板""" + if index < 0 or index >= len(manager.components): + return + + comp_data = manager.components[index] + comp_obj = comp_data['object'] + comp_type = comp_data['type'] + widget = comp_data.get('widget',comp_obj) + + # 文本属性 + # Text content + # Text content + if comp_type in ['Button', 'Text', 'Label']: + imgui.text("Text Content") + imgui.separator() + if 'text' in comp_data: + imgui.push_item_width(-1) + changed, new_text = imgui.input_text("##text_input", comp_data['text'], 256) + imgui.pop_item_width() + if changed: + comp_data['text'] = new_text + comp_obj.text = new_text + print(f"Text updated: {new_text}") + + if comp_type in ['Text', 'Label']: + imgui.spacing() + imgui.text("Font Size") + font_size = int(comp_data.get('font_size', 14)) + changed, new_size = imgui.slider_int("##font_size", font_size, 8, 96) + if changed: + comp_data['font_size'] = int(new_size) + try: + if hasattr(comp_obj, '_text') and hasattr(comp_obj._text, 'font_size'): + comp_obj._text.font_size = int(new_size) + if hasattr(comp_obj, '_shadow_text') and hasattr(comp_obj._shadow_text, 'font_size'): + comp_obj._shadow_text.font_size = int(new_size) + if hasattr(comp_obj, 'text_handle') and hasattr(comp_obj.text_handle, 'font_size'): + comp_obj.text_handle.font_size = int(new_size) + except Exception as e: + print(f"Font size update failed: {e}") + + imgui.spacing() + imgui.text("Color") + color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0)) + color = list(color) if isinstance(color, tuple) else color + changed, new_color = imgui.color_edit4("##color_edit", color) + if changed: + comp_data['color'] = tuple(new_color) + if comp_type == 'Button': + if hasattr(comp_obj, '_layout'): + try: + if hasattr(comp_obj._layout, '_sprite_left'): + comp_obj._layout._sprite_left.color = tuple(new_color) + if hasattr(comp_obj._layout, '_sprite_mid'): + comp_obj._layout._sprite_mid.color = tuple(new_color) + if hasattr(comp_obj._layout, '_sprite_right'): + comp_obj._layout._sprite_right.color = tuple(new_color) + except Exception as e: + print(f"Button color set failed: {e}") + else: + if hasattr(comp_obj, '_text'): + comp_obj._text.color = tuple(new_color) + imgui.separator() + + if comp_type == 'Button': + if hasattr(comp_obj, '_layout'): + try: + if hasattr(comp_obj._layout, '_sprite_left'): + comp_obj._layout._sprite_left.color = tuple(new_color) + if hasattr(comp_obj._layout, '_sprite_mid'): + comp_obj._layout._sprite_mid.color = tuple(new_color) + if hasattr(comp_obj._layout, '_sprite_right'): + comp_obj._layout._sprite_right.color = tuple(new_color) + except Exception as e: + print(f"\u8bbe\u7f6e\u6309\u94ae\u989c\u8272\u5931\u8d25: {e}") + else: + if hasattr(comp_obj, '_text'): + comp_obj._text.color = tuple(new_color) + imgui.separator() + imgui.spacing() + imgui.text("\u989c\u8272") + color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0)) + + + if comp_type == 'Button': + imgui.text("\u6309\u94ae\u56fe\u7247") + + def _apply_button_textures(): + normal_tex = comp_data.get('button_texture_ref_normal') or comp_data.get('button_texture_ref') + normal_uv = comp_data.get('button_atlas_uv_normal') or comp_data.get('button_atlas_uv') + hover_tex = comp_data.get('button_texture_ref_hover') + hover_uv = comp_data.get('button_atlas_uv_hover') + pressed_tex = comp_data.get('button_texture_ref_pressed') + pressed_uv = comp_data.get('button_atlas_uv_pressed') + if hasattr(comp_obj, 'set_custom_textures') and normal_tex is not None: + comp_obj.set_custom_textures(normal_tex, normal_uv, hover_tex, hover_uv, pressed_tex, pressed_uv) + elif hasattr(comp_obj, 'set_custom_texture') and normal_tex is not None: + comp_obj.set_custom_texture(normal_tex, normal_uv) + + if imgui.button("\u66f4\u6539\u9ed8\u8ba4\u56fe##button_img_norm", (160, 20)): + selected_path = manager._change_image_texture( + title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6", + filetypes = [ + ("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), + ("PNG", "*.png"), + ("JPEG", "*.jpg;*.jpeg"), + ("\u6240\u6709\u6587\u4ef6", "*.*") + ] + ) + if selected_path: + try: + atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) + if atlas_result: + if len(atlas_result) >= 7: + tex, u0, v0, u1, v1, w, h = atlas_result[:7] + else: + tex, u0, v0, u1, v1 = atlas_result + w = int(max(1, round((u1 - u0) * tex.getXSize()))) + h = int(max(1, round((v1 - v0) * tex.getYSize()))) + comp_data['button_texture_path'] = selected_path + comp_data['button_texture_path_normal'] = selected_path + comp_data['button_atlas_uv'] = (u0, v0, u1, v1) + comp_data['button_atlas_uv_normal'] = (u0, v0, u1, v1) + comp_data['button_atlas_tex'] = tex + comp_data['button_texture_ref'] = tex + comp_data['button_texture_ref_normal'] = tex + comp_data['button_image_size'] = (int(w), int(h)) + comp_data['button_image_size_normal'] = (int(w), int(h)) + _apply_button_textures() + except Exception as e: + print(f"Button texture set failed: {e}") + + if imgui.button("\u66f4\u6539\u60ac\u505c\u56fe##button_img_hover", (160, 20)): + selected_path = manager._change_image_texture( + title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6", + filetypes = [ + ("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), + ("PNG", "*.png"), + ("JPEG", "*.jpg;*.jpeg"), + ("\u6240\u6709\u6587\u4ef6", "*.*") + ] + ) + if selected_path: + try: + atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) + if atlas_result: + if len(atlas_result) >= 7: + tex, u0, v0, u1, v1, w, h = atlas_result[:7] + else: + tex, u0, v0, u1, v1 = atlas_result + w = int(max(1, round((u1 - u0) * tex.getXSize()))) + h = int(max(1, round((v1 - v0) * tex.getYSize()))) + comp_data['button_texture_path_hover'] = selected_path + comp_data['button_atlas_uv_hover'] = (u0, v0, u1, v1) + comp_data['button_texture_ref_hover'] = tex + comp_data['button_image_size_hover'] = (int(w), int(h)) + _apply_button_textures() + except Exception as e: + print(f"Button hover texture set failed: {e}") + + if imgui.button("\u66f4\u6539\u6309\u4e0b\u56fe##button_img_pressed", (160, 20)): + selected_path = manager._change_image_texture( + title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6", + filetypes = [ + ("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), + ("PNG", "*.png"), + ("JPEG", "*.jpg;*.jpeg"), + ("\u6240\u6709\u6587\u4ef6", "*.*") + ] + ) + if selected_path: + try: + atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) + if atlas_result: + if len(atlas_result) >= 7: + tex, u0, v0, u1, v1, w, h = atlas_result[:7] + else: + tex, u0, v0, u1, v1 = atlas_result + w = int(max(1, round((u1 - u0) * tex.getXSize()))) + h = int(max(1, round((v1 - v0) * tex.getYSize()))) + comp_data['button_texture_path_pressed'] = selected_path + comp_data['button_atlas_uv_pressed'] = (u0, v0, u1, v1) + comp_data['button_texture_ref_pressed'] = tex + comp_data['button_image_size_pressed'] = (int(w), int(h)) + _apply_button_textures() + except Exception as e: + print(f"Button pressed texture set failed: {e}") + + if imgui.button("\u4f7f\u7528\u9ed8\u8ba4\u56fe\u5c3a\u5bf8##button_fit", (160, 20)): + w = h = None + if 'button_image_size_normal' in comp_data and comp_data['button_image_size_normal']: + try: + w, h = comp_data['button_image_size_normal'] + except Exception: + w = h = None + elif 'button_image_size' in comp_data and comp_data['button_image_size']: + try: + w, h = comp_data['button_image_size'] + except Exception: + w = h = None + if w is not None and h is not None and w > 0 and h > 0: + comp_data['width'] = float(w) + comp_data['height'] = float(h) + comp_obj.width = float(w) + comp_obj.height = float(h) + if hasattr(comp_obj, '_apply_stretch_sizes'): + comp_obj._apply_stretch_sizes() + + if imgui.button("\u6062\u590d\u9ed8\u8ba4\u6309\u94ae##button_default", (160, 20)): + if hasattr(comp_obj, 'clear_custom_texture'): + comp_obj.clear_custom_texture() + for key in [ + 'button_texture_path', 'button_texture_path_normal', 'button_texture_path_hover', + 'button_texture_path_pressed', 'button_texture_ref', 'button_texture_ref_normal', + 'button_texture_ref_hover', 'button_texture_ref_pressed', 'button_atlas_uv', + 'button_atlas_uv_normal', 'button_atlas_uv_hover', 'button_atlas_uv_pressed', + 'button_image_size', 'button_image_size_normal', 'button_image_size_hover', 'button_image_size_pressed' + ]: + if key in comp_data: + comp_data[key] = None + + if comp_data.get('button_texture_path_normal'): + imgui.text(f"\u9ed8\u8ba4\u56fe: {comp_data['button_texture_path_normal']}") + if comp_data.get('button_texture_path_hover'): + imgui.text(f"\u60ac\u505c\u56fe: {comp_data['button_texture_path_hover']}") + if comp_data.get('button_texture_path_pressed'): + imgui.text(f"\u6309\u4e0b\u56fe: {comp_data['button_texture_path_pressed']}") + + # 布局模式 + layout_mode = comp_data.get('layout_mode', 'manual') + is_fill = (layout_mode == 'fill') + changed, new_fill = imgui.checkbox("填充父级", is_fill) + if changed: + comp_data['layout_mode'] = 'fill' if new_fill else 'manual' + if new_fill: + # Fill overrides anchor positioning + comp_data['anchored_to_parent'] = False + comp_data.setdefault('fill_margin_left', 0.0) + comp_data.setdefault('fill_margin_right', 0.0) + comp_data.setdefault('fill_margin_top', 0.0) + comp_data.setdefault('fill_margin_bottom', 0.0) + if hasattr(manager, '_apply_fill_layout'): + manager._apply_fill_layout(index) + is_fill = (comp_data.get('layout_mode') == 'fill') + if is_fill: + imgui.text_disabled("填充模式下,位置/尺寸由父级决定") + imgui.text("边距") + m_left = float(comp_data.get('fill_margin_left', 0.0)) + m_right = float(comp_data.get('fill_margin_right', 0.0)) + m_top = float(comp_data.get('fill_margin_top', 0.0)) + m_bottom = float(comp_data.get('fill_margin_bottom', 0.0)) + changed_l, new_l = imgui.input_float("左", m_left, 1.0, 10.0, "%.1f") + changed_r, new_r = imgui.input_float("右", m_right, 1.0, 10.0, "%.1f") + changed_t, new_t = imgui.input_float("上", m_top, 1.0, 10.0, "%.1f") + changed_b, new_b = imgui.input_float("下", m_bottom, 1.0, 10.0, "%.1f") + if changed_l or changed_r or changed_t or changed_b: + comp_data['fill_margin_left'] = new_l + comp_data['fill_margin_right'] = new_r + comp_data['fill_margin_top'] = new_t + comp_data['fill_margin_bottom'] = new_b + if hasattr(manager, '_apply_fill_layout'): + manager._apply_fill_layout(index) + imgui.separator() + + # 位置属性 + imgui.text("位置") + if is_fill: + imgui.text(f"Left: {comp_data.get('left', 0.0):.1f}") + imgui.text(f"Top: {comp_data.get('top', 0.0):.1f}") + else: + if 'left' in comp_data: + changed, new_left = imgui.input_float("Left", comp_data['left'], 1.0, 10.0, "%.1f") + if changed: + comp_data['left'] = new_left + comp_obj.left = new_left + + if 'top' in comp_data: + changed, new_top = imgui.input_float("Top", comp_data['top'], 1.0, 10.0, "%.1f") + if changed: + comp_data['top'] = new_top + comp_obj.top = new_top + + # 尺寸属性 + imgui.spacing() + imgui.text("尺寸") + if is_fill: + imgui.text(f"Width: {comp_data.get('width', 0.0):.1f}") + imgui.text(f"Height: {comp_data.get('height', 0.0):.1f}") + else: + width_changed = False + height_changed = False + if 'width' in comp_data: + changed, new_width = imgui.input_float("Width", comp_data['width'], 1.0, 10.0, "%.1f") + if changed and new_width > 0: + comp_data['width'] = new_width + width_changed = True + if hasattr(comp_obj, 'width'): + comp_obj.width = new_width + # 同步更新内部 Sprite (针对 Image/Plane/Video) + if 'sprite' in comp_data: + comp_data['sprite'].width = new_width + if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'): + comp_obj._apply_stretch_sizes() + if comp_type == 'Slider' and hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + if comp_type == 'InputField' and hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + + if 'height' in comp_data: + changed, new_height = imgui.input_float("Height", comp_data['height'], 1.0, 10.0, "%.1f") + if changed and new_height > 0: + comp_data['height'] = new_height + height_changed = True + if hasattr(comp_obj, 'height'): + comp_obj.height = new_height + # 同步更新内部 Sprite (针对 Image/Plane/Video) + if 'sprite' in comp_data: + comp_data['sprite'].height = new_height + if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'): + comp_obj._apply_stretch_sizes() + if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + + if (width_changed or height_changed) and hasattr(manager, '_update_anchored_children'): + manager._update_anchored_children(index) + + + if comp_type in ['VerticalLayout', 'HorizontalLayout'] and hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) + + # Layout group properties + if comp_type in ['VerticalLayout', 'HorizontalLayout']: + imgui.spacing() + imgui.text("Layout Group") + + spacing = float(comp_data.get('layout_spacing', 0.0)) + changed, new_spacing = imgui.input_float("Spacing", spacing, 1.0, 10.0, "%.1f") + if changed: + comp_data['layout_spacing'] = new_spacing + if hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) + + imgui.text("Padding") + pad_left = float(comp_data.get('layout_padding_left', 0.0)) + pad_right = float(comp_data.get('layout_padding_right', 0.0)) + pad_top = float(comp_data.get('layout_padding_top', 0.0)) + pad_bottom = float(comp_data.get('layout_padding_bottom', 0.0)) + + changed_l, new_l = imgui.input_float("Pad Left", pad_left, 1.0, 10.0, "%.1f") + changed_r, new_r = imgui.input_float("Pad Right", pad_right, 1.0, 10.0, "%.1f") + changed_t, new_t = imgui.input_float("Pad Top", pad_top, 1.0, 10.0, "%.1f") + changed_b, new_b = imgui.input_float("Pad Bottom", pad_bottom, 1.0, 10.0, "%.1f") + if changed_l or changed_r or changed_t or changed_b: + comp_data['layout_padding_left'] = new_l + comp_data['layout_padding_right'] = new_r + comp_data['layout_padding_top'] = new_t + comp_data['layout_padding_bottom'] = new_b + if hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) + + align_options = ["start", "center", "end", "stretch"] + + if comp_type in ['HorizontalLayout', 'VerticalLayout']: + wrap_enabled = bool(comp_data.get('layout_wrap', True)) + changed, new_wrap = imgui.checkbox("Wrap", wrap_enabled) + if changed: + comp_data['layout_wrap'] = new_wrap + if hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) + + line_spacing = float(comp_data.get('layout_line_spacing', 0.0)) + changed, new_line_spacing = imgui.input_float("Line Spacing (Wrap)", line_spacing, 1.0, 10.0, "%.1f") + if changed: + comp_data['layout_line_spacing'] = new_line_spacing + if hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) + + current_align = comp_data.get('layout_align', 'start') + if imgui.begin_combo("Align", current_align): + for opt in align_options: + if imgui.selectable(opt, current_align == opt)[0]: + comp_data['layout_align'] = opt + if hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) + imgui.end_combo() + + # --- 层级与显示顺序 (LUI 中组件层级由 Z-Offset 控制) --- + imgui.spacing() + imgui.text("层级与显示顺序") + + # 获取当前深度并确保其在 comp_data 中 + current_z = comp_data.get('z_offset') + if current_z is None: + # 兼容旧组件的 sort 属性,如果存在则作为初始 z_offset + current_z = comp_data.get('sort', 0.0) + if current_z == 0 and hasattr(comp_obj, 'z_offset'): + current_z = comp_obj.z_offset + comp_data['z_offset'] = float(current_z) + + # 1. 整数层级设置 (方便快速调整) + layer_val = int(current_z) + changed_layer, new_layer = imgui.input_int("渲染层级 (Layer)", layer_val) + if changed_layer: + comp_data['z_offset'] = float(new_layer) + if hasattr(comp_obj, 'set_z_offset'): + comp_obj.set_z_offset(float(new_layer)) + print(f"✓ 组件 #{index} 层级已映射到 Z-Offset: {new_layer}") + + # 2. 深度微调 (Z-Offset 浮点数) + changed_z, new_z = imgui.input_float("深度微调 (Z-Offset)", comp_data['z_offset'], 0.1, 1.0, "%.2f") + if changed_z: + comp_data['z_offset'] = new_z + if hasattr(comp_obj, 'set_z_offset'): + comp_obj.set_z_offset(new_z) + elif hasattr(comp_obj, 'z_offset'): + comp_obj.z_offset = new_z + print(f"✓ 组件 #{index} Z-Offset 已微调为: {new_z}") + + imgui.text_disabled("(注: LUI 内部组件通过 Z-Offset 决定遮挡关系)") + + # 特定类型的属性 + if comp_type == 'InputField': + imgui.spacing() + imgui.text("输入框属性") + + imgui.text("输入框图片") + if imgui.button("更改图片##input_img", (120, 20)): + selected_path = manager._change_image_texture( + title = "选择图片文件", + filetypes = [ + ("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), + ("PNG", "*.png"), + ("JPEG", "*.jpg;*.jpeg"), + ("所有文件", "*.*") + ] + ) + if selected_path: + try: + atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) + if atlas_result: + if len(atlas_result) >= 7: + tex, u0, v0, u1, v1, w, h = atlas_result[:7] + else: + tex, u0, v0, u1, v1 = atlas_result + w = int(max(1, round((u1 - u0) * tex.getXSize()))) + h = int(max(1, round((v1 - v0) * tex.getYSize()))) + comp_data['input_texture_path'] = selected_path + comp_data['input_atlas_uv'] = (u0, v0, u1, v1) + comp_data['input_texture_ref'] = tex + comp_data['input_image_size'] = (int(w), int(h)) + layout = getattr(comp_obj, '_layout', None) + if layout is not None: + for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): + spr = getattr(layout, attr, None) + if spr is not None and hasattr(spr, 'set_texture'): + spr.set_texture(tex, resize=False) + if hasattr(spr, 'set_uv_range'): + spr.set_uv_range(u0, v0, u1, v1) + except Exception as e: + print(f"InputField texture set failed: {e}") + + if imgui.button("使用图片尺寸##input_fit", (120, 20)): + w = h = None + if 'input_image_size' in comp_data and comp_data['input_image_size']: + try: + w, h = comp_data['input_image_size'] + except Exception: + w = h = None + elif 'input_atlas_uv' in comp_data and 'input_texture_ref' in comp_data and comp_data['input_texture_ref']: + try: + u0, v0, u1, v1 = comp_data['input_atlas_uv'] + tex = comp_data['input_texture_ref'] + tw = tex.getXSize() if hasattr(tex, 'getXSize') else 0 + th = tex.getYSize() if hasattr(tex, 'getYSize') else 0 + w = int(max(1, round((u1 - u0) * tw))) + h = int(max(1, round((v1 - v0) * th))) + except Exception: + w = h = None + if w is not None and h is not None and w > 0 and h > 0: + comp_data['width'] = float(w) + comp_data['height'] = float(h) + comp_obj.width = float(w) + comp_obj.height = float(h) + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(float(w)) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(float(h)) + layout = getattr(comp_obj, '_layout', None) + if layout is not None: + if hasattr(layout, 'width'): + layout.width = "100%" + if hasattr(layout, 'height'): + layout.height = "100%" + + imgui.text("输入框颜色") + in_color = comp_data.get('input_color', (1.0, 1.0, 1.0, 1.0)) + in_color = list(in_color) if isinstance(in_color, tuple) else in_color + changed, new_in_color = imgui.color_edit4("##input_color", in_color) + if changed: + comp_data['input_color'] = tuple(new_in_color) + layout = getattr(comp_obj, '_layout', None) + if layout is not None: + for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): + spr = getattr(layout, attr, None) + if spr is not None: + spr.color = tuple(new_in_color) + + if 'value' in comp_data: + changed, new_value = imgui.input_text("当前值", comp_data['value'], 256) + if changed: + comp_data['value'] = new_value + comp_obj.value = new_value + + elif comp_type == 'Slider': + imgui.spacing() + imgui.text("滑块属性") + if 'value' in comp_data: + min_val = comp_data.get('min_value', 0.0) + max_val = comp_data.get('max_value', 100.0) + changed, new_value = imgui.slider_float("当前值", comp_data['value'], min_val, max_val, "%.1f") + if changed: + comp_data['value'] = new_value + if hasattr(comp_obj, 'set_value'): + comp_obj.set_value(new_value) + + elif comp_type == 'Checkbox': + imgui.spacing() + imgui.text("复选框属性") + if 'text' in comp_data: + changed, new_text = imgui.input_text("标签文本", comp_data['text'], 256) + if changed: + comp_data['text'] = new_text + # Checkbox uses a child label for text + if hasattr(comp_obj, 'label') and hasattr(comp_obj.label, 'text'): + comp_obj.label.text = new_text + elif hasattr(comp_obj, 'text'): + comp_obj.text = new_text + + if 'checked' in comp_data: + changed, new_checked = imgui.checkbox("选中状态", comp_data['checked']) + if changed: + comp_data['checked'] = new_checked + if hasattr(comp_obj, 'checked'): + comp_obj.checked = new_checked + + elif comp_type in ['Plane', 'Image']: + imgui.spacing() + imgui.text(f"{comp_type}属性") + + # 颜色属性 + if 'color' in comp_data: + color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0)) + color = list(color) if isinstance(color, tuple) else color + changed, new_color = imgui.color_edit4("颜色", color) + if changed: + comp_data['color'] = tuple(new_color) + # 更新sprite颜色 + if 'sprite' in comp_data: + comp_data['sprite'].color = tuple(new_color) + + # 如果是Image,显示纹理路径 + if comp_type == 'Image' and 'texture_path' in comp_data: + # Fit size to image texture + if imgui.button("使用图片尺寸##fit_image"): + tex = None + # Prefer direct texture reference if available + if 'texture_ref' in comp_data and comp_data['texture_ref']: + tex = comp_data['texture_ref'] + elif 'texture' in comp_data and comp_data['texture']: + tex = comp_data['texture'] + else: + # Try to read from sprite python tag if present + try: + target = comp_data.get('sprite', comp_obj) + if target is not None and hasattr(target, 'get_python_tag'): + tex = target.get_python_tag('texture_ref') + except Exception: + tex = None + + w = h = None + if 'image_size' in comp_data and comp_data['image_size']: + try: + w, h = comp_data['image_size'] + except Exception: + w = h = None + elif 'atlas_uv' in comp_data and 'atlas_tex' in comp_data and comp_data['atlas_tex']: + try: + u0, v0, u1, v1 = comp_data['atlas_uv'] + atlas_tex = comp_data['atlas_tex'] + atlas_w = atlas_tex.getXSize() if hasattr(atlas_tex, 'getXSize') else 0 + atlas_h = atlas_tex.getYSize() if hasattr(atlas_tex, 'getYSize') else 0 + w = int(max(1, round((u1 - u0) * atlas_w))) + h = int(max(1, round((v1 - v0) * atlas_h))) + except Exception: + w = h = None + elif tex is not None: + if hasattr(tex, 'getOrigFileXSize') and hasattr(tex, 'getOrigFileYSize'): + ow = tex.getOrigFileXSize() + oh = tex.getOrigFileYSize() + if ow and oh: + w, h = ow, oh + if w is None or h is None: + if hasattr(tex, 'getXSize') and hasattr(tex, 'getYSize'): + w = tex.getXSize() + h = tex.getYSize() + + if w is not None and h is not None and w > 0 and h > 0: + comp_data['width'] = float(w) + comp_data['height'] = float(h) + comp_obj.width = float(w) + comp_obj.height = float(h) + # Sync sprite size (Image/Plane/Video) + if 'sprite' in comp_data and comp_data['sprite']: + spr = comp_data['sprite'] + if hasattr(spr, 'set_size'): + spr.set_size(float(w), float(h)) + else: + spr.width = float(w) + spr.height = float(h) + if hasattr(manager, '_hide_resize_handles'): + manager._hide_resize_handles() + else: + print("Image size not available: missing texture") + + imgui.text(f"纹理路径: {comp_data['texture_path']}") + if imgui.button("更改纹理", (100, 20)): + # 实现纹理更改功能 + selected_path = manager._change_image_texture( + title = "选择图片文件", + filetypes = [ + ("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), + ("PNG", "*.png"), + ("JPEG", "*.jpg;*.jpeg"), + ("所有文件", "*.*") + ] + ) + if selected_path: + new_path = selected_path + comp_data['texture_path'] = new_path + try: + atlas_result = manager.luiFunction._add_image_to_atlas(manager, new_path, atlas_size=2048) + if atlas_result: + if len(atlas_result) >= 7: + tex, u0, v0, u1, v1, w, h = atlas_result[:7] + else: + tex, u0, v0, u1, v1 = atlas_result + w = int(max(1, round((u1 - u0) * tex.getXSize()))) + h = int(max(1, round((v1 - v0) * tex.getYSize()))) + print(f"? Texture loaded: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}") + comp_data["current_texture"] = os.path.basename(new_path) + comp_data["atlas_uv"] = (u0, v0, u1, v1) + comp_data["atlas_tex"] = tex + comp_data["image_size"] = (int(w), int(h)) + old_spr = comp_data.get("sprite") + if old_spr: + try: + old_spr.parent = None + if hasattr(old_spr, "set_texture"): old_spr.set_texture(None) + if hasattr(old_spr, "destroy"): old_spr.destroy() + except: + pass + try: + new_spr = LUISprite(comp_obj, tex) + if hasattr(new_spr, "set_texture"): + new_spr.set_texture(tex, resize=False) + if hasattr(new_spr, "set_uv_range"): + new_spr.set_uv_range(u0, v0, u1, v1) + new_spr.width = comp_data.get("width", 100) + new_spr.height = comp_data.get("height", 100) + new_spr.left = 0 + new_spr.top = 0 + new_spr.z_offset = comp_data.get("z_offset", 0) + new_spr.color = comp_data.get("color", (1,1,1,1)) + if hasattr(new_spr, "set_uv_range"): + new_spr.set_uv_range(u0, v0, u1, v1) + comp_data["sprite"] = new_spr + comp_data["texture_ref"] = tex + if not hasattr(manager, "texture_refs"): + manager.texture_refs = [] + manager.texture_refs.append(tex) + if hasattr(new_spr, "set_python_tag"): + new_spr.set_python_tag("texture_ref", tex) + if hasattr(comp_obj, "set_python_tag"): + comp_obj.set_python_tag("texture_ref", tex) + print(f"? Replaced sprite with atlas texture image: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}") + except Exception as ex: + print(f"? Failed to create new sprite: {ex}") + target = comp_data.get("widget", comp_data.get("object")) + if hasattr(target, "set_texture"): + target.set_texture(tex) + else: + print(f"? Texture loading returned None for: {new_path}") + except Exception as e: + print(f"Image texture update failed: {e}") + if 'sprite' in comp_data: + sprite = comp_data['sprite'] + imgui.text(f"当前纹理: {comp_data.get('current_texture', 'blank')}") + + elif comp_type == 'Frame': + imgui.spacing() + imgui.text("框架属性") + + # 框架特有的颜色属性 + if 'color' in comp_data: + color = comp_data.get('color', (0.7, 0.7, 0.7, 0.8)) + color = list(color) if isinstance(color, tuple) else color + changed, new_color = imgui.color_edit4("背景颜色", color) + if changed: + comp_data['color'] = tuple(new_color) + # 更新Frame颜色 + if hasattr(comp_obj, 'set_color'): + comp_obj.set_color(tuple(new_color)) + + elif comp_type == 'Selectbox': + imgui.spacing() + imgui.text("Selectbox Options") + imgui.separator() + + options = comp_data.get('options', []) or [] + # normalize to list of (id,label) + normalized = [] + for i, opt in enumerate(options): + try: + opt_id, opt_label = opt + except Exception: + opt_id, opt_label = i, str(opt) + normalized.append((opt_id, str(opt_label))) + options = normalized + comp_data['options'] = options + + current_selected = comp_data.get('selected_option_id') + if hasattr(comp_obj, 'get_selected_option'): + try: + current_selected = comp_obj.get_selected_option() + except Exception: + pass + + opt_labels = [label for _, label in options] if options else ["(empty)"] + opt_ids = [oid for oid, _ in options] if options else [None] + try: + current_index = opt_ids.index(current_selected) + except Exception: + current_index = 0 + + changed_sel, new_index = imgui.combo("Selected", current_index, opt_labels) + if changed_sel and options: + sel_id = opt_ids[new_index] + comp_data['selected_option_id'] = sel_id + try: + if hasattr(comp_obj, '_select_option'): + comp_obj._select_option(sel_id) + except Exception: + pass + + imgui.spacing() + imgui.text("Edit Options") + imgui.separator() + + new_labels = [] + dirty = False + for i, (opt_id, opt_label) in enumerate(options): + imgui.push_item_width(-40) + changed, new_label = imgui.input_text(f"##opt_label_{i}", opt_label, 128) + imgui.pop_item_width() + if changed: + opt_label = new_label + dirty = True + imgui.same_line() + if imgui.button(f"Delete##opt_del_{i}", (60, 20)): + dirty = True + continue + new_labels.append(opt_label) + + if imgui.button("Add Option", (100, 24)): + new_labels.append(f"Option {len(new_labels)+1}") + dirty = True + + if dirty: + # rebuild options with sequential ids to avoid duplicates + options = [(i, label) for i, label in enumerate(new_labels)] + comp_data['options'] = options + # keep selection if possible + if options: + if current_selected not in [oid for oid, _ in options]: + current_selected = options[0][0] + comp_data['selected_option_id'] = current_selected + else: + comp_data['selected_option_id'] = None + try: + if hasattr(comp_obj, 'set_options'): + comp_obj.set_options(options) + elif hasattr(comp_obj, 'options'): + comp_obj.options = options + if options and hasattr(comp_obj, '_select_option'): + comp_obj._select_option(comp_data['selected_option_id']) + except Exception as e: + print(f"Selectbox options update failed: {e}") + + elif comp_type == 'Video': + imgui.spacing() + imgui.text("视频属性") + imgui.separator() + # Audio + imgui.spacing() + imgui.text("Audio") + imgui.separator() + current_audio = comp_data.get('audio_path', '') + audio_display = os.path.basename(current_audio) if current_audio else '' + imgui.text(f"Current Audio: {audio_display if audio_display else 'None'}") + if imgui.button("Select Audio", (110, 24)): + selected_audio = manager._change_image_texture( + title = "Select Audio File", + filetypes = [("Audio", "*.mp3;*.wav;*.ogg;*.flac;*.m4a"), ("All Files", "*.*")] + ) + if selected_audio: + try: + audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(selected_audio)) + if audio_sound: + comp_data["audio"] = audio_sound + comp_data["audio_path"] = selected_audio + comp_data["audio_from_video"] = False + if hasattr(audio_sound, "setLoop"): + audio_sound.setLoop(comp_data.get("loop", True)) + if hasattr(audio_sound, "setVolume"): + audio_sound.setVolume(comp_data.get("volume", 1.0)) + if comp_data.get("is_playing") and hasattr(audio_sound, "play"): + audio_sound.play() + except Exception as e: + print(f"Audio load failed: {e}") + # Volume + vol = comp_data.get("volume", 1.0) + changed, new_vol = imgui.slider_float("Volume", vol, 0.0, 1.0, "%.2f") + if changed: + comp_data["volume"] = new_vol + audio_sound = comp_data.get("audio") + if audio_sound and hasattr(audio_sound, "setVolume"): + audio_sound.setVolume(new_vol) + + + # 视频源显示与更改 (支持本地文件和 URL 流) + if 'video_path' in comp_data: + current_source = comp_data.get('video_path', '') + is_url = '://' in current_source + + # 顶部状态显示 + source_display = os.path.basename(current_source) if (current_source and not is_url) else current_source + imgui.text(f"当前源: {source_display if source_display else '未设置'}") + + # -- 方式 1: 本地文件 -- + if imgui.button("选择本地视频", (110, 24)): + selected_path = manager._change_image_texture( + title = "选择视频文件", + filetypes = [ + ("视频文件", "*.mp4;*.avi;*.mov;*.mkv;*.webm;*.ogv"), + ("所有文件", "*.*") + ] + ) + if selected_path: + comp_data['_pending_video_source'] = selected_path + + # imgui.spacing() + # imgui.text("视频URL") + # imgui.separator() + + # # -- 方式 2: URL / Stream -- + # # 使用缓存的临时输入,避免每帧刷新导致光标丢失 + # url_input_key = f"video_url_input_{index}" + # if url_input_key not in comp_data: + # comp_data[url_input_key] = current_source if is_url else "" + + # changed, new_url = imgui.input_text("##vurl", comp_data[url_input_key], 1024) + # if changed: + # comp_data[url_input_key] = new_url + + # imgui.same_line() + # if imgui.button("加载 URL", (80, 24)): + # comp_data['_pending_video_source'] = comp_data[url_input_key] + + # 统一执行加载逻辑 + if '_pending_video_source' in comp_data: + selected_path = comp_data.pop('_pending_video_source') + if selected_path: + comp_data['video_path'] = selected_path + # 重置时长以强制新视频重新探测 + comp_data['duration'] = 0.0 + print(f"✓ 尝试加载视频源: {selected_path}") + + try: + # 重新加载视频纹理 + from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData + + # Trim trailing spaces which are visible in error logs + selected_path = selected_path.strip() + + # Whitelist protocols for FFmpeg + loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file") + loadPrcFileData("", "ffmpeg-show-error #t") + + if "://" in selected_path: + print(f"🔍 尝试加载 URL: [{selected_path}]") + try: + # Use MovieVideo for better protocol handling + video_src = MovieVideo.get(Filename(selected_path)) + video_texture = MovieTexture("RemoteVideo") + if not video_texture.load(video_src): + print("⚠ MovieVideo 加载失败,回退到 Texture.read...") + if not video_texture.read(Filename(selected_path)): + raise Exception("MovieTexture 无法读取 URL") + except Exception as e: + print(f"⚠ 显式流加载失败: {e},尝试使用 Loader...") + video_texture = manager.world.loader.loadTexture(selected_path) + else: + video_texture = manager.world.loader.loadTexture(Filename.from_os_specific(selected_path)) + if video_texture: + # 更新AspectRatio + orig_w = video_texture.getOrigFileXSize() + orig_h = video_texture.getOrigFileYSize() + if orig_w > 0 and orig_h > 0: + ratio = orig_w / orig_h + ratio = orig_w / orig_h + width = comp_data.get('width', 320) + new_height = width / ratio + + print(f"✓Loaded Video Texture: {orig_w}x{orig_h}") + + # Debug: check threading support + from panda3d.core import ConfigVariableBool + if not ConfigVariableBool("support-threads").getValue(): + print("⚠ 警告: support-threads 为 False, 视频播放可能失败!") + + if 'sprite' in comp_data: + spr = comp_data['sprite'] + # Ensure full UV coordinate usage + if hasattr(spr, 'set_uv_range'): + spr.set_uv_range(0, 0, 1, 1) + + # 更新组件高度 + comp_data['height'] = new_height + comp_obj.height = new_height + if 'sprite' in comp_data: + comp_data['sprite'].height = new_height + + # Re-create sprite to ensure no atlas conflict and clean up old one + if 'sprite' in comp_data: + # Detach old sprite to prevent "ghost" rectangles + old_spr = comp_data['sprite'] + if old_spr: + try: + # Try standard LUI detachment + old_spr.parent = None + if hasattr(old_spr, 'hide'): + old_spr.hide() + except Exception as ex: + print(f"⚠ Failed to detach old sprite: {ex}") + + # Create new sprite with direct texture + # Note: LUISprite(parent, texture) is valid if texture is a Texture object + new_spr = LUISprite(comp_obj, video_texture) + # Force set texture again with resize=False to ensure binding + if hasattr(new_spr, 'set_texture'): + new_spr.set_texture(video_texture, resize=False) + + new_spr.width = width + new_spr.height = new_height + # Video needs white color to modulate correctly with texture + new_spr.color = (1, 1, 1, 1) + new_spr.z_offset = 0 + + # Ensure UVs are full frame + if hasattr(new_spr, 'set_uv_range'): + new_spr.set_uv_range(0, 0, 1, 1) + + comp_data['sprite'] = new_spr + + # WORKAROUND: Force texture update by adding a dummy node to scene graph + # LUI alone might not trigger MovieTexture updates in some pipeline configs + if 'keep_alive' in comp_data and comp_data['keep_alive']: + try: + comp_data['keep_alive'].destroy() + except: + pass + + try: + from direct.gui.OnscreenImage import OnscreenImage + from panda3d.core import TransparencyAttrib + # Create a nearly invisible card (alpha=0.01) to force update + # We position it at (0,0,0) with a small but visible scale to prevent culling + dummy = OnscreenImage(image=video_texture, parent=manager.world.render2d) + dummy.setScale(0.1) + dummy.setPos(0, 0, 0) + dummy.setTransparency(TransparencyAttrib.MAlpha) + dummy.setColorScale(1, 1, 1, 0.01) # Nearly invisible + # Ensure it's behind other UI if possible, though in render2d everything is overlay + dummy.setBin("background", -10) + + comp_data['keep_alive'] = dummy + print("✓ Created keep-alive node for video") + except Exception as e: + print(f"⚠ Keep-alive creation failed: {e}") + + # 更新引用 + comp_data['texture'] = video_texture + comp_obj.video_texture = video_texture # Prevent GC + if comp_data.get("audio_from_video"): + old_audio = comp_data.get("audio") + if old_audio and hasattr(old_audio, "stop"): + old_audio.stop() + try: + audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(selected_path)) + if audio_sound: + comp_data["audio"] = audio_sound + comp_data["audio_path"] = selected_path + if hasattr(audio_sound, "setLoop"): + audio_sound.setLoop(comp_data.get("loop", True)) + if hasattr(audio_sound, "setVolume"): + audio_sound.setVolume(comp_data.get("volume", 1.0)) + if comp_data.get("is_playing") and hasattr(audio_sound, "play"): + audio_sound.play() + except Exception as e: + print(f"Audio load failed: {e}") + + if hasattr(video_texture, 'play'): + if hasattr(video_texture, 'stop'): + video_texture.stop() # Reset + video_texture.play() + if hasattr(video_texture, 'setLoop'): + video_texture.setLoop(comp_data.get("loop", True)) + + comp_data['is_playing'] = True + + except Exception as e: + print(f"⚠ 加载视频失败: {e}") + + # 播放控制 + imgui.spacing() + + + imgui.text("播放控制") + + video_tex = comp_data.get('texture') + audio_sound = comp_data.get('audio') + if video_tex: + # Play/Pause Button + is_playing = comp_data.get('is_playing', False) + if imgui.button("暂停" if is_playing else "播放", (60, 24)): + if is_playing: + if hasattr(video_tex, 'stop'): video_tex.stop() + if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop() + comp_data['is_playing'] = False + else: + if hasattr(video_tex, 'play'): video_tex.play() + if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play() + comp_data['is_playing'] = True + + imgui.same_line() + if imgui.button("重播", (60, 24)): + if hasattr(video_tex, 'stop'): video_tex.stop() + if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop() + if hasattr(video_tex, 'play'): video_tex.play() + if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play() + comp_data['is_playing'] = True + + # Loop Checkbox + loop = comp_data.get('loop', True) + changed, new_loop = imgui.checkbox("循环播放", loop) + if changed: + comp_data['loop'] = new_loop + if hasattr(video_tex, 'setLoop'): + video_tex.setLoop(new_loop) + if audio_sound and hasattr(audio_sound, 'setLoop'): audio_sound.setLoop(new_loop) + + # Time Display + if hasattr(video_tex, 'getTime'): + t = video_tex.getTime() + + # 检查是否需要循环播放 + duration = comp_data.get('duration', 0.0) + if duration > 0 and t >= duration: + # 播放时间超过总时长,重置到开头 + if hasattr(video_tex, 'setTime'): + video_tex.setTime(0.0) + t = 0.0 + print(f"✓ 视频播放完毕,重新开始循环播放") + + imgui.text(f"当前时间: {t:.2f}s") + + # Force refresh if stuck (hack) + # if t == 0.0 and comp_data.get('is_playing'): + # video_tex.play() + + # imgui.separator() + # imgui.text("调试工具") + # if imgui.button("在Render2D中测试播放"): + # # Create a standard OnscreenImage to test playback capability + # from direct.gui.OnscreenImage import OnscreenImage + # from panda3d.core import TransparencyAttrib + + # # Remove previous test if any + # if hasattr(manager, '_debug_video_node') and manager._debug_video_node: + # manager._debug_video_node.destroy() + + # manager._debug_video_node = OnscreenImage(image=video_tex, pos=(0, 0, 0), scale=0.5, parent=manager.world.render2d) + # manager._debug_video_node.setTransparency(TransparencyAttrib.MAlpha) + # print(f"Debug: Created OnscreenImage with texture {video_tex}") + + # if imgui.button("清除Render2D测试"): + # if hasattr(manager, '_debug_video_node') and manager._debug_video_node: + # manager._debug_video_node.destroy() + # manager._debug_video_node = None + # print("Debug: Cleared OnscreenImage") + + # Time Display and Control + if hasattr(video_tex, 'getTime'): + current_time = video_tex.getTime() + + if 'duration' not in comp_data or comp_data['duration'] <= 0.0: + # Attempt to detect duration automatically + detected_duration = 0.0 + # Method 1: standard MovieTexture interface (if available) + if hasattr(video_tex, 'getVideoLength'): + detected_duration = video_tex.getVideoLength() + # Method 2: check if it has a 'length' property + elif hasattr(video_tex, 'length'): + detected_duration = video_tex.length + + if detected_duration > 0: + comp_data['duration'] = detected_duration + print(f"✓ Automatically detected video duration: {detected_duration}s") + + # Manual Duration Input (Essential if auto-detection fails) + # We only show this if we really don't know the duration + current_dur = comp_data.get('duration', 0.0) + if current_dur <= 0.1: + imgui.text_colored((1, 1, 0, 1), "⚠未知时长,请手动设置:") + changed, new_dur = imgui.input_float("总时长(s)", current_dur, 1.0, 10.0, "%.1f") + if changed: + comp_data['duration'] = new_dur + + duration = comp_data.get('duration', 0.0) + + # 检查是否需要循环播放 + if duration > 0 and current_time >= duration: + # 播放时间超过总时长,重置到开头 + if hasattr(video_tex, 'setTime'): + video_tex.setTime(0.0) + current_time = 0.0 + print(f"✓ 视频播放完毕,重新开始循环播放") + + # If we still don't have a valid duration, use a fallback for display but warn user + # REVERT: Do not auto-extend duration, it causes confusion. Trust detection or user. + + display_max = duration if duration > 0 else max(current_time + 10.0, 60.0) + + # Format function for time + def format_time(seconds): + m = int(seconds // 60) + s = int(seconds % 60) + return f"{m:02d}:{s:02d}" + + # Progress Slider + imgui.text(f"进度: {format_time(current_time)} / {format_time(display_max)}") + imgui.same_line() + + # Use push_item_width to make it fit + imgui.push_item_width(-1) + # Use a custom format for the slider to show seconds, but maybe we can show MM:SS in the text + changed, seek_time = imgui.slider_float("##seek_slider", current_time, 0.0, display_max, "%.2fs") + imgui.pop_item_width() + + if changed: + if hasattr(video_tex, 'setTime'): + video_tex.setTime(seek_time) + if audio_sound and hasattr(audio_sound, 'setTime'): audio_sound.setTime(seek_time) + # If paused, we might need to show the frame. + # Usually setTime works. + print(f"Seek to {seek_time}") + + # Update cached duration if we find a way, or if user inputs it? + # For now just show time + # imgui.text(f"时间: {current_time:.2f}s") # Already shown in slider + else: + imgui.text_colored((1, 0, 0, 1), "无有效视频纹理") + + # 锚点设置 + imgui.spacing() + imgui.separator() + imgui.text("锚点设置") + + parent_index = comp_data.get('parent_index') + has_parent = (parent_index is not None and parent_index >= 0 and parent_index < len(manager.components)) + + # 允许对父组件或Canvas进行锚点设置 + if has_parent or manager.current_canvas_index >= 0: + is_anchored = comp_data.get('anchored_to_parent', False) + anchor_pos = comp_data.get('anchor_position', '未设置') + + if has_parent: + parent_comp = manager.components[parent_index] + imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"父组件: {parent_comp['type']}") + else: + imgui.text_colored((0.0, 1.0, 0.0, 1.0), "父容器: Canvas") + + if is_anchored: + imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"当前锚点: {anchor_pos}") + + # 锚点位置选择器 - 9宫格布局 + imgui.spacing() + imgui.text("锚点位置:") + + # 定义9个锚点位置 + anchor_positions = [ + ['top-left', 'top-center', 'top-right'], + ['middle-left', 'center', 'middle-right'], + ['bottom-left', 'bottom-center', 'bottom-right'] + ] + + # 中文显示名称 + anchor_names = { + 'top-left': '左上', 'top-center': '中上', 'top-right': '右上', + 'middle-left': '左中', 'center': '中心', 'middle-right': '右中', + 'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下' + } + + # 绘制3x3网格按钮 + for row_idx, row in enumerate(anchor_positions): + for col_idx, pos in enumerate(row): + # 检查是否是当前选中的锚点 + is_current = (anchor_pos == pos) + + # 如果是当前锚点,使用不同颜色 + if is_current: + imgui.push_style_color(imgui.Col_.button, (0.2, 0.7, 0.2, 1.0)) + imgui.push_style_color(imgui.Col_.button_hovered, (0.3, 0.8, 0.3, 1.0)) + imgui.push_style_color(imgui.Col_.button_active, (0.1, 0.6, 0.1, 1.0)) + + # 绘制按钮 + button_size = (50, 25) + if imgui.button(f"{anchor_names[pos]}##anchor_{pos}", button_size): + # 更新锚点位置 + manager._update_component_anchor_position(index, pos) + + if is_current: + imgui.pop_style_color(3) + + # 同一行的按钮在同一行显示 + if col_idx < len(row) - 1: + imgui.same_line() + + imgui.spacing() + + # 锚点偏移微调 + imgui.text("位置微调:") + anchor_offset_x = comp_data.get('anchor_manual_offset_x', 0.0) + anchor_offset_y = comp_data.get('anchor_manual_offset_y', 0.0) + + changed_x, new_offset_x = imgui.input_float("X偏移", anchor_offset_x, 1.0, 10.0, "%.1f") + if changed_x: + comp_data['anchor_manual_offset_x'] = new_offset_x + manager._update_component_anchor_position(index, anchor_pos, manual_offset=(new_offset_x, anchor_offset_y)) + + changed_y, new_offset_y = imgui.input_float("Y偏移", anchor_offset_y, 1.0, 10.0, "%.1f") + if changed_y: + comp_data['anchor_manual_offset_y'] = new_offset_y + manager._update_component_anchor_position(index, anchor_pos, manual_offset=(anchor_offset_x, new_offset_y)) + + imgui.spacing() + + # 取消锚点按钮 + if imgui.button("取消锚点", (100, 20)): + comp_data['anchored_to_parent'] = False + comp_data['anchor_manual_offset_x'] = 0.0 + comp_data['anchor_manual_offset_y'] = 0.0 + print("✓ 已取消组件的锚点关系") + else: + imgui.text_colored((1.0, 0.0, 0.0, 1.0), "未使用锚点定位") + if imgui.button("设置锚点", (100, 20)): + imgui.open_popup("选择锚点位置") + manager._temp_selected_index_for_anchor = index + else: + imgui.text_disabled("无法设置锚点 (无父组件且无有效Canvas)") + + # Hierarchy & Parent Management + imgui.spacing() + imgui.text("层级与父级管理") + imgui.separator() + + current_parent_index = comp_data.get('parent_index') + parent_text = "ROOT (无父组件)" if current_parent_index is None or current_parent_index < 0 else f"{manager.components[current_parent_index]['type']} #{current_parent_index}" + imgui.text(f"当前父级: {parent_text}") + + # Create list of potential parents (exclude self and any children to avoid circular loops) + def get_all_descendants(idx): + desc = set() + comp = manager.components[idx] + for c_idx in comp.get('children_indices', []): + desc.add(c_idx) + desc.update(get_all_descendants(c_idx)) + return desc + + descendants = get_all_descendants(index) + parent_options = ["(无 / ROOT)"] + parent_indices = [-1] + + for i, comp in enumerate(manager.components): + if i != index and i not in descendants: + parent_options.append(f"{comp['type']} #{i}: {comp.get('name', '')}") + parent_indices.append(i) + + # Current selection in combo + try: + current_p_select = parent_indices.index(current_parent_index if current_parent_index is not None else -1) + except: + current_p_select = 0 + + changed, new_p_select = imgui.combo("更改父级", current_p_select, parent_options) + if changed: + new_parent_idx = parent_indices[new_p_select] + if new_parent_idx == -1: + # Unparent to Root + if current_parent_index is not None and current_parent_index >= 0: + # Remove from old parent + old_p = manager.components[current_parent_index] + if index in old_p.get('children_indices', []): + old_p['children_indices'].remove(index) + + comp_data['parent_index'] = -1 + # Physical reparent to Canvas or Root + if manager.current_canvas_index >= 0: + canvas_panel = manager.canvases[manager.current_canvas_index]['panel'] + comp_obj.reparent_to(canvas_panel) + else: + # Fallback to overlay root + comp_obj.reparent_to(manager.overlay_root) + print(f"✓ 已将组件 #{index} 设为根节点") + else: + # Set New Parent + manager._set_parent_child_relationship(index, new_parent_idx, keep_world=True) + print(f"✓ 已更改父级: 组件 #{index} -> 父级 #{new_parent_idx}") + + # 删除按钮 + imgui.spacing() + imgui.separator() + imgui.push_style_color(imgui.Col_.button, (0.8, 0.2, 0.2, 1.0)) + imgui.push_style_color(imgui.Col_.button_hovered, (0.9, 0.3, 0.3, 1.0)) + imgui.push_style_color(imgui.Col_.button_active, (0.7, 0.1, 0.1, 1.0)) + if imgui.button("删除组件", (-1, 30)): + manager.luiFunction.delete_component(manager,index) + imgui.pop_style_color(3) + + # Draw the anchor popup if requested + manager._handle_anchor_popup() diff --git a/ui/lui_manager.py b/ui/lui_manager.py new file mode 100644 index 00000000..51442b09 --- /dev/null +++ b/ui/lui_manager.py @@ -0,0 +1,3374 @@ +import os +import sys +import struct +from pathlib import Path +import panda3d.core as p3d +from panda3d.core import NodePath, CardMaker +from ui.lui_function import luiFunction + +# Ensure ui directory is in path and lui is correctly patched +UI_DIR = Path(__file__).resolve().parent +if str(UI_DIR) not in sys.path: + sys.path.insert(0, str(UI_DIR)) + +# Add Builtin to path +BUILTIN_DIR = UI_DIR / "Builtin" +if str(BUILTIN_DIR) not in sys.path: + sys.path.insert(0, str(BUILTIN_DIR)) + +# Handle DLL loading for lui.pyd +import panda3d +panda_dir = os.path.dirname(panda3d.__file__) +if hasattr(os, "add_dll_directory"): + try: + os.add_dll_directory(panda_dir) + os.add_dll_directory(str(UI_DIR)) + except Exception as e: + print(f"Warning: Failed to add DLL directory: {e}") + +try: + import lui + # Monkey patch for Builtin components + import panda3d + panda3d.lui = lui + sys.modules["panda3d.lui"] = lui + + from Builtin.LUIRegion import LUIRegion + from Builtin.LUIInputHandler import LUIInputHandler + from Builtin.LUIButton import LUIButton + from Builtin.LUILabel import LUILabel + from Builtin.LUIInputField import LUIInputField + from Builtin.LUISlider import LUISlider + from Builtin.LUIFrame import LUIFrame + from Builtin.LUISkin import LUIDefaultSkin + from Builtin.LUISprite import LUISprite + from Builtin.LUIObject import LUIObject +except ImportError as e: + print(f"Error: Failed to import LUI: {e}") + lui = None + +from imgui_bundle import imgui, imgui_ctx + +class LUIManager: + """Manager for LUI system integration""" + def __init__(self, world): + self.world = world + self.lui_enabled = (lui is not None) + + self.luiFunction = luiFunction + + # Editor State + self.show_editor = True + self.play_mode = False + self._selected_type_index = 0 + + if not self.lui_enabled: + print("LUI is not available, LUIManager will be disabled.") + return + + # 1. 优先注册中文字体 (必须在皮肤加载前) + self._register_chinese_font() + + # 2. 初始化核心 LUI 区域 + self._init_ui_regions() + + # 3. 加载皮肤资源 + self.lui_skin = LUIDefaultSkin() + self.lui_skin.load() + + # 4. 状态管理 - 与engine保持一致 + self.canvases = [] + self.current_canvas_index = -1 + self.canvas_counter = 0 + self.components = [] + self.root_order = [] + self._tree_drag_src = None + self.selected_index = -1 + self._last_selected_index = -1 + + # 拖拽和选择状态 + self.dragging_comp = None + self.pending_drag_comp = None + self.pending_drag_index = -1 + self.pending_drag_start_mouse = None + self.pending_drag_start_abs = None + self.pending_drag_start_parent = None + self.dragging_index = -1 + self.drag_start_threshold = 6.0 + self.drag_offset = (0, 0) + + # 锚点功能相关变量 + self.anchor_popup_open = False + self.pending_child_creation = False + self.pending_parent_index = -1 + + # 子父对齐功能 + self.child_parent_alignment_enabled = True + + # Resize handles 相关变量 + self.resize_handles = [] + self.selection_box = [] + self.debug_outlines = {} # comp_index -> [border sprites] + self.resizing_handle = None + self.resize_start_pos = (0, 0) + self.resize_start_bounds = {} + self.min_size = 20 + + # 使用外部字典管理状态,避免修改 C++ 对象属性导致 AttributeError + self.drag_states = {} + self._last_interaction_frame = -1 # 记录最后一次交互的帧数,用于防止事件穿透 + self._input_enabled = True + + # 创建resize handles和selection box(延迟创建,确保在Canvas之后) + if hasattr(self.world, 'taskMgr'): + self.world.taskMgr.doMethodLater(0.1, self._delayed_create_resize_handles, "delayed_create_resize_handles") + else: + self._create_resize_handles() + + # 添加任务来处理拖动和resize handles更新 + if hasattr(self.world, 'taskMgr'): + self.world.taskMgr.add(self._update_drag, "update_lui_drag") + self.world.taskMgr.add(self._update_resize_handles, "update_resize_handles") + self.world.taskMgr.add(self._update_component_outlines, "update_lui_outlines") + + print("✓ LUIManager initialized with complete functionality") + + def set_input_enabled(self, enabled): + """Enable/disable LUI input handling (used to avoid ImGui click-through).""" + if not self.lui_enabled: + return + + if enabled == self._input_enabled: + return + + self._input_enabled = enabled + + try: + if enabled: + if hasattr(self, "overlay_handler_node") and self.overlay_handler_node: + self.overlay_handler_node.reparent_to(self.world.mouseWatcher) + if hasattr(self, "overlay_region") and self.overlay_region: + self.overlay_region.set_input_handler(self.overlay_handler) + else: + if hasattr(self, "overlay_region") and self.overlay_region: + self.overlay_region.set_input_handler(None) + if hasattr(self, "overlay_handler_node") and self.overlay_handler_node: + self.overlay_handler_node.detach_node() + except Exception as e: + print(f"Warning: Failed to toggle LUI input state: {e}") + + def _register_chinese_font(self): + """注册支持中文的字体""" + from panda3d.lui import LUIFontPool + import os + + # 尝试常见的系统路径,确保使用完整的 OS 路径 + candidate_fonts = [ + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simhei.ttf", + "C:/Windows/Fonts/arial.ttf" + ] + + font_registered = False + for fpath in candidate_fonts: + if os.path.exists(fpath): + try: + # 在 LUI 中,建议直接用 loader.loadFont 载入后获取动态库指针 + # 或者直接传递路径字符串 + font = self.world.loader.loadFont(fpath) + if font: + LUIFontPool.get_global_ptr().register_font("default", font) + LUIFontPool.get_global_ptr().register_font("label", font) + print(f"✓ LUI 成功注册字体: {fpath}") + font_registered = True + break + except Exception as e: + print(f"LUI 字体注册失败 {fpath}: {e}") + + if not font_registered: + # 如果没有系统字体,至少注册一个默认的避免崩溃 + print("⚠️ 警告: 未能找到合适的 LUI 中文字体") + + def _init_ui_regions(self): + """初始化 LUI 区域 (Overlay, Camera, WorldSpace)""" + # 渲染层级说明(参考engine/main.py的设置): + # - 3D场景(render)默认sort=0 + # - LUI组件设置sort=5,在3D场景之上 + # - pixel2d (ImGui) 默认sort=20,在LUI之上 + # 这样:3D场景 < LUI组件 < ImGui面板 + + self.overlay_region = LUIRegion.make("OverlayUI", self.world.win) + self.overlay_handler = LUIInputHandler() + self.overlay_handler_node = self.world.mouseWatcher.attach_new_node(self.overlay_handler) + self.overlay_region.set_input_handler(self.overlay_handler) + self.overlay_region.set_sort(5) # 在3D场景之上,在ImGui之下(与engine保持一致) + self.overlay_root = self.overlay_region.root + + # 调试输出:确认层级设置 + actual_sort = self.overlay_region.getSort() + print(f"✓ LUI Overlay Region 层级设置: {actual_sort}") + + # 显示所有DisplayRegion的层级信息 + num_regions = self.world.win.getNumDisplayRegions() + print(f"✓ 窗口总共有 {num_regions} 个DisplayRegion:") + for i in range(num_regions): + dr = self.world.win.getDisplayRegion(i) + sort_value = dr.getSort() + camera = dr.getCamera() + camera_name = camera.getName() if not camera.isEmpty() else "无摄像机" + print(f" Region {i}: sort={sort_value}, camera={camera_name}") + + # 当前操作的根节点 + self.root = self.overlay_root + + def _make_canvas_draggable(self, canvas_panel): + """使Canvas可拖拽""" + def on_canvas_drag_start(event): + # 检查是否有组件或手柄在当前帧被点击 + current_frame = 0 + if hasattr(p3d, 'ClockObject'): + current_frame = p3d.ClockObject.getGlobalClock().getFrameCount() + else: + current_frame = self.world.taskMgr.globalClock.getFrameCount() + + if self._last_interaction_frame == current_frame: + # 当前帧已经处理了组件或手柄的点击,忽略Canvas点击事件 + return + + # 点击空白处:不自动取消选中,避免事件穿透导致闪退 + # self.deselect_all() + + # 如果启用填充模式,则自动禁用,以便用户可以自由拖动 + canvas_index = -1 + for i, c in enumerate(self.canvases): + if c['panel'] == canvas_panel: + canvas_index = i + break + + if canvas_index >= 0: + canvas_data = self.canvases[canvas_index] + + # 如果是固定模式,则禁止拖动初始化 + if canvas_data.get('fixed', False): + return + + if canvas_data.get('fill_mode', False): + canvas_data['fill_mode'] = False + print("✓ 自动禁用填充模式以允许自由拖动") + + # 记录Canvas拖拽状态 + if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): + mouse_x = self.world.mouseWatcherNode.getMouseX() + mouse_y = self.world.mouseWatcherNode.getMouseY() + + win_x = self.world.win.getXSize() + win_y = self.world.win.getYSize() + pixel_x = (mouse_x + 1) * win_x / 2 + pixel_y = (1 - mouse_y) * win_y / 2 + + pos = canvas_panel.get_pos() + self.canvas_drag_offset = (pixel_x - pos.x, pixel_y - pos.y) + self.dragging_canvas = canvas_panel + + canvas_panel.bind("mousedown", on_canvas_drag_start) + + def switch_canvas(self, index): + """切换到指定Canvas并显示/隐藏相应的UI组件""" + if index < 0 or index >= len(self.canvases): + return + + self.current_canvas_index = index + + # 显示选中的Canvas,隐藏其他Canvas + for i, canvas in enumerate(self.canvases): + if i == index: + canvas['panel'].show() + canvas['visible'] = True + else: + canvas['panel'].hide() + canvas['visible'] = False + + # 显示/隐藏属于不同Canvas的组件(包括子组件) + visible_count = 0 + hidden_count = 0 + + for comp in self.components: + comp_canvas_index = comp.get('canvas_index') + comp_obj = comp['object'] + comp_name = comp.get('name', 'Unknown') + + should_be_visible = (comp_canvas_index == index) + + if should_be_visible: + if hasattr(comp_obj, 'show'): + comp_obj.show() + elif hasattr(comp_obj, 'visible'): + comp_obj.visible = True + visible_count += 1 + else: + if hasattr(comp_obj, 'hide'): + comp_obj.hide() + elif hasattr(comp_obj, 'visible'): + comp_obj.visible = False + hidden_count += 1 + + # 更新当前根节点 + if index >= 0: + self.root = self.canvases[index]['panel'] + + # 如果选中的组件不属于当前Canvas,取消选中 + if self.selected_index >= 0 and self.selected_index < len(self.components): + selected_comp = self.components[self.selected_index] + if selected_comp.get('canvas_index') != index: + if getattr(self, '_force_keep_selection_frames', 0) > 0: + pass + else: + self.selected_index = -1 + self._hide_selection() + + print(f"✓ Switched to Canvas: {self.canvases[index]['name']}") + print(f" 显示组件: {visible_count}, 隐藏组件: {hidden_count}") + + def _update_canvas_geometry(self, canvas_data): + """根据填充模式和边距更新Canvas几何信息""" + if not canvas_data.get('fill_mode', False): + return + + panel = canvas_data['panel'] + margins = canvas_data.get('margins', {'left':0,'right':0,'top':0,'bottom':0}) + + win_width = 800 + win_height = 600 + if hasattr(self.world, 'win'): + win_width = self.world.win.getXSize() + win_height = self.world.win.getYSize() + + new_x = margins['left'] + new_y = margins['top'] + new_width = max(10, win_width - margins['left'] - margins['right']) + new_height = max(10, win_height - margins['top'] - margins['bottom']) + + panel.set_pos(new_x, new_y) + panel.width = new_width + panel.height = new_height + + if 'background' in canvas_data: + bg = canvas_data['background'] + bg.width = new_width + bg.height = new_height + + def _setup_component_drag(self, comp_data, comp_index): + """为组件设置拖动功能 - 支持Canvas相对坐标系""" + comp_obj = comp_data['object'] + + def on_mouse_down(event): + comp_type = comp_data.get('type') + if getattr(self, 'play_mode', False): + # Let runtime widgets handle their own events (especially Slider) + if comp_type == 'Slider': + return + try: + if hasattr(comp_obj, 'on_mousedown'): + comp_obj.on_mousedown(event) + except Exception: + pass + return + # Prevent re-entrant drag + if self.dragging_comp is not None: + return + + # Find current index + current_index = -1 + for i, c in enumerate(self.components): + if c is comp_data: + current_index = i + break + + if current_index == -1: + return + + # Mark interaction frame + if hasattr(p3d, 'ClockObject'): + self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount() + else: + self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount() + + # Select component + self.selected_index = current_index + self._last_selected_index = current_index + self._force_keep_selection_frames = 3 + + # Force component to current canvas to avoid immediate deselect + try: + comp_data['canvas_index'] = self.current_canvas_index + except Exception: + pass + + try: + if comp_data.get('canvas_index') is None: + comp_data['canvas_index'] = self.current_canvas_index + except Exception: + pass + try: + self._show_and_update_handles(comp_data) + except Exception: + pass + + # Slider: allow knob/bar drag in edit mode without hijacking + if comp_type == 'Slider': + sender = getattr(event, 'sender', None) + knob = getattr(comp_obj, '_knob', None) + bg = getattr(comp_obj, '_slider_bg', None) + if sender is not None and (sender == knob or sender == bg): + # Let slider handle its own drag logic + try: + if hasattr(comp_obj, '_start_drag'): + comp_obj._start_drag(event) + except Exception: + pass + return + + parent_idx = comp_data.get('parent_index') + if parent_idx is not None and parent_idx >= 0: + parent_type = self.components[parent_idx].get('type') + if parent_type in ['VerticalLayout', 'HorizontalLayout']: + return + + if not comp_data.get('draggable', True): + return + + self.pending_drag_comp = comp_data + self.pending_drag_index = current_index + self.pending_drag_start_abs = self._get_component_accumulated_pos(current_index) + self.pending_drag_start_parent = comp_obj.parent + self._is_drag_reparented = False + + if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): + mouse_x = self.world.mouseWatcherNode.getMouseX() + mouse_y = self.world.mouseWatcherNode.getMouseY() + + win_x = self.world.win.getXSize() + win_y = self.world.win.getYSize() + pixel_x = (mouse_x + 1) * win_x / 2 + pixel_y = (1 - mouse_y) * win_y / 2 + self.pending_drag_start_mouse = (pixel_x, pixel_y) + else: + self.pending_drag_start_mouse = None + + # Bind mouse event + comp_obj.bind("mousedown", on_mouse_down) + + # Slider selection bindings: allow clicking knob/bar to select in edit mode + try: + if comp_data.get('type') == 'Slider': + knob = getattr(comp_obj, '_knob', None) + bg = getattr(comp_obj, '_slider_bg', None) + except Exception: + pass + + def on_mouse_up(event): + if getattr(self, 'play_mode', False): + try: + if hasattr(comp_obj, 'on_mouseup'): + comp_obj.on_mouseup(event) + except Exception: + pass + return + + if comp_data.get('type') == 'Slider': + sender = getattr(event, 'sender', None) + knob = getattr(comp_obj, '_knob', None) + if sender is not None and sender == knob: + try: + if hasattr(comp_obj, '_stop_drag'): + comp_obj._stop_drag(event) + except Exception: + pass + + comp_obj.bind("mouseup", on_mouse_up) + + def _make_draggable(self, lui_obj): + """启用 LUI 对象的鼠标拖拽功能 - 兼容旧接口""" + # 查找对应的组件索引 + comp_index = -1 + for i, comp in enumerate(self.components): + if comp.get('object') == lui_obj: + comp_index = i + break + + if comp_index >= 0: + self._setup_component_drag(self.components[comp_index], comp_index) + + def _get_component_accumulated_pos(self, comp_index): + """递归计算组件相对于Canvas的累积位置""" + if comp_index < 0 or comp_index >= len(self.components): + return 0, 0 + + comp_data = self.components[comp_index] + comp_obj = comp_data['object'] + + # 优先使用递归计算(更可靠) + left = comp_data.get('left', 0) + top = comp_data.get('top', 0) + + parent_index = comp_data.get('parent_index') + if parent_index is not None and parent_index >= 0: + parent_left, parent_top = self._get_component_accumulated_pos(parent_index) + result_x = parent_left + left + result_y = parent_top + top + print(f"[_get_component_accumulated_pos] Component #{comp_index} (has parent #{parent_index}): local=({left:.1f}, {top:.1f}), parent_offset=({parent_left:.1f}, {parent_top:.1f}), result=({result_x:.1f}, {result_y:.1f})") + return result_x, result_y + + # 没有父组件,直接返回局部坐标(相对于Canvas) + print(f"[_get_component_accumulated_pos] Component #{comp_index} (root): local=({left:.1f}, {top:.1f})") + return left, top + + def _update_drag(self, task): + """每帧更新拖动状态 - 支持Canvas边界约束和局部坐标""" + if getattr(self, 'play_mode', False): + # Disable editor drag logic while in play mode + return task.cont + + # 1. 处理Canvas拖动 + if hasattr(self, 'dragging_canvas') and self.dragging_canvas: + # 查找对应的Canvas数据 + canvas_data = None + for c in self.canvases: + if c['panel'] == self.dragging_canvas: + canvas_data = c + break + + # 检查Canvas是否锁定 + if canvas_data and canvas_data.get('fixed', False): + return task.cont + + if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): + return task.cont + + import panda3d.core as p3d + if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): + self.dragging_canvas = None + return task.cont + + mouse_x = self.world.mouseWatcherNode.getMouseX() + mouse_y = self.world.mouseWatcherNode.getMouseY() + + win_x = self.world.win.getXSize() + win_y = self.world.win.getYSize() + pixel_x = (mouse_x + 1) * win_x / 2 + pixel_y = (1 - mouse_y) * win_y / 2 + + new_x = pixel_x - self.canvas_drag_offset[0] + new_y = pixel_y - self.canvas_drag_offset[1] + + self.dragging_canvas.set_pos(new_x, new_y) + return task.cont + + # 2. 处理组件拖动 + if self.dragging_comp is None: + # Pending drag: only start after threshold + if self.pending_drag_comp is None: + return task.cont + + if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): + return task.cont + + import panda3d.core as p3d + if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): + # click without drag + self.pending_drag_comp = None + self.pending_drag_index = -1 + self.pending_drag_start_mouse = None + self.pending_drag_start_abs = None + self.pending_drag_start_parent = None + return task.cont + + mouse_x = self.world.mouseWatcherNode.getMouseX() + mouse_y = self.world.mouseWatcherNode.getMouseY() + win_x = self.world.win.getXSize() + win_y = self.world.win.getYSize() + pixel_x = (mouse_x + 1) * win_x / 2 + pixel_y = (1 - mouse_y) * win_y / 2 + + if self.pending_drag_start_mouse is None: + self.pending_drag_start_mouse = (pixel_x, pixel_y) + return task.cont + + dx = pixel_x - self.pending_drag_start_mouse[0] + dy = pixel_y - self.pending_drag_start_mouse[1] + if (dx * dx + dy * dy) < (self.drag_start_threshold * self.drag_start_threshold): + return task.cont + + # Start drag now + comp_data = self.pending_drag_comp + self.dragging_comp = comp_data + self.dragging_index = self.pending_drag_index + self._original_parent_obj = self.pending_drag_start_parent + self._is_drag_reparented = False + + acc_left, acc_top = self.pending_drag_start_abs + if acc_left is None or acc_top is None: + acc_left, acc_top = self._get_component_accumulated_pos(self.pending_drag_index) + + # If child, reparent to canvas for free drag + p_idx = comp_data.get('parent_index') + if p_idx is not None and p_idx >= 0 and self.current_canvas_index >= 0: + canvas_panel = self.canvases[self.current_canvas_index]['panel'] + comp_obj = comp_data['object'] + if not comp_data.get('visual_parent_canvas'): + try: + if getattr(comp_obj, 'parent', None) == canvas_panel: + pass + elif hasattr(comp_obj, 'reparent_to'): + comp_obj.reparent_to(canvas_panel) + if hasattr(comp_obj, 'set_pos'): + comp_obj.set_pos(acc_left, acc_top) + else: + comp_obj.left = acc_left + comp_obj.top = acc_top + self._is_drag_reparented = True + except Exception: + pass + + # Compute drag offset + canvas_abs_left = 0 + canvas_abs_top = 0 + if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): + canvas_panel = self.canvases[self.current_canvas_index]['panel'] + canvas_abs_left = getattr(canvas_panel, 'left', 0) + canvas_abs_top = getattr(canvas_panel, 'top', 0) + if canvas_abs_left == 0 and canvas_abs_top == 0: + try: + canvas_pos = canvas_panel.get_pos() + canvas_abs_left = canvas_pos.x + canvas_abs_top = canvas_pos.y + except: + canvas_abs_left = 300 + canvas_abs_top = 100 + + comp_abs_left = canvas_abs_left + acc_left + comp_abs_top = canvas_abs_top + acc_top + self.drag_offset = (pixel_x - comp_abs_left, pixel_y - comp_abs_top) + + # Clear pending state + self.pending_drag_comp = None + self.pending_drag_index = -1 + self.pending_drag_start_mouse = None + self.pending_drag_start_abs = None + self.pending_drag_start_parent = None + + # Continue with drag in same frame + + + # 检查鼠标状态 + if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): + return task.cont + + # 检查鼠标左键是否释放 + import panda3d.core as p3d + if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): + if self.dragging_comp: + comp_data = self.dragging_comp + print(f"组件移动到: left={comp_data.get('left', 0):.1f}, top={comp_data.get('top', 0):.1f} (局部坐标)") + + # 拖动结束:检查是否需要接触父子关系 (Auto-unparent) + should_unparent = False + if getattr(self, '_is_drag_reparented', False): + comp_obj = comp_data['object'] + + # Check if center of component is outside parent bounds + p_idx = comp_data.get('parent_index') + if p_idx is not None and p_idx >= 0: + parent_data = self.components[p_idx] + p_w = parent_data.get('width', 100) + p_h = parent_data.get('height', 100) + + c_w = comp_data.get('width', 0) + c_h = comp_data.get('height', 0) + + # Current comp_data['left'] is LOCAL to parent + local_l = comp_data.get('left', 0) + local_t = comp_data.get('top', 0) + + center_x = local_l + c_w / 2 + center_y = local_t + c_h / 2 + + # Check bounds: center must be in 0..p_w, 0..p_h + # Allow some leniency or strict? Let's say if center is outside, unparent. + if center_x < 0 or center_x > p_w or center_y < 0 or center_y > p_h: + should_unparent = True + + if should_unparent: + # Unparent! + # The component is currently visually attached to Canvas (from drag start) + # We just need to update the data to reflect this permanent change. + + # Calculate the new global (canvas-relative) position + # current local + parent offset + p_off_x, p_off_y = self._get_component_accumulated_pos(p_idx) + new_canvas_left = local_l + p_off_x + new_canvas_top = local_t + p_off_y + + comp_data['left'] = new_canvas_left + comp_data['top'] = new_canvas_top + + # Remove parent linkage + if 'parent_index' in comp_data: + del comp_data['parent_index'] + if 'anchored_to_parent' in comp_data: + del comp_data['anchored_to_parent'] + + print(f"✓ Auto-unparented component from #{p_idx} (Dragged out)") + + elif hasattr(comp_obj, 'reparent_to') and hasattr(self, '_original_parent_obj'): + comp_obj.reparent_to(self._original_parent_obj) + # 重新应用计算出的局部坐标 + comp_obj.left = comp_data.get('left', 0) + comp_obj.top = comp_data.get('top', 0) + print(f"✓ 组件归位到逻辑父级") + + self.dragging_comp = None + return task.cont + + # 获取鼠标位置 + mouse_x = self.world.mouseWatcherNode.getMouseX() + mouse_y = self.world.mouseWatcherNode.getMouseY() + + # 转换为像素坐标 + win_x = self.world.win.getXSize() + win_y = self.world.win.getYSize() + pixel_x = (mouse_x + 1) * win_x / 2 + pixel_y = (1 - mouse_y) * win_y / 2 + + # 计算新的绝对位置(减去偏移量) + new_abs_left = pixel_x - self.drag_offset[0] + new_abs_top = pixel_y - self.drag_offset[1] + + # 获取Canvas信息 + canvas_abs_left = 0 + canvas_abs_top = 0 + canvas_width = 800 + canvas_height = 600 + + if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): + canvas_data = self.canvases[self.current_canvas_index] + canvas_panel = canvas_data['panel'] + + # 获取Canvas的绝对位置 + canvas_abs_left = getattr(canvas_panel, 'left', 0) + canvas_abs_top = getattr(canvas_panel, 'top', 0) + + if canvas_abs_left == 0 and canvas_abs_top == 0: + try: + pos = canvas_panel.get_pos() + canvas_abs_left = pos.x + canvas_abs_top = pos.y + except: + # 如果获取失败,使用默认值 + canvas_abs_left = canvas_data.get('margins', {}).get('left', 240) + canvas_abs_top = canvas_data.get('margins', {}).get('top', 89) + + # 获取Canvas的实际尺寸 + try: + if hasattr(canvas_panel, 'width') and canvas_panel.width is not None: + canvas_width = float(canvas_panel.width) + elif hasattr(canvas_panel, 'get_width'): + canvas_width = float(canvas_panel.get_width()) + + if hasattr(canvas_panel, 'height') and canvas_panel.height is not None: + canvas_height = float(canvas_panel.height) + elif hasattr(canvas_panel, 'get_height'): + canvas_height = float(canvas_panel.get_height()) + except Exception as e: + print(f"Warning: Failed to get canvas size: {e}") + # 使用窗口尺寸减去边距作为后备 + margins = canvas_data.get('margins', {'left': 240, 'right': 480, 'top': 89, 'bottom': 220}) + canvas_width = win_x - margins.get('left', 0) - margins.get('right', 0) + canvas_height = win_y - margins.get('top', 0) - margins.get('bottom', 0) + + # 转换为Canvas相对坐标 (Global Canvas Pos) + canvas_relative_left = new_abs_left - canvas_abs_left + canvas_relative_top = new_abs_top - canvas_abs_top + + # 获取当前拖拽的组件数据 + comp_data = self.dragging_comp + comp_obj = comp_data['object'] + + # 获取组件宽高 (用于边界限制) + comp_width = comp_data.get('width', 0) + comp_height = comp_data.get('height', 0) + + # 尝试从对象获取更准确的宽高 + if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0: + comp_width = comp_obj.width + if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0: + comp_height = comp_obj.height + + # 限制在Canvas范围内 + # 确保左边界不小于0 + if canvas_relative_left < 0: + canvas_relative_left = 0 + + # 确保上边界不小于0 + if canvas_relative_top < 0: + canvas_relative_top = 0 + + # 确保右边界不超出Canvas宽度 + if canvas_relative_left + comp_width > canvas_width: + canvas_relative_left = max(0, canvas_width - comp_width) + + # 确保下边界不超出Canvas高度 + if canvas_relative_top + comp_height > canvas_height: + canvas_relative_top = max(0, canvas_height - comp_height) + + # 如果有父组件,需要转换为相对于父组件的局部坐标 + comp_data = self.dragging_comp + parent_index = comp_data.get('parent_index') + parent_offset_x = 0 + parent_offset_y = 0 + + if parent_index is not None and parent_index >= 0: + # 获取父组件相对于Canvas的累积位置 + parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index) + + # 计算新的局部坐标 + new_local_left = canvas_relative_left - parent_offset_x + new_local_top = canvas_relative_top - parent_offset_y + + # 如果物理上已脱离父级(挂在Canvas上),则物理坐标就是Canvas相对坐标 + comp_obj = comp_data['object'] + if getattr(self, '_is_drag_reparented', False): + # 子组件被重定向到Canvas,使其可以移动到负数局部坐标(即父组件外侧) + if hasattr(comp_obj, 'set_pos'): + comp_obj.set_pos(canvas_relative_left, canvas_relative_top) + else: + comp_obj.left = canvas_relative_left + comp_obj.top = canvas_relative_top + # print(f"Dragging Child (Canvas Rel): {canvas_relative_left:.1f}, {canvas_relative_top:.1f} | Local: {new_local_left:.1f}, {new_local_top:.1f}") + else: + # If visual parent is canvas, keep world position while storing local + if comp_data.get('visual_parent_canvas'): + abs_left = parent_offset_x + new_local_left + abs_top = parent_offset_y + new_local_top + if hasattr(comp_obj, 'set_pos'): + comp_obj.set_pos(abs_left, abs_top) + else: + comp_obj.left = abs_left + comp_obj.top = abs_top + else: + if hasattr(comp_obj, 'set_pos'): + comp_obj.set_pos(new_local_left, new_local_top) + else: + comp_obj.left = new_local_left + comp_obj.top = new_local_top + + # 更新存储的逻辑坐标数据 (保持始终相对于逻辑父级) + comp_data['left'] = new_local_left + comp_data['top'] = new_local_top + + # Sync visually-canvas children for non-container parents + if self.dragging_index is not None and self.dragging_index >= 0: + self._sync_canvas_children(self.dragging_index) + + return task.cont + + def _create_resize_handles(self): + """创建选择框和8个resize手柄""" + from Builtin.LUISprite import LUISprite + from Builtin.LUIObject import LUIObject + + print("开始创建resize handles...") + + # 先创建4条选择框边线(蓝色,2px宽) + for i in range(4): + border = LUISprite(self.overlay_root, "blank", "skin") + border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色 + border.visible = False + + # 设置选择框层级,在组件之上但在LUI内部 + if hasattr(border, 'z_offset'): + border.z_offset = 40 # 选中框在普通边框(25)之上 + elif hasattr(border, 'set_sort'): + border.set_sort(40) + + # 确保边框在最后创建,这样在渲染顺序上会在前面 + self.selection_box.append(border) + + # 最后创建8个resize手柄(白色圆形,蓝色边框) + handle_positions = [ + 'top-left', 'top', 'top-right', 'right', + 'bottom-right', 'bottom', 'bottom-left', 'left' + ] + + for i, pos in enumerate(handle_positions): + # 创建容器对象用于接收鼠标事件 + handle_container = LUIObject(parent=self.overlay_root) + handle_container.width = 10 + handle_container.height = 10 + handle_container.solid = True + handle_container.visible = False + + # 设置手柄层级,在选择框之上但在LUI内部 + if hasattr(handle_container, 'z_offset'): + handle_container.z_offset = 50 # 在选择框之上 + elif hasattr(handle_container, 'set_sort'): + handle_container.set_sort(50) # 在选择框之上 + + # 创建蓝色边框(外圈) + handle_border = LUISprite(handle_container, "blank", "skin") + handle_border.width = 10 + handle_border.height = 10 + handle_border.left = 0 + handle_border.top = 0 + handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框 + + # 创建白色圆形背景(内圈) + handle_bg = LUISprite(handle_container, "blank", "skin") + handle_bg.width = 8 + handle_bg.height = 8 + handle_bg.left = 1 + handle_bg.top = 1 + handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色 + + # 存储手柄位置信息 + handle_container._resize_position = pos + handle_container._resize_index = i + handle_container._bg = handle_bg + handle_container._border = handle_border + + # 绑定鼠标事件 + handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h)) + + self.resize_handles.append(handle_container) + + print(f"✓ 创建了{len(self.selection_box)}条边框和{len(self.resize_handles)}个手柄") + + # 强制将手柄移到最前面(在LUI中,后创建的元素通常在前面) + for handle in self.resize_handles: + # 尝试重新设置父节点来强制更新渲染顺序 + if hasattr(handle, 'reparent_to'): + handle.reparent_to(self.overlay_root) + + for border in self.selection_box: + if hasattr(border, 'reparent_to'): + border.reparent_to(self.overlay_root) + + def _delayed_create_resize_handles(self, task): + """延迟创建resize handles,确保在Canvas之后创建""" + self._create_resize_handles() + return task.done + + def _on_handle_mousedown(self, event, handle): + """手柄鼠标按下事件""" + # 标记交互帧,防止Canvas点击事件触发取消选中 + if hasattr(p3d, 'ClockObject'): + self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount() + else: + # Fallback if specific import not avail in this scope (though p3d is imported) + self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount() + + if self.selected_index < 0: + return + + comp_data = self.components[self.selected_index] + print(f"✓ 开始resize: 手柄位置={handle._resize_position}") + + # 记录开始调整大小的状态 + self.resizing_handle = handle + + # 获取鼠标位置 + if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): + mouse_x = self.world.mouseWatcherNode.getMouseX() + mouse_y = self.world.mouseWatcherNode.getMouseY() + + win_x = self.world.win.getXSize() + win_y = self.world.win.getYSize() + pixel_x = (mouse_x + 1) * win_x / 2 + pixel_y = (1 - mouse_y) * win_y / 2 + + self.resize_start_pos = (pixel_x, pixel_y) + + # 记录组件的初始边界 + self.resize_start_bounds = { + 'left': comp_data.get('left', 0), + 'top': comp_data.get('top', 0), + 'width': comp_data.get('width', 100), + 'height': comp_data.get('height', 30), + } + + def _update_resize_handles(self, task): + # Restore selection if it was cleared immediately + if self.selected_index < 0 and getattr(self, '_force_keep_selection_frames', 0) > 0: + if getattr(self, '_last_selected_index', -1) >= 0: + self.selected_index = self._last_selected_index + + if getattr(self, '_force_keep_selection_frames', 0) > 0: + self._force_keep_selection_frames -= 1 + + """更新resize handles的位置和可见性""" + # 检查当前Canvas是否可见 + if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): + canvas = self.canvases[self.current_canvas_index] + if not canvas.get('visible', True): + self._hide_resize_handles() + return task.cont + + if self.selected_index < 0 or self.selected_index >= len(self.components): + # 隐藏所有handles + self._hide_resize_handles() + return task.cont + + comp_data = self.components[self.selected_index] + comp_type = comp_data['type'] + + # 只对Frame、Button、Slider、InputField、Plane、Image显示resize handles + if comp_type not in ['Frame', 'Button', 'Slider', 'InputField', 'Plane', 'Image', 'Checkbox', 'Text', 'Label', 'Video', 'Progressbar', 'Selectbox', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout']: + self._hide_resize_handles() + return task.cont + + # 显示并更新handles位置 + self._show_and_update_handles(comp_data) + + # 处理resize拖拽 + if self.resizing_handle is not None: + self._handle_resize_drag() + + return task.cont + + def _hide_resize_handles(self): + """隐藏所有resize handles和selection box""" + for border in self.selection_box: + border.visible = False + for handle in self.resize_handles: + handle.visible = False + + def _ensure_component_outline(self, comp_index): + 'Create a simple 4-edge outline for a component if not exists.' + if comp_index in self.debug_outlines: + return self.debug_outlines[comp_index] + + from Builtin.LUISprite import LUISprite + + borders = [] + for _ in range(4): + border = LUISprite(self.overlay_root, "blank", "skin") + border.color = (0.2, 0.7, 1.0, 1.0) + border.visible = False + + # Ensure outline is above components + if hasattr(border, 'z_offset'): + border.z_offset = 25 + elif hasattr(border, 'set_sort'): + border.set_sort(25) + + borders.append(border) + + self.debug_outlines[comp_index] = borders + return borders + + def _update_component_outlines(self, task): + 'Show component outlines in edit mode, hide in play mode.' + if getattr(self, 'play_mode', False) or not self.lui_enabled or not self.show_editor: + for borders in self.debug_outlines.values(): + for b in borders: + b.visible = False + return task.cont + + for idx, comp_data in enumerate(self.components): + # If selected, let the resize handles/selection box handle the outline (Bright Blue) + # We hide the "debug" outline for the selected item to avoid double-drawing or z-fighting + if idx == self.selected_index: + borders = self.debug_outlines.get(idx) + if borders: + for b in borders: + b.visible = False + continue + + comp_obj = comp_data.get('object') + if comp_obj is None: + continue + + canvas_index = comp_data.get('canvas_index', self.current_canvas_index) + if canvas_index is None or canvas_index < 0 or canvas_index >= len(self.canvases): + # Component not assigned to a valid canvas + borders = self.debug_outlines.get(idx) + if borders: + for b in borders: b.visible = False + continue + + canvas = self.canvases[canvas_index] + if not canvas.get('visible', True): + # Hide outlines for components on hidden canvases + borders = self.debug_outlines.get(idx) + if borders: + for b in borders: + b.visible = False + continue + + # Determine size + width = comp_data.get('width', 0) + height = comp_data.get('height', 0) + try: + if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0: + width = comp_obj.width + if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0: + height = comp_obj.height + except Exception: + pass + + if width <= 0 or height <= 0: + continue + + # Determine Position (Absolute) + canvas_abs_left = 0 + canvas_abs_top = 0 + canvas_panel = self.canvases[canvas_index]['panel'] + canvas_abs_left = getattr(canvas_panel, 'left', 0) + canvas_abs_top = getattr(canvas_panel, 'top', 0) + if canvas_abs_left == 0 and canvas_abs_top == 0: + try: + canvas_pos = canvas_panel.get_pos() + canvas_abs_left = canvas_pos.x + canvas_abs_top = canvas_pos.y + except Exception: + canvas_abs_left = 300 + canvas_abs_top = 100 + + if hasattr(comp_obj, 'get_abs_pos'): + try: + abs_pos = comp_obj.get_abs_pos() + abs_left = abs_pos.x + abs_top = abs_pos.y + except Exception: + acc_left, acc_top = self._get_component_accumulated_pos(idx) + abs_left = canvas_abs_left + acc_left + abs_top = canvas_abs_top + acc_top + else: + acc_left, acc_top = self._get_component_accumulated_pos(idx) + abs_left = canvas_abs_left + acc_left + abs_top = canvas_abs_top + acc_top + + # Draw Outline + borders = self._ensure_component_outline(idx) + + # Style for unselected components: Gray, thinner or same thickness + for b in borders: + b.color = (0.7, 0.7, 0.7, 0.5) # Light gray, semi-transparent + if hasattr(b, 'z_offset'): + b.z_offset = 25 + elif hasattr(b, 'set_sort'): + b.set_sort(25) + b.visible = True + + t = 1.0 # Thickness + + borders[0].left = abs_left + borders[0].top = abs_top - t + borders[0].width = width + borders[0].height = t + + borders[1].left = abs_left + borders[1].top = abs_top + height + borders[1].width = width + borders[1].height = t + + borders[2].left = abs_left - t + borders[2].top = abs_top + borders[2].width = t + borders[2].height = height + + borders[3].left = abs_left + width + borders[3].top = abs_top + borders[3].width = t + borders[3].height = height + + return task.cont + + def _show_and_update_handles(self, comp_data): + """显示并更新handles位置 - 修复坐标系不一致问题""" + # 每次显示时重新创建手柄,确保它们在最前面 + if not hasattr(self, '_handles_recreated'): + self._recreate_handles_on_top() + self._handles_recreated = True + + # 获取组件的Canvas相对位置和尺寸 + comp_obj = comp_data['object'] + + # 获取Canvas的绝对位置用于坐标转换 + canvas_abs_left = 0 + canvas_abs_top = 0 + if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): + canvas_panel = self.canvases[self.current_canvas_index]['panel'] + # 使用LUI对象的left和top属性,而不是get_pos()方法 + canvas_abs_left = getattr(canvas_panel, 'left', 0) + canvas_abs_top = getattr(canvas_panel, 'top', 0) + + # 如果left/top属性不存在,尝试使用get_pos() + if canvas_abs_left == 0 and canvas_abs_top == 0: + try: + canvas_pos = canvas_panel.get_pos() + canvas_abs_left = canvas_pos.x + canvas_abs_top = canvas_pos.y + except: + # 如果get_pos()也失败,使用默认值 + canvas_abs_left = 300 # 默认Canvas位置 + canvas_abs_top = 100 + + # 尝试直接获取组件的绝对屏幕坐标 + if hasattr(comp_obj, 'get_abs_pos'): + try: + abs_pos = comp_obj.get_abs_pos() + abs_left = abs_pos.x + abs_top = abs_pos.y + # print(f"使用 get_abs_pos(): {abs_left}, {abs_top}") + except Exception as e: + print(f"get_abs_pos() 失败: {e}") + # Fallback to manual calculation + acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index) + abs_left = canvas_abs_left + acc_left + abs_top = canvas_abs_top + acc_top + else: + # 递归计算组件的 Canvas 相对位置(累加所有父组件的相对位置) + # 使用 self.selected_index 似乎最安全,或者反查 comp_data + if self.selected_index >= 0 and self.components[self.selected_index] == comp_data: + acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index) + else: + acc_left = comp_data.get('left', 0) + acc_top = comp_data.get('top', 0) + + # 转换为绝对屏幕坐标(用于手柄和边框显示) + abs_left = canvas_abs_left + acc_left + abs_top = canvas_abs_top + acc_top + + # 获取组件尺寸 - 优先从LUI对象获取实际尺寸 + comp_type = comp_data['type'] + width = comp_data.get('width', 100) + height = comp_data.get('height', 30) + + # 尝试从LUI对象获取实际尺寸 + try: + if hasattr(comp_obj, 'width') and comp_obj.width is not None: + actual_width = comp_obj.width + if actual_width > 0: + width = actual_width + if hasattr(comp_obj, 'height') and comp_obj.height is not None: + actual_height = comp_obj.height + if actual_height > 0: + height = actual_height + except: + pass + + # 确保宽高是数值类型 + try: + width = float(width) + height = float(height) + except (TypeError, ValueError): + width = 100.0 + height = 30.0 + + # 更新comp_data中的尺寸 + comp_data['width'] = width + comp_data['height'] = height + + # 更新4条边框(使用绝对位置,因为边框在overlay_root下) + # 确保 selection_box 存在 + if not self.selection_box: + self._create_resize_handles() + if not self.selection_box: # Still empty? + return + + # 上边框 + self.selection_box[0].left = abs_left + self.selection_box[0].top = abs_top - 2 + self.selection_box[0].width = width + self.selection_box[0].height = 2 + self.selection_box[0].visible = True + + # 下边框 + self.selection_box[1].left = abs_left + self.selection_box[1].top = abs_top + height + self.selection_box[1].width = width + self.selection_box[1].height = 2 + self.selection_box[1].visible = True + + # 左边框 + self.selection_box[2].left = abs_left - 2 + self.selection_box[2].top = abs_top + self.selection_box[2].width = 2 + self.selection_box[2].height = height + self.selection_box[2].visible = True + + # 右边框 + self.selection_box[3].left = abs_left + width + self.selection_box[3].top = abs_top + self.selection_box[3].width = 2 + self.selection_box[3].height = height + self.selection_box[3].visible = True + + # 更新8个手柄位置(使用绝对位置,因为手柄在overlay_root下) + handle_positions = [ + (abs_left - 5, abs_top - 5), # 左上 + (abs_left + width/2 - 5, abs_top - 5), # 上 + (abs_left + width - 5, abs_top - 5), # 右上 + (abs_left + width - 5, abs_top + height/2 - 5), # 右 + (abs_left + width - 5, abs_top + height - 5), # 右下 + (abs_left + width/2 - 5, abs_top + height - 5), # 下 + (abs_left - 5, abs_top + height - 5), # 左下 + (abs_left - 5, abs_top + height/2 - 5), # 左 + ] + + for i, (h_left, h_top) in enumerate(handle_positions): + if i < len(self.resize_handles): + handle = self.resize_handles[i] + handle.left = h_left + handle.top = h_top + handle.visible = True + + def _recreate_handles_on_top(self): + """重新创建手柄,确保它们在最前面""" + from Builtin.LUISprite import LUISprite + from Builtin.LUIObject import LUIObject + + # 清除旧的手柄 + for handle in self.resize_handles: + if hasattr(handle, 'remove'): + handle.remove() + for border in self.selection_box: + if hasattr(border, 'remove'): + border.remove() + + self.resize_handles.clear() + self.selection_box.clear() + + # 重新创建边框 + for i in range(4): + border = LUISprite(self.overlay_root, "blank", "skin") + border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色 + border.visible = False + + # 设置选择框层级,在组件之上但在LUI内部 + if hasattr(border, 'z_offset'): + border.z_offset = 40 + elif hasattr(border, 'set_sort'): + border.set_sort(40) + + self.selection_box.append(border) + + # 重新创建手柄 + handle_positions = [ + 'top-left', 'top', 'top-right', 'right', + 'bottom-right', 'bottom', 'bottom-left', 'left' + ] + + for i, pos in enumerate(handle_positions): + handle_container = LUIObject(parent=self.overlay_root) + handle_container.width = 10 + handle_container.height = 10 + handle_container.solid = True + handle_container.visible = False + + # 设置手柄层级,在选择框之上但在LUI内部 + if hasattr(handle_container, 'z_offset'): + handle_container.z_offset = 50 + elif hasattr(handle_container, 'set_sort'): + handle_container.set_sort(50) + + # 创建蓝色边框(外圈) + handle_border = LUISprite(handle_container, "blank", "skin") + handle_border.width = 10 + handle_border.height = 10 + handle_border.left = 0 + handle_border.top = 0 + handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框 + + # 创建白色圆形背景(内圈) + handle_bg = LUISprite(handle_container, "blank", "skin") + handle_bg.width = 8 + handle_bg.height = 8 + handle_bg.left = 1 + handle_bg.top = 1 + handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色 + + # 存储手柄位置信息 + handle_container._resize_position = pos + handle_container._resize_index = i + handle_container._bg = handle_bg + handle_container._border = handle_border + + # 绑定鼠标事件 + handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h)) + + self.resize_handles.append(handle_container) + + print("✓ 重新创建了手柄,确保在最前面") + + def _handle_resize_drag(self): + """处理resize拖拽 - 完整功能实现,支持Shift和Alt键操作""" + if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): + return + + # 检查鼠标左键是否释放 + import panda3d.core as p3d + if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): + if self.resizing_handle: + comp_data = self.components[self.selected_index] + width_val = comp_data.get('width', 0) + height_val = comp_data.get('height', 0) + print(f"✓ 组件调整完成: width={width_val:.1f}, height={height_val:.1f}") + + # 更新锚点位置(如果有锚点) + if comp_data.get('anchored_to_parent'): + anchor_pos = comp_data.get('anchor_position') + if anchor_pos: + self._update_component_anchor_position(self.selected_index, anchor_pos) + + self.resizing_handle = None + return + + # 获取当前鼠标位置 + mouse_x = self.world.mouseWatcherNode.getMouseX() + mouse_y = self.world.mouseWatcherNode.getMouseY() + + win_x = self.world.win.getXSize() + win_y = self.world.win.getYSize() + pixel_x = (mouse_x + 1) * win_x / 2 + pixel_y = (1 - mouse_y) * win_y / 2 + + # 计算鼠标移动距离 + delta_x = pixel_x - self.resize_start_pos[0] + delta_y = pixel_y - self.resize_start_pos[1] + + # 检查修饰键 + shift_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.shift()) + alt_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.alt()) + + # 获取组件数据 + comp_data = self.components[self.selected_index] + comp_obj = comp_data['object'] + comp_type = comp_data['type'] + + # 获取初始边界(Canvas相对坐标) + start_left = self.resize_start_bounds['left'] + start_top = self.resize_start_bounds['top'] + start_width = self.resize_start_bounds['width'] + start_height = self.resize_start_bounds['height'] + + # 计算初始宽高比 + aspect_ratio = start_width / start_height if start_height > 0 else 1 + + # 根据手柄位置计算新的边界 + handle_pos = self.resizing_handle._resize_position + + new_left = start_left + new_top = start_top + new_width = start_width + new_height = start_height + + # 根据手柄位置调整尺寸 + if handle_pos == 'top-left': + new_left = start_left + delta_x + new_top = start_top + delta_y + new_width = start_width - delta_x + new_height = start_height - delta_y + elif handle_pos == 'top': + new_top = start_top + delta_y + new_height = start_height - delta_y + elif handle_pos == 'top-right': + new_top = start_top + delta_y + new_width = start_width + delta_x + new_height = start_height - delta_y + elif handle_pos == 'right': + new_width = start_width + delta_x + elif handle_pos == 'bottom-right': + new_width = start_width + delta_x + new_height = start_height + delta_y + elif handle_pos == 'bottom': + new_height = start_height + delta_y + elif handle_pos == 'bottom-left': + new_left = start_left + delta_x + new_width = start_width - delta_x + new_height = start_height + delta_y + elif handle_pos == 'left': + new_left = start_left + delta_x + new_width = start_width - delta_x + + # 应用最小尺寸限制 + if new_width < self.min_size: + if handle_pos in ['top-left', 'left', 'bottom-left']: + new_left = start_left + start_width - self.min_size + new_width = self.min_size + + if new_height < self.min_size: + if handle_pos in ['top-left', 'top', 'top-right']: + new_top = start_top + start_height - self.min_size + new_height = self.min_size + + # Shift键:保持宽高比 + if shift_held: + width_change = abs(new_width - start_width) + height_change = abs(new_height - start_height) + + if width_change > height_change: + new_height = new_width / aspect_ratio + if handle_pos in ['top-left', 'top', 'top-right']: + new_top = start_top + start_height - new_height + else: + new_width = new_height * aspect_ratio + if handle_pos in ['top-left', 'left', 'bottom-left']: + new_left = start_left + start_width - new_width + + # Alt键:从中心点缩放 + if alt_held: + center_x = start_left + start_width / 2 + center_y = start_top + start_height / 2 + + width_diff = new_width - start_width + height_diff = new_height - start_height + + new_width = start_width + width_diff * 2 + new_height = start_height + height_diff * 2 + new_left = center_x - new_width / 2 + new_top = center_y - new_height / 2 + + # 再次应用最小尺寸限制 + if new_width < self.min_size: + new_width = self.min_size + new_left = center_x - new_width / 2 + if new_height < self.min_size: + new_height = self.min_size + new_top = center_y - new_height / 2 + + # 获取Canvas边界约束 + canvas_width = 800 # 默认Canvas宽度 + canvas_height = 600 # 默认Canvas高度 + + if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): + canvas_panel = self.canvases[self.current_canvas_index]['panel'] + canvas_width = canvas_panel.get_width() + canvas_height = canvas_panel.get_height() + + + # 最后的安全检查,防止宽高变为负数 + if new_width < 1.0: new_width = 1.0 + if new_height < 1.0: new_height = 1.0 + + if self.selected_index < 0: + return + + comp_data = self.components[self.selected_index] + parent_index = comp_data.get('parent_index') + parent_offset_x = 0 + parent_offset_y = 0 + + if parent_index is not None and parent_index >= 0: + parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index) + + # Update component data + comp_data['left'] = new_left + comp_data['top'] = new_top + comp_data['width'] = new_width + comp_data['height'] = new_height + + # Update component position + child_parent = getattr(comp_obj, 'parent', None) + parent_obj = None + if parent_index is not None and parent_index >= 0: + parent_obj = self.components[parent_index].get('object') + + # Check if actually parented in LUI scene graph + is_scene_parented = (child_parent is not None and child_parent == parent_obj) + + if is_scene_parented: + # Component is physically parented to its logical parent -> Use Relative coords + comp_obj.left = new_left + comp_obj.top = new_top + else: + # Component is physically root/canvas -> Use Absolute coords (Parent Abs + Relative) + comp_obj.left = parent_offset_x + new_left + comp_obj.top = parent_offset_y + new_top + + # Update component size + try: + if hasattr(comp_obj, 'width'): + comp_obj.width = new_width + if hasattr(comp_obj, 'height'): + comp_obj.height = new_height + + if comp_type == 'Frame': + pass + elif comp_type in ['Plane', 'Image', 'Video']: + sprite = comp_data.get('sprite') + if sprite is not None: + sprite.width = new_width + sprite.height = new_height + elif comp_type == 'Button': + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + if hasattr(comp_obj, '_apply_stretch_sizes'): + comp_obj._apply_stretch_sizes() + elif comp_type == 'InputField': + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + if hasattr(comp_obj, 'width'): + comp_obj.width = new_width + if hasattr(comp_obj, 'height'): + comp_obj.height = new_height + try: + layout = getattr(comp_obj, '_layout', None) + if layout is not None: + if hasattr(layout, 'width'): + layout.width = '100%' + if hasattr(layout, 'height'): + layout.height = '100%' + inner = getattr(layout, '_layout', None) + if inner is not None: + if hasattr(inner, 'width'): + inner.width = '100%' + if hasattr(inner, 'height'): + inner.height = '100%' + for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): + spr = getattr(layout, attr, None) + if spr is not None and hasattr(spr, 'height'): + spr.height = '100%' + if spr is not None and attr == '_sprite_mid' and hasattr(spr, 'width'): + spr.width = '100%' + except Exception: + pass + elif comp_type == 'Slider': + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + + if comp_type in ['VerticalLayout', 'HorizontalLayout']: + self._update_layout_inner(self.selected_index) + + self._update_anchored_children(self.selected_index) + + modifier_info = [] + if shift_held: + modifier_info.append('Shift(keep ratio)') + if alt_held: + modifier_info.append('Alt(center scale)') + modifier_str = ' + '.join(modifier_info) if modifier_info else 'Normal' + except Exception as e: + print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}") + + def _add_to_scene_tree(self, comp_data): + """Add a virtual node to represent the LUI component in the scene tree""" + from panda3d.core import NodePath + node_name = f"UI_{comp_data['type']}_{len(self.components)}" + ui_node = NodePath(node_name) + + # Reparent to current canvas node if available + if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): + canvas_node = self.canvases[self.current_canvas_index]['node'] + ui_node.reparent_to(canvas_node) + comp_data['canvas_index'] = self.current_canvas_index + else: + ui_node.reparent_to(self.world.render) + comp_data['canvas_index'] = None + + comp_data['ui_node'] = ui_node + comp_data['parent_index'] = None + comp_data['children_indices'] = [] + + def _set_parent_child_relationship(self, child_index, parent_index, anchor_position=None, keep_world=False): + # Set parent-child relationship + if (child_index < 0 or child_index >= len(self.components) or + parent_index < 0 or parent_index >= len(self.components)): + print("Error: invalid component index") + return + + child_data = self.components[child_index] + parent_data = self.components[parent_index] + + if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: + child_data['draggable'] = False + + if child_data.get('parent_index') == parent_index: + print(f"Component {child_index} already under {parent_index}") + return + + current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index) + + print(f"[_set_parent_child_relationship] Child #{child_index} -> Parent #{parent_index}") + print(f" Current absolute position: ({current_abs_left:.1f}, {current_abs_top:.1f})") + + old_parent_index = child_data.get('parent_index') + if old_parent_index is not None and old_parent_index >= 0: + old_parent_data = self.components[old_parent_index] + if child_index in old_parent_data.get('children_indices', []): + old_parent_data['children_indices'].remove(child_index) + + child_data['parent_index'] = parent_index + if 'children_indices' not in parent_data: + parent_data['children_indices'] = [] + if child_index not in parent_data['children_indices']: + parent_data['children_indices'].append(child_index) + + parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) + new_local_left = current_abs_left - parent_abs_left + new_local_top = current_abs_top - parent_abs_top + + print(f" Parent absolute position: ({parent_abs_left:.1f}, {parent_abs_top:.1f})") + print(f" Calculated local position: ({new_local_left:.1f}, {new_local_top:.1f})") + + # 在数据结构中存储局部坐标(相对于父组件) + child_data['left'] = new_local_left + child_data['top'] = new_local_top + + child_obj = child_data['object'] + parent_obj = parent_data['object'] + parent_type = parent_data.get('type') + visual_container_types = [ + 'Frame', 'Plane', 'Video', + 'ScrollableRegion', 'TabbedFrame', + 'VerticalLayout', 'HorizontalLayout' + ] + if parent_data.get('type') == 'ScrollableRegion': + parent_obj = parent_data['object'].content_node + elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): + if parent_data.get('layout_wrap', True): + parent_obj = parent_data.get('object') + else: + parent_obj = parent_data.get('layout_obj').cell() + + try: + if parent_type in visual_container_types: + child_data['visual_parent_canvas'] = False + # 关键修改:不要调用 reparent_to(),避免破坏内部结构 + # 只更新数据结构,保持原有的父对象关系 + print(f"[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)") + else: + # Keep visual parent on canvas to avoid clipping, but keep logical parent + child_data['visual_parent_canvas'] = True + + # If not preserving world position, clamp into parent bounds for visibility + if not keep_world: + try: + parent_w = parent_data.get('width') + parent_h = parent_data.get('height') + if parent_w is None and hasattr(parent_obj, 'width'): + parent_w = parent_obj.width + if parent_h is None and hasattr(parent_obj, 'height'): + parent_h = parent_obj.height + child_w = child_data.get('width') + child_h = child_data.get('height') + if child_w is None and hasattr(child_obj, 'width'): + child_w = child_obj.width + if child_h is None and hasattr(child_obj, 'height'): + child_h = child_obj.height + if parent_w and parent_h and child_w and child_h: + max_x = max(0.0, float(parent_w) - float(child_w)) + max_y = max(0.0, float(parent_h) - float(child_h)) + new_local_left = max(0.0, min(float(new_local_left), max_x)) + new_local_top = max(0.0, min(float(new_local_top), max_y)) + child_data['left'] = new_local_left + child_data['top'] = new_local_top + except Exception: + pass + + # 关键修改:由于我们不实际重新父化对象,所有组件都保持相对于Canvas的绝对位置 + # 只在数据结构中记录相对于父组件的局部坐标 + if child_data.get('visual_parent_canvas'): + # Keep world position + try: + child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index) + except Exception: + pass + + # 根据keep_world参数决定是否保持世界位置 + if keep_world: + # 保持世界位置:使用绝对坐标 + if hasattr(child_obj, 'left'): + child_obj.left = current_abs_left + if hasattr(child_obj, 'top'): + child_obj.top = current_abs_top + print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") + else: + # 不保持世界位置:使用局部坐标(但由于没有实际重新父化,这会导致位置跳变) + # 为了避免跳变,我们仍然使用绝对坐标 + if hasattr(child_obj, 'left'): + child_obj.left = current_abs_left + if hasattr(child_obj, 'top'): + child_obj.top = current_abs_top + print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") + + # Ensure child visuals are visible above parent + if hasattr(child_obj, 'z_offset'): + try: + parent_z = getattr(parent_obj, 'z_offset', 0) + child_obj.z_offset = float(parent_z) + 2.0 + except Exception: + pass + + # Special handling for Button: raise internal sprites/label above parent + if child_data.get('type') == 'Button': + try: + layout = getattr(child_obj, '_layout', None) + if layout is not None: + for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): + spr = getattr(layout, attr, None) + if spr is not None and hasattr(spr, 'z_offset'): + spr.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 1.0 + lbl = getattr(child_obj, '_label', None) + if lbl is not None and hasattr(lbl, 'z_offset'): + lbl.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 2.0 + # Ensure label visible + if lbl is not None and hasattr(lbl, 'show'): + lbl.show() + except Exception: + pass + + # Special handling for Frame: ensure visibility + elif child_data.get('type') == 'Frame': + try: + if hasattr(child_obj, 'show'): + child_obj.show() + if hasattr(child_obj, 'visible'): + child_obj.visible = True + print(f"[_set_parent_child_relationship] Frame visibility ensured") + except Exception as e: + print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}") + + # Special handling for Plane, Image, Video: ensure sprite visibility + elif child_data.get('type') in ['Plane', 'Image', 'Video']: + try: + spr = child_data.get('sprite') + if spr is not None: + if hasattr(spr, 'show'): + spr.show() + if hasattr(spr, 'visible'): + spr.visible = True + + # Ensure sprite color is not transparent + if child_data.get('type') == 'Plane': + color = child_data.get('color', (1, 1, 1, 1)) + if hasattr(spr, 'color'): + spr.color = color + + print(f"[_set_parent_child_relationship] {child_data.get('type')} sprite visibility ensured") + except Exception as e: + print(f"[_set_parent_child_relationship] Error ensuring {child_data.get('type')} visibility: {e}") + + child_sprite = child_data.get('sprite') + if child_sprite is not None: + try: + if hasattr(child_sprite, 'show'): + child_sprite.show() + # Ensure sprite is attached to the child object + if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj: + child_sprite.parent = child_obj + # Restore sprite color if stored + if hasattr(child_sprite, 'color') and 'color' in child_data: + child_sprite.color = child_data.get('color', child_sprite.color) + if hasattr(child_sprite, 'z_offset'): + parent_sprite = parent_data.get('sprite') + parent_sprite_z = 0.0 + if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): + parent_sprite_z = float(parent_sprite.z_offset) + child_sprite.z_offset = max(parent_sprite_z + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0) + except Exception: + pass + # Keep child visible above parent visuals + if hasattr(child_obj, 'z_offset'): + parent_z = getattr(parent_obj, 'z_offset', 0) + try: + child_obj.z_offset = float(parent_z) + 2.0 + except Exception: + pass + # Ensure child sprite renders above parent visuals + child_sprite = child_data.get('sprite') + if child_sprite is not None and hasattr(child_sprite, 'z_offset'): + parent_sprite_z = 0 + try: + parent_sprite = parent_data.get('sprite') + if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): + parent_sprite_z = float(parent_sprite.z_offset) + except Exception: + parent_sprite_z = 0 + try: + child_sprite.z_offset = max(float(parent_sprite_z) + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0) + except Exception: + pass + if hasattr(child_obj, 'show'): + child_obj.show() + + except Exception as e: + print(f"Reparenting error: {e}") + + # Ensure canvas_index follows parent canvas (visibility) + parent_canvas = parent_data.get('canvas_index') + if parent_canvas is not None: + self._update_canvas_index_recursive(child_index, parent_canvas) + + if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True): + self._apply_wrap_layout(parent_index) + + print(f"Set parent: child #{child_index} -> parent #{parent_index}") + + + def _set_parent_root(self, child_index, keep_world=False): + """Detach a component from its parent and move it to root (canvas).""" + if child_index < 0 or child_index >= len(self.components): + print("Error: invalid component index") + return + + child_data = self.components[child_index] + + # 保存当前的累积位置(相对于Canvas的绝对位置) + current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index) + + print(f"[_set_parent_root] Component #{child_index} current accumulated pos: ({current_abs_left:.1f}, {current_abs_top:.1f})") + + old_parent_index = child_data.get('parent_index') + if old_parent_index is not None and old_parent_index >= 0: + old_parent_data = self.components[old_parent_index] + if child_index in old_parent_data.get('children_indices', []): + old_parent_data['children_indices'].remove(child_index) + + # 更新数据结构 + child_data['parent_index'] = None + child_data['visual_parent_canvas'] = False + child_data['draggable'] = True + + # 获取Canvas panel + parent_obj = None + canvas_index = self.current_canvas_index + if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases): + parent_obj = self.canvases[canvas_index]['panel'] + child_data['canvas_index'] = canvas_index + else: + parent_obj = getattr(self, 'overlay_root', None) or getattr(self.world, 'render', None) + + child_obj = child_data.get('object') + + # 关键修改:对于 Button 等复杂组件,不要重新父化! + # 只更新位置和数据结构 + if child_obj is not None: + # 更新位置数据 + if keep_world: + child_data['left'] = current_abs_left + child_data['top'] = current_abs_top + + # 直接设置对象位置,不改变父对象 + if hasattr(child_obj, 'set_pos'): + child_obj.set_pos(current_abs_left, current_abs_top) + else: + if hasattr(child_obj, 'left'): + child_obj.left = current_abs_left + if hasattr(child_obj, 'top'): + child_obj.top = current_abs_top + + print(f"[_set_parent_root] Set position to: ({current_abs_left:.1f}, {current_abs_top:.1f}) without reparenting") + else: + # 不保持世界位置:使用原有的局部坐标 + if hasattr(child_obj, 'left'): + child_obj.left = child_data.get('left', 0) + if hasattr(child_obj, 'top'): + child_obj.top = child_data.get('top', 0) + + # 确保可见性 + if child_obj is not None: + try: + if hasattr(child_obj, 'show'): + child_obj.show() + elif hasattr(child_obj, 'visible'): + child_obj.visible = True + except Exception: + pass + + # 对于 Button,确保内部组件可见 + comp_type = child_data.get('type') + if comp_type == 'Button' and child_obj is not None: + try: + layout = getattr(child_obj, '_layout', None) + lbl = getattr(child_obj, '_label', None) + + if layout is not None: + if hasattr(layout, 'show'): + layout.show() + if hasattr(layout, 'visible'): + layout.visible = True + + if lbl is not None: + if hasattr(lbl, 'show'): + lbl.show() + if hasattr(lbl, 'visible'): + lbl.visible = True + + # 重新设置文本 + current_text = child_data.get('text', 'Button') + if hasattr(lbl, 'set_text'): + lbl.set_text(current_text) + elif hasattr(lbl, 'text'): + lbl.text = current_text + + print(f"[_set_parent_root] Button components visibility ensured") + except Exception as e: + print(f"[_set_parent_root] Error ensuring button visibility: {e}") + + # 对于 Plane, Image, Video - 确保内部 sprite 可见 + elif comp_type in ['Plane', 'Image', 'Video'] and child_obj is not None: + try: + spr = child_data.get('sprite') + if spr is not None: + if hasattr(spr, 'show'): + spr.show() + if hasattr(spr, 'visible'): + spr.visible = True + + # 确保 sprite 的颜色不是透明的 + if comp_type == 'Plane': + color = child_data.get('color', (1, 1, 1, 1)) + if hasattr(spr, 'color'): + spr.color = color + + print(f"[_set_parent_root] {comp_type} sprite visibility ensured") + except Exception as e: + print(f"[_set_parent_root] Error ensuring {comp_type} visibility: {e}") + + # 对于 Frame - 直接确保可见 + elif comp_type == 'Frame' and child_obj is not None: + try: + if hasattr(child_obj, 'show'): + child_obj.show() + if hasattr(child_obj, 'visible'): + child_obj.visible = True + print(f"[_set_parent_root] Frame visibility ensured") + except Exception as e: + print(f"[_set_parent_root] Error ensuring Frame visibility: {e}") + + # Update canvas index recursively + self._update_canvas_index_recursive(child_index, child_data.get('canvas_index')) + + # 调试输出:确认组件状态 + print(f"[_set_parent_root] Component #{child_index} ({child_data.get('name', 'Unknown')}) moved to root (data only)") + print(f" - Position: ({child_data.get('left', 0):.1f}, {child_data.get('top', 0):.1f})") + print(f" - Size: ({child_data.get('width', 0):.1f}, {child_data.get('height', 0):.1f})") + print(f" - Canvas index: {child_data.get('canvas_index')}") + print(f" - Visible: {getattr(child_obj, 'visible', 'N/A') if child_obj else 'N/A'}") + print(f" - Parent (unchanged): {getattr(child_obj, 'parent', 'N/A') if child_obj else 'N/A'}") + def create_child_component(self, parent_index, comp_type, anchor_position=None): + # Create child component under parent + print("\n=== Create child component ===") + print(f"Parent index: {parent_index}, Type: {comp_type}") + + if parent_index < 0 or parent_index >= len(self.components): + print("Error: invalid parent index") + return None + + parent_data = self.components[parent_index] + parent_obj = parent_data['object'] + if parent_data.get('type') == 'ScrollableRegion': + parent_obj = parent_data['object'].content_node + elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): + if parent_data.get('layout_wrap', True): + parent_obj = parent_data.get('object') + else: + parent_obj = parent_data.get('layout_obj').cell() + + parent_width = parent_data.get('width', 100) + parent_height = parent_data.get('height', 30) + + comp_sizes = { + 'Button': (100, 30), + 'Text': (80, 20), + 'Label': (80, 20), + 'CheckBox': (120, 20), + 'InputField': (200, 24), + 'Slider': (200, 16), + 'Frame': (300, 200), + 'Plane': (100, 100), + 'Image': (100, 100), + 'Video': (320, 240), + 'Progressbar': (200, 30), + 'Selectbox': (200, 30), + 'ScrollableRegion': (200, 200), + 'TabbedFrame': (300, 200), + 'VerticalLayout': (300, 200), + 'HorizontalLayout': (300, 200), + } + + child_w, child_h = comp_sizes.get(comp_type, (80, 20)) + child_x = (parent_width - child_w) / 2 + child_y = (parent_height - child_h) / 2 + + if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: + child_x = 0 + child_y = 0 + + child_obj = None + try: + if comp_type == 'Button': + child_obj = self.luiFunction.create_button(self, text="ChildButton", x=child_x, y=child_y, parent=parent_obj) + elif comp_type in ['Text', 'Label']: + child_obj = self.luiFunction.create_label(self, text="ChildText", x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'CheckBox': + child_obj = self.luiFunction.create_checkbox(self, label="ChildCheckbox", x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'InputField': + child_obj = self.luiFunction.create_input_field(self, text="ChildInput", x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'Slider': + child_obj = self.luiFunction.create_slider(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'Frame': + child_obj = self.luiFunction.create_frame(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'Plane': + child_obj = self.luiFunction.create_plane(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'Image': + child_obj = self.luiFunction.create_image(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'Video': + child_obj = self.luiFunction.create_video(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'Progressbar': + child_obj = self.luiFunction.create_progressbar(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'Selectbox': + child_obj = self.luiFunction.create_selectbox(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'ScrollableRegion': + child_obj = self.luiFunction.create_scrollable_region(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'TabbedFrame': + child_obj = self.luiFunction.create_tabbed_frame(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'VerticalLayout': + child_obj = self.luiFunction.create_vertical_layout(self, x=child_x, y=child_y, parent=parent_obj) + elif comp_type == 'HorizontalLayout': + child_obj = self.luiFunction.create_horizontal_layout(self, x=child_x, y=child_y, parent=parent_obj) + else: + print(f"Error: unsupported component type {comp_type}") + return None + + except Exception as e: + print(f"Error: create component failed: {e}") + import traceback + traceback.print_exc() + return None + + if child_obj: + child_index = -1 + for i, comp in enumerate(self.components): + if comp.get('object') == child_obj: + child_index = i + break + + if child_index >= 0: + child_data = self.components[child_index] + if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True): + self._apply_wrap_layout(parent_index) + + if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: + child_data['draggable'] = False + + child_data['parent_index'] = parent_index + if 'children_indices' not in parent_data: + parent_data['children_indices'] = [] + if child_index not in parent_data['children_indices']: + parent_data['children_indices'].append(child_index) + + print(f"Linked child #{child_index} -> parent #{parent_index}") + return child_index + else: + print("Child creation failed") + return None + + def _get_descendants(self, idx): + """Return set of all descendant indices for a component.""" + desc = set() + if idx < 0 or idx >= len(self.components): + return desc + comp = self.components[idx] + for c_idx in comp.get('children_indices', []): + if c_idx in desc: + continue + desc.add(c_idx) + desc.update(self._get_descendants(c_idx)) + return desc + + def _update_canvas_index_recursive(self, idx, canvas_index): + """Update canvas_index for component and its descendants.""" + if idx < 0 or idx >= len(self.components): + return + comp = self.components[idx] + comp['canvas_index'] = canvas_index + for c_idx in comp.get('children_indices', []): + self._update_canvas_index_recursive(c_idx, canvas_index) + + def _reorder_list(self, items, src, target): + """Move src before target within items list.""" + if src == target: + return items + if src not in items or target not in items: + return items + items = list(items) + items.remove(src) + insert_at = items.index(target) + items.insert(insert_at, src) + return items + + def _sync_canvas_children(self, parent_index): + """Update positions for children that keep visual parent on canvas.""" + if parent_index < 0 or parent_index >= len(self.components): + return + + # Get parent's absolute position + parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) + parent_data = self.components[parent_index] + parent_obj = parent_data.get('object') + + for child_idx in parent_data.get('children_indices', []): + if child_idx < 0 or child_idx >= len(self.components): + continue + child = self.components[child_idx] + child_obj = child.get('object') + + if child_obj is None: + continue + + # Check actual parenting + child_parent = getattr(child_obj, 'parent', None) + is_scene_parented = (child_parent is not None and child_parent == parent_obj) + + if not is_scene_parented: + # If child is NOT physically parented to the parent object (e.g. it is on Canvas), + # we MUST manually update its absolute position to follow the parent. + abs_left = parent_abs_left + float(child.get('left', 0)) + abs_top = parent_abs_top + float(child.get('top', 0)) + + if hasattr(child_obj, 'left'): + child_obj.left = abs_left + if hasattr(child_obj, 'top'): + child_obj.top = abs_top + if hasattr(child_obj, 'set_pos'): + child_obj.set_pos(abs_left, abs_top) + + # Recurse for grandchildren + self._sync_canvas_children(child_idx) + + + + + + def draw_editor(self): + """Draw the LUI Editor Window""" + if not self.lui_enabled or not self.show_editor: + return + + imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever) + with imgui_ctx.begin("LUI编辑器", True) as (opened, show): + self.show_editor = opened + if not show: + return + + # Play/Stop + if imgui.button("Play" if not self.play_mode else "Stop", (120, 25)): + self.play_mode = not self.play_mode + if self.play_mode: + # Clear editor-only state when entering play mode + self.selected_index = -1 + self.dragging_comp = None + self.pending_drag_comp = None + self.pending_drag_index = -1 + self.pending_drag_start_mouse = None + self.pending_drag_start_abs = None + self.pending_drag_start_parent = None + self._hide_resize_handles() + # Keep LUI input enabled for runtime UI interaction + self.set_input_enabled(True) + imgui.same_line() + imgui.text_colored((0.2, 1.0, 0.2, 1.0) if self.play_mode else (1.0, 0.8, 0.2, 1.0), + "Running" if self.play_mode else "Editing") + imgui.separator() + + # In play mode, show toolbar only + if self.play_mode: + return + + # Canvas Management + imgui.text("Canvas 管理") + imgui.separator() + + if imgui.button("创建新Canvas", (150, 25)): + self.luiFunction.create_canvas(self) + + imgui.same_line() + + # 显示当前Canvas信息 + if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): + current_canvas_name = self.canvases[self.current_canvas_index]['name'] + imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"当前: {current_canvas_name}") + else: + imgui.text_colored((1.0, 0.0, 0.0, 1.0), "无Canvas") + + if len(self.canvases) > 0: + canvas_names = [c['name'] for c in self.canvases] + if self.current_canvas_index >= len(canvas_names): + self.current_canvas_index = len(canvas_names) - 1 + + changed, new_index = imgui.combo("选择Canvas", self.current_canvas_index, canvas_names) + if changed: + self.switch_canvas(new_index) + + # Canvas Properties + if self.current_canvas_index >= 0: + canvas = self.canvases[self.current_canvas_index] + panel = canvas['panel'] + imgui.indent() + + # Visibility + is_visible = canvas.get('visible', True) + changed, visible = imgui.checkbox("显示Canvas", is_visible) + if changed: + canvas['visible'] = visible + if visible: + panel.show() + else: + panel.hide() + + + # Fill Mode & Margins + is_fill_mode = canvas.get('fill_mode', False) + imgui.separator() + changed, fill_mode = imgui.checkbox("场景填充模式", is_fill_mode) + if changed: + canvas['fill_mode'] = fill_mode + if fill_mode: + # 刚切换到填充模式时,更新一次 + self._update_canvas_geometry(canvas) + + if fill_mode: + imgui.text("场景边距设置 (调整以匹配视口)") + margins = canvas.get('margins', {'left':0,'right':0,'top':0,'bottom':0}) + geometry_changed = False + + changed, margins['left'] = imgui.drag_float("左边距", margins['left'], 1.0, 0, 1000) + if changed: geometry_changed = True + + changed, margins['right'] = imgui.drag_float("右边距", margins['right'], 1.0, 0, 1000) + if changed: geometry_changed = True + + changed, margins['top'] = imgui.drag_float("顶边距", margins['top'], 1.0, 0, 500) + if changed: geometry_changed = True + + changed, margins['bottom'] = imgui.drag_float("底边距", margins['bottom'], 1.0, 0, 500) + if changed: geometry_changed = True + + if geometry_changed: + canvas['margins'] = margins + self._update_canvas_geometry(canvas) + else: + # Manual Position + curr_pos_val = panel.get_pos() + curr_pos = (curr_pos_val.x, curr_pos_val.y) + + # 固定开关 + is_fixed = canvas.get('fixed', False) + changed, fixed = imgui.checkbox("锁定位置 (禁止拖动)", is_fixed) + if changed: + canvas['fixed'] = fixed + + if not is_fixed: + changed, pos = imgui.drag_float2("Canvas位置", curr_pos) + if changed: + panel.set_pos(pos[0], pos[1]) + + + # Manual Size + curr_size = (panel.get_width(), panel.get_height()) + changed, size = imgui.drag_float2("Canvas尺寸", curr_size, 1, 100, 4000) + if changed: + panel.width = size[0] + panel.height = size[1] + # 同时更新背景尺寸 + if 'background' in canvas: + bg = canvas['background'] + bg.width = size[0] + bg.height = size[1] + + # Canvas Color + if 'background' in canvas: + imgui.separator() + bg = canvas['background'] + # 获取当前颜色 (RGBA) + current_color = bg.color + # 转换为可编辑的格式 (list) + edit_color = [current_color[0], current_color[1], current_color[2], current_color[3]] + + changed, new_color = imgui.color_edit4("背景颜色/透明度", edit_color) + if changed: + bg.color = (new_color[0], new_color[1], new_color[2], new_color[3]) + + # Title settings + if 'title_label' in canvas: + imgui.separator() + imgui.text("标题设置") + title = canvas['title_label'] + + # Visible + is_title_visible = title.visible + changed, title_visible = imgui.checkbox("显示标题", is_title_visible) + if changed: + try: + if title_visible: + title.show() + else: + title.hide() + except: + title.visible = title_visible + + # Text content + curr_text = title.text + changed, new_text = imgui.input_text("标题文本", curr_text, 128) + if changed: + title.text = new_text + + # Position + curr_pos_val = title.get_pos() + curr_pos = (curr_pos_val.x, curr_pos_val.y) + changed, new_pos = imgui.drag_float2("标题位置", curr_pos) + if changed: + title.set_pos(new_pos[0], new_pos[1]) + + imgui.unindent() + else: + imgui.text_colored((1, 1, 0, 1), "请先创建一个Canvas") + + imgui.spacing() + imgui.text("创建组件") + imgui.separator() + + if len(self.canvases) == 0: + imgui.begin_disabled() + + # Component Type Selection + component_types = ["按钮 (Button)", "文本 (Text)", "复选框 (CheckBox)", "输入框 (InputField)", + "滑块 (Slider)", "框架 (Frame)","面板 (Plane)","图片 (Image)","视频 (Video)", + "进度条 (Progressbar)", "下拉框 (Selectbox)", "滚动区域 (ScrollableRegion)", "标签页 (TabbedFrame)", + "垂直布局组 (VerticalLayout)", "水平布局组 (HorizontalLayout)"] + + changed, self._selected_type_index = imgui.combo("组件类型", self._selected_type_index, component_types) + + if imgui.button("创建组件", (120, 25)): + self._create_component_by_type(self._selected_type_index) + + if len(self.canvases) == 0: + imgui.end_disabled() + + imgui.spacing() + imgui.text(f"已创建组件 ({len(self.components)})") + imgui.separator() + + # Component Tree Helper + # self.draw_component_tree() + + imgui.spacing() + imgui.separator() + + # Anchor Popup Handling + self._handle_anchor_popup() + + def draw_component_tree(self): + """Draw the component tree (can be used in other windows)""" + # Track tree item rects for drop hit-testing (allows drop on earlier items) + self._tree_item_rects = {} + + def _store_item_rect(item_key): + try: + rmin = imgui.get_item_rect_min() + rmax = imgui.get_item_rect_max() + self._tree_item_rects[item_key] = (rmin, rmax) + except Exception: + pass + + def _hit_test_tree_item(mx, my): + for key, (rmin, rmax) in self._tree_item_rects.items(): + try: + min_x = rmin.x + min_y = rmin.y + max_x = rmax.x + max_y = rmax.y + except Exception: + min_x = rmin[0] + min_y = rmin[1] + max_x = rmax[0] + max_y = rmax[1] + if min_x <= mx <= max_x and min_y <= my <= max_y: + return key + return None + + # Root drop target + if imgui.selectable("ROOT", self.selected_index < 0): + # ???????? + pass + _store_item_rect(-1) + + def render_component_tree(idx): + if idx >= len(self.components): return True + comp = self.components[idx] + comp_type = comp.get('type', 'Unknown') + + label = f"{comp.get('name', 'N/A')}" + + # Determine flags + flags = imgui.TreeNodeFlags_.open_on_arrow | imgui.TreeNodeFlags_.open_on_double_click | imgui.TreeNodeFlags_.default_open + if self.selected_index == idx: + flags |= imgui.TreeNodeFlags_.selected + + # Check for children + children = list(comp.get('children_indices', [])) + if not children: + flags |= imgui.TreeNodeFlags_.leaf + else: + label += f" ({len(children)})" + + # Draw tree node + is_open = imgui.tree_node_ex(f"##node_{idx}", flags, label) + _store_item_rect(idx) + + # Custom drag/drop (avoid payload capsule issues) + if imgui.is_item_active() and imgui.is_mouse_dragging(0): + self._tree_drag_src = idx + imgui.begin_tooltip() + imgui.text(f"Move: {label}") + imgui.end_tooltip() + + # Handle selection + if imgui.is_item_clicked(): + self.selected_index = idx + # Clear 3D scene selection + if hasattr(self.world, 'selection') and self.world.selection: + self.world.selection.selectedNode = None + self.world.selection.clearSelectionBox() + self.world.selection.clearGizmo() + + # Right-click menu + if imgui.begin_popup_context_item(f"##comp_ctx_{idx}"): + if imgui.menu_item("删除", "", False)[0]: + self.luiFunction.delete_component(self, idx) + imgui.end_popup() + if is_open: imgui.tree_pop() + return False + + imgui.separator() + if imgui.begin_menu("创建子组件"): + child_types = ['Button', 'Text', 'CheckBox', 'InputField', 'Slider', 'Frame', 'Plane','Image', 'Video','Progressbar','Selectbox','ScrollableRegion','TabbedFrame', 'VerticalLayout', 'HorizontalLayout'] + cn_names = ['按钮', '标签', '复选框', '输入框', '滑块', '框架', '面板', '图片', '视频','进度条','下拉框','滚动区域','标签页', '垂直布局组', '水平布局组'] + for ct_idx, ct in enumerate(child_types): + if imgui.menu_item(f"{cn_names[ct_idx]}", "", False)[0]: + self.create_child_component(idx, ct) + imgui.end_menu() + imgui.end_popup() + + # Draw children if open + if is_open: + for child_idx in children: + # After a deletion, the list might have shifted, + # so we check bounds again or just return False to stop this frame's recursion + if child_idx < len(self.components): + if not render_component_tree(child_idx): + # If a child (or sub-child) was deleted, indices shifted. + # Stopping current rendering frame's branch is safest. + imgui.tree_pop() + return False + imgui.tree_pop() + return True + + # Find root components + roots = [] + for i, comp in enumerate(self.components): + p_idx = comp.get('parent_index') + if p_idx is None or p_idx < 0: + roots.append(i) + + # Maintain root order for sorting + if not self.root_order: + self.root_order = list(roots) + else: + self.root_order = [r for r in self.root_order if r in roots] + for r in roots: + if r not in self.root_order: + self.root_order.append(r) + + # Render roots + for root_idx in self.root_order: + if not render_component_tree(root_idx): + break + + # Handle tree drop after full render (allows drop on earlier items) + if self._tree_drag_src is not None and imgui.is_mouse_released(0): + src_idx = self._tree_drag_src + self._tree_drag_src = None + mx, my = imgui.get_mouse_pos() + target_idx = _hit_test_tree_item(mx, my) + if target_idx is None: + target_idx = -1 + if 0 <= src_idx < len(self.components): + if target_idx == -1: + self._set_parent_root(src_idx, keep_world=True) + elif target_idx != src_idx: + if target_idx in self._get_descendants(src_idx): + print("Invalid drop: cannot parent to descendant") + else: + src_parent = self.components[src_idx].get('parent_index', -1) + tgt_parent = self.components[target_idx].get('parent_index', -1) + io = imgui.get_io() + want_reorder = bool(getattr(io, 'key_shift', False)) + if want_reorder and src_parent == tgt_parent: + if src_parent is None or src_parent < 0: + if not self.root_order: + self.root_order = [i for i, c in enumerate(self.components) if c.get('parent_index') is None or c.get('parent_index') < 0] + self.root_order = self._reorder_list(self.root_order, src_idx, target_idx) + else: + parent_data = self.components[src_parent] + children = parent_data.get('children_indices', []) + parent_data['children_indices'] = self._reorder_list(children, src_idx, target_idx) + else: + self._set_parent_child_relationship(src_idx, target_idx, keep_world=True) + + # Clear drag state if released outside any item + if self._tree_drag_src is not None and imgui.is_mouse_released(0): + self._tree_drag_src = None + + imgui.spacing() + imgui.separator() + + # Property Editing + if self.selected_index >= 0 and self.selected_index < len(self.components): + imgui.text("属性编辑") + imgui.separator() + self.luiFunction._draw_component_properties(self,self.selected_index) + else: + imgui.text_disabled("未选中任何组件") + + # Anchor popup + if self.anchor_popup_open: + imgui.open_popup("选择锚点位置") + + if imgui.begin_popup("选择锚点位置"): + imgui.text("选择锚点位置:") + + # 中文显示名称 + anchor_names = { + 'top-left': '左上', 'top-center': '中上', 'top-right': '右上', + 'middle-left': '左中', 'center': '中心', 'middle-right': '右中', + 'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下' + } + + anchor_positions = [ + ['top-left', 'top-center', 'top-right'], + ['middle-left', 'center', 'middle-right'], + ['bottom-left', 'bottom-center', 'bottom-right'] + ] + + for row in anchor_positions: + for i, pos in enumerate(row): + button_text = anchor_names.get(pos, pos) + if imgui.button(f"{button_text}##popup_{pos}", (50, 25)): + # 使用新的锚点更新方法 + if hasattr(self, '_temp_selected_index_for_anchor'): + selected_idx = self._temp_selected_index_for_anchor + self._update_component_anchor_position(selected_idx, pos) + + self.anchor_popup_open = False + if hasattr(self, '_temp_selected_index_for_anchor'): + delattr(self, '_temp_selected_index_for_anchor') + imgui.close_current_popup() + if i < len(row) - 1: + imgui.same_line() + + imgui.spacing() + if imgui.button("取消"): + self.anchor_popup_open = False + if hasattr(self, '_temp_selected_index_for_anchor'): + delattr(self, '_temp_selected_index_for_anchor') + imgui.close_current_popup() + + imgui.end_popup() + + def _create_component_by_type(self, type_index): + """Create LUI component by index - 默认生成在Canvas中心""" + + # 1. 确定生成位置 (默认 Canvas 中心) + canvas_relative_x = 100 + canvas_relative_y = 100 + + if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): + canvas_panel = self.canvases[self.current_canvas_index]['panel'] + canvas_width = canvas_panel.get_width() + canvas_height = canvas_panel.get_height() + + # 根据组件类型预测尺寸,以便进行中心点对齐 + # 注意:这些是 lui_function.py 中定义的默认值 + comp_sizes = { + 0: (100, 30), # Button + 1: (80, 20), # Text + 2: (120, 20), # Checkbox + 3: (200, 24), # InputField + 4: (200, 16), # Slider + 5: (300, 200), # Frame + 6: (100, 100), # Plane + 7: (100, 100), # Image + 8: (320, 240), # Video + 9: (200, 30), # Progressbar + 10: (200, 30), # Selectbox + 11: (200, 200), # ScrollableRegion + 12: (300, 200), # TabbedFrame + 13: (300, 200), # VerticalLayout + 14: (300, 200) # HorizontalLayout + } + + + w, h = comp_sizes.get(type_index, (100, 100)) + + # 计算中心位置 + canvas_relative_x = (canvas_width - w) / 2 + canvas_relative_y = (canvas_height - h) / 2 + + # 确保不超出合理范围 (防止 Canvas 太小时位置为负) + canvas_relative_x = max(10, canvas_relative_x) + canvas_relative_y = max(10, canvas_relative_y) + + try: + if type_index == 0: # Button + self.luiFunction.create_button(self, text="New Button", x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 1: # Text + self.luiFunction.create_label(self, text="New Text", x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 2: # Checkbox + self.luiFunction.create_checkbox(self, label="Check box", x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 3: # InputField + self.luiFunction.create_input_field(self, text="", x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 4: # Slider + self.luiFunction.create_slider(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 5: # Frame + self.luiFunction.create_frame(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 6: # Plane + self.luiFunction.create_plane(self, x=canvas_relative_x, y=canvas_relative_y, color=(0.8, 0.8, 0.8, 1)) + elif type_index == 7: # Image + self.luiFunction.create_image(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 8: # Video + self.luiFunction.create_video(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 9: # Progressbar + self.luiFunction.create_progressbar(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 10: # Selectbox + self.luiFunction.create_selectbox(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 11: # ScrollableRegion + self.luiFunction.create_scrollable_region(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 12: # TabbedFrame + self.luiFunction.create_tabbed_frame(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 13: # VerticalLayout + self.luiFunction.create_vertical_layout(self, x=canvas_relative_x, y=canvas_relative_y) + elif type_index == 14: # HorizontalLayout + self.luiFunction.create_horizontal_layout(self, x=canvas_relative_x, y=canvas_relative_y) + + print(f"✓ 创建组件成功,已重定向至 Canvas 中心: ({canvas_relative_x:.1f}, {canvas_relative_y:.1f})") + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() + except Exception as e: + print(f"创建组件失败: {str(e)}") + + + + def set_component_anchor(self, comp_index, anchor_position): + """设置组件锚点 - 保持向后兼容""" + self._update_component_anchor_position(comp_index, anchor_position) + + def _update_anchored_children(self, parent_index): + """Recursively update anchored/fill children.""" + if parent_index < 0 or parent_index >= len(self.components): + return + + print(f"[_update_anchored_children] Updating children of #{parent_index}") + parent_data = self.components[parent_index] + children = parent_data.get('children_indices', []) + for child_idx in children: + if child_idx < len(self.components): + child_data = self.components[child_idx] + # Fill layout has priority over anchored positioning + if child_data.get('layout_mode') == 'fill': + print(f" -> Child #{child_idx} is FILL mode") + self._apply_fill_layout(child_idx) + elif child_data.get('anchored_to_parent'): + anchor_pos = child_data.get('anchor_position') + manual_offset = ( + child_data.get('anchor_manual_offset_x', 0.0), + child_data.get('anchor_manual_offset_y', 0.0) + ) + if anchor_pos: + self._update_component_anchor_position(child_idx, anchor_pos, manual_offset) + + # Recurse into children + self._update_anchored_children(child_idx) + + def _apply_fill_layout(self, comp_index): + """Apply fill layout to component: match parent/canvas size with margins.""" + if comp_index < 0 or comp_index >= len(self.components): + return + + comp_data = self.components[comp_index] + comp_obj = comp_data.get('object') + comp_type = comp_data.get('type', '') + + parent_width = None + parent_height = None + + parent_index = comp_data.get('parent_index') + if parent_index is not None and parent_index >= 0 and parent_index < len(self.components): + parent_data = self.components[parent_index] + parent_width = parent_data.get('width') + parent_height = parent_data.get('height') + parent_obj = parent_data.get('object') + if parent_width is None and parent_obj is not None and hasattr(parent_obj, 'width'): + parent_width = parent_obj.width + if parent_height is None and parent_obj is not None and hasattr(parent_obj, 'height'): + parent_height = parent_obj.height + else: + canvas_index = comp_data.get('canvas_index', self.current_canvas_index) + if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases): + canvas_panel = self.canvases[canvas_index].get('panel') + if canvas_panel is not None: + if hasattr(canvas_panel, 'width'): + parent_width = canvas_panel.width + elif hasattr(canvas_panel, 'get_width'): + parent_width = canvas_panel.get_width() + if hasattr(canvas_panel, 'height'): + parent_height = canvas_panel.height + elif hasattr(canvas_panel, 'get_height'): + parent_height = canvas_panel.get_height() + + # DEBUG: Trace fill layout calculations + # print(f"[_apply_fill_layout] Comp #{comp_index} Parent #{parent_index}: PW={parent_width} PH={parent_height}") + + if parent_width is None or parent_height is None: + # print(f"[_apply_fill_layout] Failed to get parent dimensions for Comp #{comp_index}") + return + + margin_left = float(comp_data.get('fill_margin_left', 0.0)) + margin_right = float(comp_data.get('fill_margin_right', 0.0)) + margin_top = float(comp_data.get('fill_margin_top', 0.0)) + margin_bottom = float(comp_data.get('fill_margin_bottom', 0.0)) + + new_left = margin_left + new_top = margin_top + new_width = float(parent_width) - (margin_left + margin_right) + new_height = float(parent_height) - (margin_top + margin_bottom) + + if new_width < 1.0: + new_width = 1.0 + if new_height < 1.0: + new_height = 1.0 + + comp_data['left'] = new_left + comp_data['top'] = new_top + comp_data['width'] = new_width + comp_data['height'] = new_height + + if comp_obj is not None: + # Handle visual_parent_canvas case: convert local to absolute + abs_left = new_left + abs_top = new_top + + if parent_index is not None and parent_index >= 0 and comp_data.get('visual_parent_canvas'): + p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index) + abs_left = p_abs_x + new_left + abs_top = p_abs_y + new_top + + if hasattr(comp_obj, 'set_pos'): + comp_obj.set_pos(abs_left, abs_top) + else: + if hasattr(comp_obj, 'left'): + comp_obj.left = abs_left + if hasattr(comp_obj, 'top'): + comp_obj.top = abs_top + if hasattr(comp_obj, 'width'): + comp_obj.width = new_width + if hasattr(comp_obj, 'height'): + comp_obj.height = new_height + + if comp_type in ['Plane', 'Image', 'Video'] and 'sprite' in comp_data: + comp_data['sprite'].width = new_width + comp_data['sprite'].height = new_height + elif comp_type == 'Button': + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + + # Force update internal layout for Button to ensure fill works + try: + layout = getattr(comp_obj, '_layout', None) + if layout: + if hasattr(layout, 'width'): layout.width = new_width + if hasattr(layout, 'height'): layout.height = new_height + + # Inner layout + inner = getattr(layout, '_layout', None) + if inner: + if hasattr(inner, 'width'): inner.width = new_width + if hasattr(inner, 'height'): inner.height = new_height + + # Mid sprite usually stretches + spr_mid = getattr(layout, '_sprite_mid', None) + if spr_mid: + spr_mid.width = new_width + spr_mid.height = new_height + except Exception: + pass + elif comp_type == 'InputField': + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + elif comp_type == 'Slider': + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + + + def _apply_layout_spacing(self, layout_obj, spacing): + try: + if hasattr(layout_obj, 'set_spacing'): + layout_obj.set_spacing(spacing) + elif hasattr(layout_obj, 'setSpacing'): + layout_obj.setSpacing(spacing) + elif hasattr(layout_obj, 'spacing'): + layout_obj.spacing = spacing + elif hasattr(layout_obj, '_spacing'): + layout_obj._spacing = spacing + except Exception: + pass + + def _apply_layout_alignment(self, layout_obj, align): + try: + if hasattr(layout_obj, 'set_alignment'): + layout_obj.set_alignment(align) + elif hasattr(layout_obj, 'alignment'): + layout_obj.alignment = align + elif hasattr(layout_obj, 'align'): + layout_obj.align = align + except Exception: + pass + + def _update_layout_inner(self, comp_index): + if comp_index < 0 or comp_index >= len(self.components): + return + + comp_data = self.components[comp_index] + layout_obj = comp_data.get('layout_obj') + if layout_obj is None: + return + + width = float(comp_data.get('width', 0.0)) + height = float(comp_data.get('height', 0.0)) + + pad_left = float(comp_data.get('layout_padding_left', 0.0)) + pad_right = float(comp_data.get('layout_padding_right', 0.0)) + pad_top = float(comp_data.get('layout_padding_top', 0.0)) + pad_bottom = float(comp_data.get('layout_padding_bottom', 0.0)) + + inner_w = max(1.0, width - (pad_left + pad_right)) + inner_h = max(1.0, height - (pad_top + pad_bottom)) + + try: + if hasattr(layout_obj, 'left'): + layout_obj.left = pad_left + if hasattr(layout_obj, 'top'): + layout_obj.top = pad_top + if hasattr(layout_obj, 'width'): + layout_obj.width = inner_w + if hasattr(layout_obj, 'height'): + layout_obj.height = inner_h + if hasattr(layout_obj, 'set_size'): + layout_obj.set_size(inner_w, inner_h) + except Exception: + pass + + spacing = float(comp_data.get('layout_spacing', 0.0)) + self._apply_layout_spacing(layout_obj, spacing) + + align = comp_data.get('layout_align', None) + if align: + self._apply_layout_alignment(layout_obj, align) + + + if comp_data.get('type') in ['HorizontalLayout', 'VerticalLayout']: + self._apply_wrap_layout(comp_index) + + try: + if hasattr(layout_obj, 'update'): + layout_obj.update() + except Exception: + pass + + + def _reparent_scrollable_children(self, parent_index): + if parent_index < 0 or parent_index >= len(self.components): + return + parent_data = self.components[parent_index] + if parent_data.get('type') != 'ScrollableRegion': + return + parent_obj = parent_data['object'] + try: + content_node = parent_obj.content_node + except Exception: + return + + for child_idx in parent_data.get('children_indices', []): + if child_idx < 0 or child_idx >= len(self.components): + continue + child_data = self.components[child_idx] + child_obj = child_data.get('object') + if child_obj is None: + continue + try: + if hasattr(child_obj, 'reparent_to'): + child_obj.reparent_to(content_node) + elif hasattr(child_obj, 'parent'): + child_obj.parent = content_node + except Exception: + pass + + def _apply_wrap_layout(self, parent_index): + if parent_index < 0 or parent_index >= len(self.components): + return + + parent_data = self.components[parent_index] + layout_type = parent_data.get('type') + if layout_type not in ['HorizontalLayout', 'VerticalLayout']: + return + if not parent_data.get('layout_wrap', True): + return + + # Get parent absolute position for coordinate conversion + parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) + + width = float(parent_data.get('width', 0.0)) + height = float(parent_data.get('height', 0.0)) + + pad_left = float(parent_data.get('layout_padding_left', 0.0)) + pad_right = float(parent_data.get('layout_padding_right', 0.0)) + pad_top = float(parent_data.get('layout_padding_top', 0.0)) + pad_bottom = float(parent_data.get('layout_padding_bottom', 0.0)) + spacing = float(parent_data.get('layout_spacing', 0.0)) + line_spacing = float(parent_data.get('layout_line_spacing', 0.0)) + + inner_w = max(1.0, width - (pad_left + pad_right)) + inner_h = max(1.0, height - (pad_top + pad_bottom)) + + if layout_type == 'HorizontalLayout': + x = pad_left + y = pad_top + row_h = 0.0 + + for child_idx in parent_data.get('children_indices', []): + if child_idx < 0 or child_idx >= len(self.components): + continue + child = self.components[child_idx] + child_obj = child.get('object') + + cw = float(child.get('width', 0.0)) + ch = float(child.get('height', 0.0)) + # Allow layout even if size is 0, but usually skip + # if cw <= 0 or ch <= 0: continue + + if x > pad_left and (x + cw) > (pad_left + inner_w): + x = pad_left + y += row_h + line_spacing + row_h = 0.0 + + # Data stores relative position (to parent) + child['left'] = x + child['top'] = y + + # Update visual object + try: + child_parent = getattr(child_obj, 'parent', None) + parent_obj = parent_data.get('object') + + if child_parent is not None and child_parent == parent_obj: + # Child is parented to layout object: use relative coordinates + if hasattr(child_obj, 'left'): + child_obj.left = x + if hasattr(child_obj, 'top'): + child_obj.top = y + if hasattr(child_obj, 'set_pos'): + child_obj.set_pos(x, y) + else: + # Child is NOT parented to layout object (e.g. at root): use absolute coordinates + abs_x = parent_abs_left + x + abs_y = parent_abs_top + y + + if hasattr(child_obj, 'left'): + child_obj.left = abs_x + if hasattr(child_obj, 'top'): + child_obj.top = abs_y + if hasattr(child_obj, 'set_pos'): + child_obj.set_pos(abs_x, abs_y) + except Exception: + pass + + # Recursively update child layout/visuals + if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True): + self._apply_wrap_layout(child_idx) + elif child.get('children_indices'): + self._sync_canvas_children(child_idx) + + row_h = max(row_h, ch) + x += cw + spacing + else: + # Vertical wrap: flow top->bottom, then next column + x = pad_left + y = pad_top + col_w = 0.0 + + for child_idx in parent_data.get('children_indices', []): + if child_idx < 0 or child_idx >= len(self.components): + continue + child = self.components[child_idx] + child_obj = child.get('object') + + cw = float(child.get('width', 0.0)) + ch = float(child.get('height', 0.0)) + + if y > pad_top and (y + ch) > (pad_top + inner_h): + y = pad_top + x += col_w + line_spacing + col_w = 0.0 + + child['left'] = x + child['top'] = y + + try: + child_parent = getattr(child_obj, 'parent', None) + parent_obj = parent_data.get('object') + + if child_parent is not None and child_parent == parent_obj: + # Child is parented to layout object: use relative coordinates + if hasattr(child_obj, 'left'): + child_obj.left = x + if hasattr(child_obj, 'top'): + child_obj.top = y + if hasattr(child_obj, 'set_pos'): + child_obj.set_pos(x, y) + else: + # Child is NOT parented to layout object (e.g. at root): use absolute coordinates + abs_x = parent_abs_left + x + abs_y = parent_abs_top + y + + if hasattr(child_obj, 'left'): + child_obj.left = abs_x + if hasattr(child_obj, 'top'): + child_obj.top = abs_y + if hasattr(child_obj, 'set_pos'): + child_obj.set_pos(abs_x, abs_y) + except Exception: + pass + + # If child is also a layout, recursively update it + if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True): + self._apply_wrap_layout(child_idx) + # If child is NOT a layout but has children (like a Frame with children), update their visual text + elif child.get('children_indices'): + self._sync_canvas_children(child_idx) + + col_w = max(col_w, cw) + y += ch + spacing + + def get_selected_component(self): + """获取当前选中的组件""" + if self.selected_index >= 0 and self.selected_index < len(self.components): + return self.components[self.selected_index] + return None + + def select_component(self, index): + """选中指定索引的组件""" + if 0 <= index < len(self.components): + self.selected_index = index + print(f"✓ 选中组件 #{index}: {self.components[index]['type']}") + else: + self.selected_index = -1 + + def deselect_all(self): + """取消选中所有组件""" + if getattr(self, '_force_keep_selection_frames', 0) > 0: + return + self.selected_index = -1 + print("✓ 已取消选中所有组件") + + def _change_image_texture(self, title = "选择文件",filetypes=None,initialdir=None): + """更改图像组件的纹理""" + try: + import tkinter as tk + from tkinter import filedialog + + root = tk.Tk() + root.withdraw() + root.attributes('-topmost',True) + + if filetypes is None: + filetypes = [("所有文件","*.*")] + + file_path = filedialog.askopenfilename( + title = title, + filetypes = filetypes, + initialdir = initialdir + ) + root.destroy() + + if file_path: + return file_path + return None + + except Exception as e: + print(f"更改纹理失败: {e}") + + def _update_component_anchor_position(self, comp_index, anchor_position, manual_offset=(0.0, 0.0)): + """更新组件的锚点位置""" + if comp_index < 0 or comp_index >= len(self.components): + return + + + + comp_data = self.components[comp_index] + comp_obj = comp_data['object'] + + # Update data + comp_data['anchor_position'] = anchor_position + comp_data['anchored_to_parent'] = True + comp_data['anchor_manual_offset_x'] = manual_offset[0] + comp_data['anchor_manual_offset_y'] = manual_offset[1] + + # Determine strict parent reference + parent_index = comp_data.get('parent_index') + parent_width = 0 + parent_height = 0 + + if parent_index is not None and parent_index >= 0: + parent_data = self.components[parent_index] + parent_width = parent_data.get('width', 100) + parent_height = parent_data.get('height', 30) + # Try to get actual object size if available + p_obj = parent_data.get('object') + if p_obj: + if hasattr(p_obj, 'width') and p_obj.width > 0: parent_width = p_obj.width + if hasattr(p_obj, 'height') and p_obj.height > 0: parent_height = p_obj.height + elif self.current_canvas_index >= 0: + # Canvas anchor + canvas = self.canvases[self.current_canvas_index]['panel'] + if hasattr(canvas, 'get_width'): + parent_width = canvas.get_width() + parent_height = canvas.get_height() + else: + parent_width = canvas.width + parent_height = canvas.height + + # Calculate anchor point in parent local space + anchor_ratios = { + 'top-left': (0, 0), 'top-center': (0.5, 0), 'top-right': (1, 0), + 'middle-left': (0, 0.5), 'center': (0.5, 0.5), 'middle-right': (1, 0.5), + 'bottom-left': (0, 1), 'bottom-center': (0.5, 1), 'bottom-right': (1, 1) + } + rx, ry = anchor_ratios.get(anchor_position, (0, 0)) + + anchor_x = parent_width * rx + anchor_y = parent_height * ry + + child_width = comp_data.get('width', 0) + child_height = comp_data.get('height', 0) + # Try to get actual object size + if hasattr(comp_obj, 'width') and comp_obj.width > 0: child_width = comp_obj.width + if hasattr(comp_obj, 'height') and comp_obj.height > 0: child_height = comp_obj.height + + # Center child on anchor (pivot logic) + child_left = anchor_x - (child_width * rx) + manual_offset[0] + child_top = anchor_y - (child_height * ry) + manual_offset[1] + + # Update logical position data + comp_data['left'] = child_left + comp_data['top'] = child_top + + # Update visual position + if parent_index is not None and parent_index >= 0: + # Check if visually parented to canvas (absolute positioning needed) + if comp_data.get('visual_parent_canvas'): + p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index) + abs_left = p_abs_x + child_left + abs_top = p_abs_y + child_top + + # Prefer left/top properties for LUI objects + if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): + comp_obj.left = abs_left + comp_obj.top = abs_top + elif hasattr(comp_obj, 'set_pos'): + try: + comp_obj.set_pos(abs_left, abs_top) + except TypeError: + # Fallback for NodePath-like objects requiring 3 args + comp_obj.set_pos(abs_left, abs_top, 0) + else: + # Standard local parenting + if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): + comp_obj.left = child_left + comp_obj.top = child_top + elif hasattr(comp_obj, 'set_pos'): + try: + comp_obj.set_pos(child_left, child_top) + except TypeError: + comp_obj.set_pos(child_left, child_top, 0) + else: + # Canvas root + if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): + comp_obj.left = child_left + comp_obj.top = child_top + elif hasattr(comp_obj, 'set_pos'): + try: + comp_obj.set_pos(child_left, child_top) + except TypeError: + comp_obj.set_pos(child_left, child_top, 0) + + print(f"✓ 已更新锚点位置: {anchor_position} -> ({child_left:.1f}, {child_top:.1f})") + + # Update children positions (for visual_parent_canvas or anchored children) + self._sync_canvas_children(comp_index) + self._update_anchored_children(comp_index) + + def _handle_anchor_popup(self): + """Handle component anchor selection popup""" + if imgui.begin_popup("选择锚点位置"): + imgui.text("选择锚点位置:") + + anchor_names = { + 'top-left': '左上', 'top-center': '中上', 'top-right': '右上', + 'middle-left': '左中', 'center': '中心', 'middle-right': '右中', + 'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下' + } + + anchor_positions = [ + ['top-left', 'top-center', 'top-right'], + ['middle-left', 'center', 'middle-right'], + ['bottom-left', 'bottom-center', 'bottom-right'] + ] + + for row in anchor_positions: + for i, pos in enumerate(row): + button_text = anchor_names.get(pos, pos) + if imgui.button(f"{button_text}##popup_{pos}", (50, 25)): + if hasattr(self, '_temp_selected_index_for_anchor'): + selected_idx = self._temp_selected_index_for_anchor + # Reset manual offset by passing (0,0) explicitly to match original behavior logic + self._update_component_anchor_position(selected_idx, pos, manual_offset=(0.0, 0.0)) + + # Clean up temp attribute + delattr(self, '_temp_selected_index_for_anchor') + + # Reset flag if used elsewhere + self.anchor_popup_open = False + imgui.close_current_popup() + + if i < len(row) - 1: + imgui.same_line() + + imgui.spacing() + if imgui.button("取消", (160, 25)): + self.anchor_popup_open = False + if hasattr(self, '_temp_selected_index_for_anchor'): + delattr(self, '_temp_selected_index_for_anchor') + imgui.close_current_popup() + + imgui.end_popup() \ No newline at end of file diff --git a/ui/panels/__init__.py b/ui/panels/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui/panels/animation_tools.py b/ui/panels/animation_tools.py new file mode 100644 index 00000000..5d295af8 --- /dev/null +++ b/ui/panels/animation_tools.py @@ -0,0 +1,434 @@ +import os +from pathlib import Path + +from direct.actor.Actor import Actor +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import NodePath + + +class AnimationTools: + """Animation and actor helper methods extracted from main world.""" + + def __init__(self, app): + self.app = app + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + + def _getActor(self, origin_model): + """ + 获取或创建模型的Actor,用于动画控制 + 复用Qt版本经过验证的实现方式 + """ + # 检查缓存 + if origin_model in self._actor_cache: + return self._actor_cache[origin_model] + + # 尝试直接从内存创建 + if origin_model.hasTag("can_create_actor_from_memory"): + try: + test_actor = Actor(origin_model) + anims = test_actor.getAnimNames() + self._actor_cache[origin_model] = test_actor + print(f"[Actor加载] 内存创建检测到动画: {anims}") + if anims: + return test_actor + else: + test_actor.cleanup() + test_actor.removeNode() + except Exception as e: + print(f"从内存模型创建Actor失败: {e}") + + # 如果不能直接从内存创建,再尝试通过文件路径加载 + filepath = origin_model.getTag("model_path") + if not filepath: + return None + + print(f"[Actor加载] 尝试加载: {filepath}") + + # 处理跨平台路径问题 + import os + # 检查路径是否有效,如果无效则尝试修复 + if not os.path.exists(filepath): + original_filepath = filepath + # 尝试多种修复策略 + fixed = False + + import platform + # 策略1: 处理Linux风格路径在Windows上的问题 + if filepath.startswith('/') and platform.system() == "Windows": + print("[路径转换] 尝试处理Linux风格路径:", filepath) + path_parts = filepath.split('/') + print(platform.system()) + if len(path_parts) > 1: + drive_letter = path_parts[1].upper() + ':\\' # 添加反斜杠确保正确路径格式 + remaining_path = '\\'.join(path_parts[2:]) if len(path_parts) > 2 else '' + potential_path = os.path.join(drive_letter, remaining_path) + print(f"[路径转换] 构造的潜在路径: {potential_path}") + if os.path.exists(potential_path): + filepath = potential_path + fixed = True + print(f"[路径转换] 成功: {original_filepath} -> {filepath}") + else: + print(f"[路径转换] 文件不存在: {potential_path}") + + + # 策略2: 处理路径分隔符问题 + if not fixed: + # 尝试规范化路径 + normalized_path = os.path.normpath(filepath) + print(f"[路径规范化] 尝试规范化路径: {filepath} -> {normalized_path}") + if os.path.exists(normalized_path): + filepath = normalized_path + fixed = True + print(f"[路径规范化] 成功: {filepath}") + else: + print(f"[路径规范化] 文件不存在: {normalized_path}") + + # 策略3: 在Resources目录中查找 + if not fixed: + # 尝试在Resources目录中查找文件 + resources_path = str(Path(__file__).resolve().parents[2] / "Resources") + filename = os.path.basename(filepath) + potential_path = os.path.join(resources_path, filename) + print(f"[Resources查找] 尝试在Resources目录查找: {potential_path}") + if os.path.exists(potential_path): + filepath = potential_path + fixed = True + print(f"[Resources查找] 成功: {filepath}") + else: + print(f"[Resources查找] 文件不存在: {potential_path}") + + if fixed: + print(f"路径修复: {original_filepath} -> {filepath}") + # 更新模型标签 + origin_model.setTag("model_path", filepath) + else: + print(f"[警告] 模型文件不存在: {filepath}") + return None + + # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 + if filepath.lower().endswith('.fbx'): + pass + #return self._createFBXActor(origin_model, filepath) + + # 其他格式使用标准 Actor 加载 + try: + import gltf + from panda3d.core import Filename + + # 将Panda3D路径转换为操作系统特定路径 + panda_filename = Filename(filepath) + os_specific_path = panda_filename.to_os_specific() + print(f"[路径转换] {filepath} -> {os_specific_path}") + + print(f"[GLTF加载] 尝试加载: {os_specific_path}") + + # 使用明确的设置确保动画被加载 + gltf_settings = gltf.GltfSettings(skip_animations=False) + model_root = gltf.load_model(os_specific_path, gltf_settings) + model_node = NodePath(model_root) + test_actor = Actor(model_node) + anims = test_actor.getAnimNames() + test_actor.reparentTo(self.render) + self._actor_cache[origin_model] = test_actor + print(f"[Actor加载] 标准加载检测到动画: {anims}") + if not anims: + test_actor.cleanup() + test_actor.removeNode() + return None + return test_actor + except Exception as e: + print(f"创建Actor失败: {e}") + return None + + + def _getModelFormat(self, origin_model): + """获取模型格式信息""" + filepath = origin_model.getTag("model_path") + original_path = origin_model.getTag("original_path") + converted_from = origin_model.getTag("converted_from") + + if filepath: + ext = filepath.lower().split('.')[-1] + format_name = ext.upper() + + # 如果是转换后的文件,显示转换信息 + if converted_from and original_path: + original_ext = converted_from.upper() + format_name = f"{format_name} (从{original_ext}转换)" + + return format_name + return "未知" + + def _processAnimationNames(self, origin_model, anim_names): + """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]""" + format_info = self._getModelFormat(origin_model) + processed = [] + + print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + + for name in anim_names: + display_name = name + original_name = name + + if format_info == "GLB": + # GLB 格式通常有真实的动画名称 + if "|" in name: + # 处理类似 'Armature|mixamo.com|Layer0' 的名称 + parts = name.split("|") + if "mixamo" in name.lower(): + # Mixamo 动画 + display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name + elif len(parts) > 2: + # 其他复杂命名 + display_name = f"{parts[0]}_{parts[-1]}" + else: + display_name = parts[-1] + + elif format_info == "FBX": + # FBX 格式可能需要特殊处理 + if self._isLikelyBoneGroup(name): + # 检查是否是骨骼组而非动画 + print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组") + display_name = f"⚠️ {name} (可能非动画)" + else: + display_name = name + + elif format_info in ["EGG", "BAM"]: + # 原生格式通常命名规范 + display_name = name + + processed.append((display_name, original_name)) + print(f"[动画分析] {original_name} → {display_name}") + + return processed + + def _isLikelyBoneGroup(self, name): + """判断动画名称是否更像骨骼组而不是动画序列""" + bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig'] + name_lower = name.lower() + + # 如果包含这些关键词,可能是骨骼组 + for indicator in bone_indicators: + if indicator in name_lower: + return True + + # 如果名称太简单(少于3个字符),可能不是动画 + if len(name) < 3: + return True + + return False + + def _analyzeAnimationQuality(self, actor, anim_names, format_info): + """分析动画质量和类型(优化版本,减少详细分析以提高性能)""" + try: + valid_anims = 0 + + # 简化分析:只检查动画是否存在,不详细分析帧数 + for anim_name in anim_names: + try: + control = actor.getAnimControl(anim_name) + if control and control.getNumFrames() > 1: + valid_anims += 1 + except Exception: + # 忽略单个动画的分析错误,继续处理其他动画 + continue + + if valid_anims == 0: + return "⚠️ 无有效动画" + elif valid_anims < len(anim_names): + return f"⚠️ {valid_anims}/{len(anim_names)} 个有效" + else: + return f"✓ {valid_anims} 个动画" + + except Exception as e: + # 简化错误处理 + return "分析异常" + + + def _playAnimation(self, origin_model): + """播放动画""" + actor = self._getActor(origin_model) + if not actor: + return + + # 保存原始世界坐标 + original_world_pos = origin_model.getPos(self.render) + original_world_hpr = origin_model.getHpr(self.render) + original_world_scale = origin_model.getScale(self.render) + + # 设置Actor位置和姿态 + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + # 隐藏原始模型,显示Actor + origin_model.hide() + actor.show() + + # 创建任务来维持世界坐标不变 + def maintainWorldPosition(task): + try: + if not actor.isEmpty(): + actor.setPos(self.render, original_world_pos) + actor.setHpr(self.render, original_world_hpr) + actor.setScale(self.render, original_world_scale) + return task.cont + else: + return task.done + except: + return task.done + + # 添加维持位置的任务 + taskMgr.add(maintainWorldPosition, f"maintain_anim_pos_{id(actor)}") + + # 获取当前选中的动画 + current_anim = origin_model.getPythonTag("selected_animation") + if current_anim: + actor.play(current_anim) + print(f"『动画播放』:{current_anim}") + else: + # 兜底:使用第一个可用动画 + anim_names = actor.getAnimNames() + if anim_names: + actor.play(anim_names[0]) + print(f"『动画播放』:{anim_names[0]}") + + def _pauseAnimation(self, origin_model): + """暂停动画""" + actor = self._getActor(origin_model) + if not actor: + return + + # 设置Actor位置和姿态 + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + # 隐藏原始模型,显示Actor + origin_model.hide() + actor.show() + + # 停止动画(保持当前姿势) + actor.stop() + print("『动画』暂停") + + def _stopAnimation(self, origin_model): + """停止动画""" + actor = self._getActor(origin_model) + if not actor: + return + + # 停止动画 + actor.stop() + + # 获取当前选中的动画 + current_anim = origin_model.getPythonTag("selected_animation") + if current_anim and actor.getAnimControl(current_anim): + actor.getAnimControl(current_anim).pose(0) + + # 隐藏Actor,显示原始模型 + actor.hide() + origin_model.show() + + # 移除维持位置的任务 + taskMgr.remove(f"maintain_anim_pos_{id(actor)}") + + print("『动画』停止切换至原始模型") + + def _loopAnimation(self, origin_model): + """循环播放动画""" + actor = self._getActor(origin_model) + if not actor: + return + + # 保存原始世界坐标 + original_world_pos = origin_model.getPos(self.render) + original_world_hpr = origin_model.getHpr(self.render) + original_world_scale = origin_model.getScale(self.render) + + # 设置Actor位置和姿态 + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + # 隐藏原始模型,显示Actor + origin_model.hide() + actor.show() + + # 创建任务来维持世界坐标不变 + def maintainWorldPosition(task): + try: + if not actor.isEmpty(): + actor.setPos(self.render, original_world_pos) + actor.setHpr(self.render, original_world_hpr) + actor.setScale(self.render, original_world_scale) + return task.cont + else: + return task.done + except: + return task.done + + # 添加维持位置的任务 + taskMgr.add(maintainWorldPosition, f"maintain_anim_pos_{id(actor)}") + + # 获取当前选中的动画 + current_anim = origin_model.getPythonTag("selected_animation") + if current_anim: + actor.loop(current_anim) + print(f"[动画] 循环: {current_anim}") + else: + # 兜底:使用第一个可用动画 + anim_names = actor.getAnimNames() + if anim_names: + actor.loop(anim_names[0]) + print(f"[动画] 循环: {anim_names[0]}") + + def _setAnimationSpeed(self, origin_model, speed): + """设置动画播放速度""" + actor = self._getActor(origin_model) + if not actor: + return + + # 获取当前选中的动画 + current_anim = origin_model.getPythonTag("selected_animation") + if current_anim: + actor.setPlayRate(speed, current_anim) + print(f"[动画] 速度设为: {speed} ({current_anim})") + else: + # 兜底:尝试所有动画 + anim_names = actor.getAnimNames() + for anim_name in anim_names: + actor.setPlayRate(speed, anim_name) + print(f"[动画] 速度设为: {speed} (所有动画)") + + + def _clear_animation_cache(self, node): + """清除节点的动画缓存,当模型发生变化时调用""" + node.setPythonTag("cached_anim_info", None) + node.setPythonTag("cached_processed_names", None) + node.setPythonTag("animation", None) # 同时清除动画检测结果 + + # 如果Actor在缓存中,也需要清理 + if node in self._actor_cache: + actor = self._actor_cache[node] + try: + # 清理相关任务 + taskMgr.remove(f"maintain_anim_pos_{id(actor)}") + # 清理Actor + if not actor.isEmpty(): + actor.cleanup() + actor.removeNode() + except Exception as e: + print(f"清理Actor缓存失败: {e}") + finally: + del self._actor_cache[node] + print(f"[缓存清理] 清除节点 {node.getName()} 的动画缓存") diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py new file mode 100644 index 00000000..aeb64a1d --- /dev/null +++ b/ui/panels/app_actions.py @@ -0,0 +1,1113 @@ +import os + +from imgui_bundle import imgui +from direct.showbase.ShowBaseGlobal import globalClock +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import WindowProperties + + +class AppActions: + """Project, input, messaging, edit actions, and import workflows.""" + + def __init__(self, app): + self.app = app + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + + def _toggle_hot_reload(self): + """切换热重载状态""" + if hasattr(self, 'script_manager') and self.script_manager: + try: + current_state = getattr(self.script_manager, 'hot_reload_enabled', False) + self.script_manager.hot_reload_enabled = not current_state + + new_state = "启用" if not current_state else "禁用" + self.add_success_message(f"热重载已{new_state}") + print(f"[脚本系统] 热重载已{new_state}") + except Exception as e: + self.add_error_message(f"切换热重载失败: {str(e)}") + print(f"[脚本系统] 切换热重载失败: {e}") + + + def _create_new_script(self): + """创建新脚本""" + if not hasattr(self, '_new_script_name') or not self._new_script_name.strip(): + self.add_error_message("请输入脚本名称") + return + + script_name = self._new_script_name.strip() + if not script_name.endswith('.py'): + script_name += '.py' + + # 确定模板类型 + template_map = { + 0: "basic", + 1: "movement", + 2: "rotation", + 3: "scale", + 4: "animation" + } + template_type = template_map.get(getattr(self, '_selected_template', 0), "basic") + + try: + if hasattr(self, 'script_manager') and self.script_manager: + result = self.script_manager.create_script_file(script_name, template_type) + if result: + self.add_success_message(f"脚本 {script_name} 创建成功") + print(f"[脚本系统] 创建脚本成功: {script_name}") + # 刷新脚本列表 + self._refresh_scripts_list() + else: + self.add_error_message(f"脚本 {script_name} 创建失败") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"创建脚本失败: {str(e)}") + print(f"[脚本系统] 创建脚本失败: {e}") + + + def _refresh_scripts_list(self): + """刷新脚本列表""" + try: + if hasattr(self, 'script_manager') and self.script_manager: + # 这里可以添加缓存逻辑,避免频繁刷新 + available_scripts = self.script_manager.get_available_scripts() + print(f"[脚本系统] 刷新脚本列表: {len(available_scripts)} 个脚本") + self.add_success_message(f"脚本列表已刷新,共 {len(available_scripts)} 个脚本") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"刷新脚本列表失败: {str(e)}") + print(f"[脚本系统] 刷新脚本列表失败: {e}") + + + def _reload_all_scripts(self): + """重载所有脚本""" + try: + if hasattr(self, 'script_manager') and self.script_manager: + # 获取所有可用脚本并逐个重载 + available_scripts = self.script_manager.get_available_scripts() + success_count = 0 + + for script_name in available_scripts: + if self.script_manager.reload_script(script_name): + success_count += 1 + + self.add_success_message(f"重载完成: {success_count}/{len(available_scripts)} 个脚本成功") + print(f"[脚本系统] 重载脚本: {success_count}/{len(available_scripts)} 成功") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"重载脚本失败: {str(e)}") + print(f"[脚本系统] 重载脚本失败: {e}") + + + def _on_script_selected(self, script_name): + """处理脚本选择事件""" + print(f"[脚本系统] 选择脚本: {script_name}") + self.add_info_message(f"已选择脚本: {script_name}") + + + def _edit_script(self, script_name): + """编辑脚本""" + try: + if hasattr(self, 'script_manager') and self.script_manager: + # 获取脚本信息 + script_info = self.script_manager.get_script_info(script_name) + if script_info and script_info.get("file"): + script_path = script_info["file"] + + # 打开系统默认编辑器 + import subprocess + import platform + + system = platform.system() + try: + if system == "Windows": + subprocess.run(['notepad', script_path]) + elif system == "Darwin": # macOS + subprocess.run(['open', script_path]) + else: # Linux + subprocess.run(['xdg-open', script_path]) + + self.add_success_message(f"已打开脚本编辑器: {script_name}") + print(f"[脚本系统] 编辑脚本: {script_path}") + except Exception as e: + self.add_error_message(f"打开编辑器失败: {str(e)}") + else: + self.add_error_message(f"找不到脚本文件: {script_name}") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"编辑脚本失败: {str(e)}") + print(f"[脚本系统] 编辑脚本失败: {e}") + + + def _mount_script_to_selected(self, script_name): + """挂载脚本到选中对象""" + selected_node = None + if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): + selected_node = self.selection.selectedNode + + if not selected_node or selected_node.isEmpty(): + self.add_error_message("请先选择一个对象") + return + + try: + if hasattr(self, 'script_manager') and self.script_manager: + script_component = self.script_manager.add_script_to_object(selected_node, script_name) + if script_component: + self.add_success_message(f"脚本 {script_name} 已挂载到 {selected_node.getName()}") + print(f"[脚本系统] 挂载脚本: {script_name} -> {selected_node.getName()}") + else: + self.add_error_message(f"挂载脚本 {script_name} 失败") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"挂载脚本失败: {str(e)}") + print(f"[脚本系统] 挂载脚本失败: {e}") + + + def _unmount_script_from_selected(self, script_name): + """从选中对象卸载脚本""" + selected_node = None + if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): + selected_node = self.selection.selectedNode + + if not selected_node or selected_node.isEmpty(): + self.add_error_message("请先选择一个对象") + return + + try: + if hasattr(self, 'script_manager') and self.script_manager: + result = self.script_manager.remove_script_from_object(selected_node, script_name) + if result: + self.add_success_message(f"脚本 {script_name} 已从 {selected_node.getName()} 卸载") + print(f"[脚本系统] 卸载脚本: {script_name} <- {selected_node.getName()}") + else: + self.add_error_message(f"卸载脚本 {script_name} 失败") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"卸载脚本失败: {str(e)}") + print(f"[脚本系统] 卸载脚本失败: {e}") + + # ==================== 菜单处理函数 ==================== + + + def _on_new_project(self): + """处理新建项目菜单项""" + self.add_info_message("打开新建项目对话框") + self.show_new_project_dialog = True + + + def _on_open_project(self): + """处理打开项目菜单项""" + self.add_info_message("打开项目对话框") + self.show_open_project_dialog = True + + + def _on_save_project(self): + """处理保存项目菜单项""" + if hasattr(self, 'project_manager') and self.project_manager: + try: + # 检查是否有当前项目路径 + if not self.project_manager.current_project_path: + self.add_warning_message("没有当前项目路径,请先创建或打开项目") + self.show_save_as_dialog = True + return + + # 直接调用保存逻辑,避免Qt依赖 + if self._save_project_impl(): + self.add_success_message("项目保存成功") + else: + self.add_error_message("项目保存失败") + except Exception as e: + self.add_error_message(f"项目保存失败: {e}") + else: + self.add_error_message("项目管理器未初始化") + + + def _on_save_as_project(self): + """处理另存为项目菜单项""" + self.add_info_message("另存为项目(功能待实现)") + # TODO: 实现另存为对话框 + # self.show_save_as_dialog = True + + + def _on_exit(self): + """处理退出菜单项""" + self.add_info_message("退出应用程序") + self.userExit() + + # ==================== 键盘事件处理函数 ==================== + + + def _on_ctrl_pressed(self): + """Ctrl键按下""" + self.ctrl_pressed = True + + + def _on_ctrl_released(self): + """Ctrl键释放""" + self.ctrl_pressed = False + + + def _on_alt_pressed(self): + """Alt键按下""" + self.alt_pressed = True + + + def _on_alt_released(self): + """Alt键释放""" + self.alt_pressed = False + + + def _on_n_pressed(self): + """N键按下 - 检查Ctrl+N组合键""" + if self.ctrl_pressed: + self._on_new_project() + + + def _on_o_pressed(self): + """O键按下 - 检查Ctrl+O组合键""" + if self.ctrl_pressed: + self._on_open_project() + + + + + def _on_f4_pressed(self): + """F4键按下 - 检查Alt+F4组合键""" + if self.alt_pressed: + self._on_exit() + + # 移除了单独的按键处理方法,现在直接使用组合键事件 + + + def _on_delete_pressed(self): + """Delete键按下 - 删除选中节点""" + self._on_delete() + + def _on_escape_pressed(self): + """Escape键按下 - 取消所有选区""" + # 1. 取消 LUI 组件选择 + if hasattr(self, 'lui_manager'): + if self.lui_manager.selected_index >= 0: + self.lui_manager.selected_index = -1 + print("✓ 已取消LUI组件选中") + + # 2. 取消 3D 场景选择 + if hasattr(self, 'selection') and self.selection: + if self.selection.selectedNode: + self.selection.selectedNode = None + self.selection.clearSelectionBox() + self.selection.clearGizmo() + print("✓ 已取消场景节点选中") + + + def _on_wheel_up(self): + """滚轮向上滚动 - 相机前进""" + try: + if not self.camera_control_enabled: + return + + # 检查鼠标是否在ImGui窗口上 + if self._is_mouse_over_imgui(): + return + + # 沿相机前向向量移动 + forward = self.camera.getMat().getRow3(1) + distance = 20.0 * globalClock.getDt() + currentPos = self.camera.getPos() + newPos = currentPos + forward * distance + self.camera.setPos(newPos) + except Exception as e: + print(f"滚轮前进失败: {e}") + + + def _on_wheel_down(self): + """滚轮向下滚动 - 相机后退""" + try: + # 检查鼠标是否在ImGui窗口上 + if self._is_mouse_over_imgui(): + return + + # 沿相机前向向量移动 + forward = self.camera.getMat().getRow3(1) + distance = -20.0 * globalClock.getDt() + currentPos = self.camera.getPos() + newPos = currentPos + forward * distance + self.camera.setPos(newPos) + except Exception as e: + print(f"滚轮后退失败: {e}") + + + def _is_mouse_over_imgui(self): + """检测鼠标是否在ImGui窗口上""" + try: + # 检查是否有任何ImGui窗口想要捕获鼠标 + if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse: + return True + + # 检查鼠标是否在任何ImGui窗口内 + mouse_pos = self.mouseWatcherNode.getMouse() + if not mouse_pos: + return False + + # 简单的边界检查(可以根据需要扩展) + display_size = imgui.get_io().display_size + mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2 + mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2) + + # 检查是否在常见的ImGui界面区域内 + # 这里可以根据实际的ImGui窗口位置进行更精确的检测 + if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏) + return True + if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏) + return True + if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域 + return True + + return False + except Exception as e: + print(f"ImGui界面检测失败: {e}") + return False + + + def processImGuiMouseClick(self, x, y): + """处理ImGui鼠标点击事件,返回是否消费了该事件""" + try: + # ImGui优先策略:如果ImGui想要捕获鼠标,则由ImGui处理 + if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse: + return True + + # 检查是否有任何ImGui窗口悬停 + try: + if imgui.is_any_window_hovered(): + return True + except AttributeError: + # 如果方法不存在,跳过这个检查 + pass + + # 检查鼠标是否在ImGui界面区域内 + if self._is_mouse_over_imgui(): + return True + + # 如果以上条件都不满足,则让3D场景处理该事件 + return False + + except Exception as e: + print(f"ImGui鼠标点击处理失败: {e}") + return False + + # ==================== 消息系统 ==================== + + + def add_message(self, text, color=(1.0, 1.0, 1.0, 1.0)): + """添加消息到消息列表,同时输出到终端""" + import datetime + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + + # 输出到终端 + print(f"[{timestamp}] {text}") + + # 添加到GUI消息列表 + self.messages.append({ + 'text': text, + 'color': color, + 'timestamp': timestamp + }) + + # 限制消息数量 + if len(self.messages) > self.max_messages: + self.messages = self.messages[-self.max_messages:] + + + def add_success_message(self, text): + """添加成功消息""" + self.add_message(f"✓ {text}", (0.176, 1.0, 0.769, 1.0)) + + + def add_error_message(self, text): + """添加错误消息""" + self.add_message(f"✗ {text}", (1.0, 0.3, 0.3, 1.0)) + + + def add_warning_message(self, text): + """添加警告消息""" + self.add_message(f"⚠ {text}", (0.953, 0.616, 0.471, 1.0)) + + + def add_info_message(self, text): + """添加信息消息""" + self.add_message(f"ℹ {text}", (0.157, 0.620, 1.0, 1.0)) + + # ==================== 编辑菜单功能实现 ==================== + + + def _on_undo(self): + """处理撤销操作""" + try: + # 1) 优先使用命令系统(删除/创建等命令) + if hasattr(self, 'command_manager') and self.command_manager and self.command_manager.can_undo(): + success = self.command_manager.undo() + if success: + self.add_success_message("撤销操作成功") + return + self.add_error_message("撤销操作失败") + return + + # 2) 回退到 TransformGizmo 历史(拖拽位移/旋转/缩放) + tg = getattr(self, 'newTransform', None) + if tg and hasattr(tg, 'undo_last') and tg.undo_last(): + self.add_success_message("撤销操作成功") + return + + self.add_warning_message("没有可撤销的操作") + except Exception as e: + self.add_error_message(f"撤销操作失败: {e}") + + + def _on_redo(self): + """处理重做操作""" + try: + # 1) 优先使用命令系统 + if hasattr(self, 'command_manager') and self.command_manager and self.command_manager.can_redo(): + success = self.command_manager.redo() + if success: + self.add_success_message("重做操作成功") + return + self.add_error_message("重做操作失败") + return + + # 2) 回退到 TransformGizmo 重做栈 + tg = getattr(self, 'newTransform', None) + if tg and hasattr(tg, 'redo_last') and tg.redo_last(): + self.add_success_message("重做操作成功") + return + + self.add_warning_message("没有可重做的操作") + except Exception as e: + self.add_error_message(f"重做操作失败: {e}") + + + def _on_copy(self): + """处理复制操作""" + try: + if not hasattr(self, 'selection') or not self.selection: + self.add_error_message("选择系统未初始化") + return + + # 获取当前选中的节点 + selected_node = self.selection.selectedNode + if not selected_node: + self.add_warning_message("没有选中的节点") + return + + # 检查节点有效性(不能复制根节点) + if selected_node.getName() == "render": + self.add_warning_message("不能复制根节点") + return + + # 序列化节点 + if hasattr(self, 'scene_manager') and self.scene_manager: + node_data = self.scene_manager.serializeNodeForCopy(selected_node) + if node_data: + self.clipboard = [node_data] + # Keep live source nodes for robust copy->paste clone path. + self.clipboard_source_nodes = [selected_node] + self.clipboard_mode = "copy" + self.add_success_message(f"已复制节点: {selected_node.getName()}") + else: + self.add_error_message("节点序列化失败") + else: + self.add_error_message("场景管理器未初始化") + except Exception as e: + self.add_error_message(f"复制操作失败: {e}") + + + def _on_cut(self): + """处理剪切操作""" + try: + if not hasattr(self, 'selection') or not self.selection: + self.add_error_message("选择系统未初始化") + return + + # 获取当前选中的节点 + selected_node = self.selection.selectedNode + if not selected_node: + self.add_warning_message("没有选中的节点") + return + + # 检查节点有效性(不能剪切根节点和系统节点) + node_name = selected_node.getName() + if node_name == "render": + self.add_warning_message("不能剪切根节点") + return + + # 序列化节点 + if hasattr(self, 'scene_manager') and self.scene_manager: + node_data = self.scene_manager.serializeNodeForCopy(selected_node) + if node_data: + self.clipboard = [node_data] + # Cut uses serialized restore path after original deletion. + self.clipboard_source_nodes = [] + self.clipboard_mode = "cut" + + # 删除原节点 + self._delete_node(selected_node) + self.selection.clearSelection() + + self.add_success_message(f"已剪切节点: {node_name}") + else: + self.add_error_message("节点序列化失败") + else: + self.add_error_message("场景管理器未初始化") + except Exception as e: + self.add_error_message(f"剪切操作失败: {e}") + + + def _on_paste(self): + """处理粘贴操作""" + try: + if not self.clipboard: + self.add_warning_message("剪切板为空") + return + + if not hasattr(self, 'scene_manager') or not self.scene_manager: + self.add_error_message("场景管理器未初始化") + return + + # 确定粘贴目标父节点 + parent_node = None + if hasattr(self, 'selection') and self.selection: + selected_node = self.selection.selectedNode + if selected_node and not selected_node.isEmpty(): + # Paste as sibling by default (not as child of selected node), + # which matches editor expectations and avoids "pasted but invisible". + if selected_node.getName() == "render": + parent_node = selected_node + else: + p = selected_node.getParent() + parent_node = p if p and not p.isEmpty() else self.render + + # 如果没有选中节点,使用渲染根节点 + if not parent_node: + parent_node = self.render + + # 反序列化并添加节点 + created_any = False + source_nodes = getattr(self, "clipboard_source_nodes", []) or [] + for i, node_data in enumerate(self.clipboard): + new_node = None + + # Copy mode: prefer direct NodePath clone to preserve visual geometry. + if self.clipboard_mode == "copy" and i < len(source_nodes): + source_node = source_nodes[i] + if source_node and not source_node.isEmpty(): + try: + new_node = source_node.copyTo(parent_node) + if hasattr(self.scene_manager, "_generateUniqueName"): + unique_name = self.scene_manager._generateUniqueName(source_node.getName(), parent_node) + new_node.setName(unique_name) + # Offset slightly so the new node can be seen immediately. + new_node.setPos(new_node.getX() + 0.2, new_node.getY() + 0.2, new_node.getZ()) + except Exception: + new_node = None + + # Fallback: recreate from serialized data. + if not new_node: + if hasattr(self.scene_manager, "recreateNodeFromData"): + new_node = self.scene_manager.recreateNodeFromData(node_data, parent_node) + else: + new_node = self.scene_manager.deserializeNode(node_data, parent_node) + if new_node: + created_any = True + # Ensure pasted model can be picked by legacy collision fallback. + try: + if hasattr(self, "scene_manager") and self.scene_manager: + if not new_node.hasTag("tree_item_type"): + new_node.setTag("tree_item_type", "IMPORTED_MODEL_NODE") + new_node.setTag("is_model_root", "1") + new_node.setTag("is_scene_element", "1") + self.scene_manager.setupCollision(new_node) + if hasattr(self.scene_manager, "models") and new_node not in self.scene_manager.models: + self.scene_manager.models.append(new_node) + except Exception as _e: + print(f"[Paste] setup collision for pasted node failed: {_e}") + self.add_success_message(f"已粘贴节点: {new_node.getName()}") + else: + self.add_error_message("节点反序列化失败") + + # 如果是剪切模式且粘贴成功,清空剪切板 + if created_any and self.clipboard_mode == "cut": + self.clipboard = [] + self.clipboard_source_nodes = [] + self.clipboard_mode = "" + except Exception as e: + self.add_error_message(f"粘贴操作失败: {e}") + + + def _on_delete(self): + """处理删除操作""" + try: + if not hasattr(self, 'selection') or not self.selection: + self.add_error_message("选择系统未初始化") + return + + # 获取当前选中的节点 + selected_node = self.selection.selectedNode + if not selected_node: + self.add_warning_message("没有选中的节点") + return + + # 检查节点有效性(不能删除根节点) + node_name = selected_node.getName() + if node_name == "render": + self.add_warning_message("不能删除根节点") + return + + # 删除节点 + if hasattr(self, 'scene_manager') and self.scene_manager: + self._delete_node(selected_node) + self.selection.clearSelection() + self.add_success_message(f"已删除节点: {node_name}") + else: + self.add_error_message("场景管理器未初始化") + except Exception as e: + self.add_error_message(f"删除操作失败: {e}") + + + def _delete_node(self, node): + """删除节点的通用方法 - 使用命令系统""" + try: + if not node or node.isEmpty(): + return False + + node_name = node.getName() or "未命名节点" + parent = node.getParent() + + # 创建删除命令 + if hasattr(self, 'command_manager') and self.command_manager: + from core.Command_System import DeleteNodeCommand + command = DeleteNodeCommand(node, parent, self) + self.command_manager.execute_command(command) + print(f"[命令系统] 创建删除命令: {node_name}") + else: + # 备用方案:直接删除并执行清理 + print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}") + self._perform_node_cleanup(node) + node.removeNode() + + print(f"[删除] 成功删除节点: {node_name}") + return True + + except Exception as e: + print(f"[删除] 删除节点失败: {e}") + return False + + + def _perform_node_cleanup(self, node): + """执行节点清理逻辑""" + try: + node_name = node.getName() or "未命名节点" + + # 从场景管理器的模型列表中移除(如果是模型) + if hasattr(self, 'scene_manager') and self.scene_manager: + if node in self.scene_manager.models: + self.scene_manager.models.remove(node) + print(f"[场景管理器] 从模型列表移除: {node_name}") + + # 停止所有与该节点相关的脚本 + if hasattr(self, 'script_manager') and self.script_manager: + try: + # 移除该节点上的所有脚本 + if node in self.script_manager.object_scripts: + del self.script_manager.object_scripts[node] + print(f"[脚本系统] 移除节点 {node_name} 的所有脚本") + except Exception as e: + print(f"[脚本系统] 移除脚本失败: {e}") + + # 清理碰撞体 + if hasattr(self, 'collision_manager') and self.collision_manager: + try: + self.collision_manager.remove_collision_for_node(node) + print(f"[碰撞系统] 移除节点 {node_name} 的碰撞体") + except Exception as e: + print(f"[碰撞系统] 移除碰撞体失败: {e}") + + # 清理Actor缓存(如果有动画) + if hasattr(self, '_actor_cache') and node in self._actor_cache: + actor = self._actor_cache[node] + try: + # 清理相关任务 + taskMgr.remove(f"maintain_anim_pos_{id(actor)}") + # 清理Actor + if not actor.isEmpty(): + actor.cleanup() + actor.removeNode() + print(f"[动画系统] 清理节点 {node_name} 的Actor缓存") + except Exception as e: + print(f"[动画系统] 清理Actor缓存失败: {e}") + finally: + del self._actor_cache[node] + + except Exception as e: + print(f"[清理] 节点清理失败: {e}") + + # ==================== 对话框绘制函数 ==================== + + + def _create_new_project(self, name, path): + """创建新项目的实际实现""" + if not hasattr(self, 'project_manager') or not self.project_manager: + print("✗ 项目管理器未初始化") + return + + try: + if self._create_new_project_impl(name, path): + print(f"✓ 项目创建成功: {name}") + else: + print(f"✗ 项目创建失败: {name}") + except Exception as e: + print(f"✗ 项目创建失败: {e}") + + + def _open_project_path(self, path): + """打开项目的实际实现""" + if not hasattr(self, 'project_manager') or not self.project_manager: + print("✗ 项目管理器未初始化") + return + + try: + print(f"打开项目: {path}") + if self._open_project_impl(path): + print(f"✓ 项目打开成功: {path}") + else: + print(f"✗ 项目打开失败: {path}") + except Exception as e: + print(f"✗ 项目打开失败: {e}") + + # ==================== 项目管理具体实现 ==================== + + + def _save_project_impl(self): + """保存项目的具体实现(不依赖Qt)""" + import json + import datetime + import os + + project_path = self.project_manager.current_project_path + scenes_path = os.path.join(project_path, "scenes") + + # 固定的场景文件名 + scene_file = os.path.join(scenes_path, "scene.bam") + + # 如果存在旧文件,先删除 + if os.path.exists(scene_file): + try: + os.remove(scene_file) + print(f"已删除旧场景文件: {scene_file}") + except Exception as e: + print(f"删除旧场景文件失败: {str(e)}") + return False + + # 保存场景 + if self.scene_manager.saveScene(scene_file, project_path): + # 更新项目配置文件 + config_file = os.path.join(project_path, "project.json") + if os.path.exists(config_file): + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 更新最后修改时间 + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # 记录场景文件路径 + project_config["scene_file"] = os.path.relpath(scene_file, project_path) + + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + + # 更新项目配置 + self.project_manager.project_config = project_config + return True + return False + + + def _open_project_impl(self, project_path): + """打开项目的具体实现(不依赖Qt)""" + import json + import datetime + import os + + try: + # 检查项目管理器是否已初始化 + if not hasattr(self, 'project_manager') or not self.project_manager: + print("✗ 项目管理器未初始化") + self.add_error_message("项目管理器未初始化") + return False + + # 检查场景管理器是否已初始化 + if not hasattr(self, 'scene_manager') or not self.scene_manager: + print("✗ 场景管理器未初始化") + self.add_error_message("场景管理器未初始化") + return False + + # 检查是否是有效的项目文件夹 + config_file = os.path.join(project_path, "project.json") + if not os.path.exists(config_file): + print(f"⚠ 选择的不是有效的项目文件夹: {project_path}") + self.add_warning_message(f"选择的不是有效的项目文件夹: {project_path}") + return False + + # 读取项目配置 + try: + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + except Exception as e: + print(f"✗ 读取项目配置文件失败: {e}") + self.add_error_message(f"读取项目配置文件失败: {e}") + return False + + # 检查场景文件 + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if os.path.exists(scene_file): + # 加载场景 + try: + if self.scene_manager.loadScene(scene_file): + # 更新项目配置 + project_config["scene_file"] = os.path.relpath(scene_file, project_path) + print(f"✓ 场景加载成功: {scene_file}") + else: + print(f"⚠ 场景加载失败: {scene_file}") + self.add_warning_message(f"场景加载失败: {scene_file}") + except Exception as e: + print(f"✗ 加载场景时发生错误: {e}") + self.add_error_message(f"加载场景时发生错误: {e}") + # 继续执行,不阻止项目打开 + + # 更新项目配置 + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + try: + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + except Exception as e: + print(f"✗ 保存项目配置失败: {e}") + self.add_error_message(f"保存项目配置失败: {e}") + + # 更新项目状态 + self.project_manager.current_project_path = project_path + self.project_manager.project_config = project_config + + # 更新窗口标题 + project_name = os.path.basename(project_path) + self._update_window_title(project_name) + + print(f"✓ 项目打开成功: {project_path}") + self.add_success_message(f"项目打开成功: {project_name}") + return True + + except Exception as e: + print(f"✗ 打开项目时发生错误: {e}") + self.add_error_message(f"打开项目时发生错误: {e}") + return False + + + def _create_new_project_impl(self, name, path): + """创建新项目的具体实现(不依赖Qt)""" + import json + import datetime + import os + + full_project_path = os.path.normpath(os.path.join(path, name)) + print(f"创建项目路径: {full_project_path}") + + try: + # 创建项目文件夹结构 + os.makedirs(full_project_path) + os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹 + os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹 + scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹 + os.makedirs(scenes_path) + + # 创建项目配置文件 + project_config = { + "name": name, + "path": full_project_path, + "created": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "version": "1.0", + "scene_file": "scenes/scene.bam" + } + + # 保存项目配置 + config_file = os.path.join(full_project_path, "project.json") + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + + # 保存初始场景 + scene_file = os.path.join(scenes_path, "scene.bam") + self.scene_manager.saveScene(scene_file, full_project_path) + + # 更新项目管理器状态 + self.project_manager.current_project_path = full_project_path + self.project_manager.project_config = project_config + + # 更新窗口标题 + self._update_window_title(name) + + return True + + except Exception as e: + print(f"创建项目失败: {e}") + return False + + + def _update_window_title(self, project_name): + """更新窗口标题""" + try: + props = WindowProperties() + props.set_title(f"EG Engine - {project_name}") + self.win.request_properties(props) + print(f"窗口标题已更新: EG Engine - {project_name}") + except Exception as e: + print(f"更新窗口标题失败: {e}") + + # ==================== 路径浏览器辅助方法 ==================== + + + def _import_model_for_runtime(self, file_path): + """Import model through the active runtime path. + SSBO mode: load via SSBOEditor only (avoid duplicate SceneManager model). + Legacy mode: load via SceneManager. + """ + if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None): + try: + # Remove legacy scene-manager models to avoid duplicate rendering + if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'): + for m in list(self.scene_manager.models): + try: + if m and not m.isEmpty(): + m.removeNode() + except Exception: + pass + self.scene_manager.models = [] + + # Replace previous SSBO model + old_model = getattr(self.ssbo_editor, 'model', None) + if old_model is not None: + try: + if not old_model.isEmpty(): + old_model.removeNode() + except Exception: + pass + + self.ssbo_editor.load_model(file_path) + model_np = getattr(self.ssbo_editor, 'model', None) + # Keep legacy ray-pick fallback usable by adding a collision body. + if model_np and hasattr(self, 'scene_manager') and self.scene_manager: + try: + self.scene_manager.setupCollision(model_np) + except Exception as e: + print(f"[SSBO] setupCollision failed: {e}") + return model_np + except Exception as e: + print(f"[SSBO] load_model failed: {e}") + return None + + # Legacy fallback + if hasattr(self, 'scene_manager') and self.scene_manager: + return self.scene_manager.importModel(file_path) + return None + + def _on_import_model(self): + """处理导入模型菜单项""" + self.add_info_message("打开导入模型对话框") + self.show_import_dialog = True + + + def _import_model(self): + """导入模型的具体实现""" + try: + if not self.import_file_path: + self.add_error_message("请选择要导入的文件") + return + + if not os.path.exists(self.import_file_path): + self.add_error_message(f"文件不存在: {self.import_file_path}") + return + + # 检查文件格式 + file_ext = os.path.splitext(self.import_file_path)[1].lower() + if file_ext not in self.supported_formats: + self.add_error_message(f"不支持的文件格式: {file_ext}") + return + + # 调用场景管理器导入模型 + if hasattr(self, 'scene_manager') and self.scene_manager: + self.add_info_message(f"正在导入模型: {os.path.basename(self.import_file_path)}") + + # 导入模型 + model_node = self._import_model_for_runtime(self.import_file_path) + + if model_node: + # 添加材质处理确保颜色正常 + if hasattr(self.scene_manager, 'processMaterials'): + self.scene_manager.processMaterials(model_node) + self.add_info_message("已应用默认材质") + + # 额外的材质处理,确保颜色正确显示 + try: + # 强制刷新模型显示 + model_node.clearMaterial() + model_node.clearTexture() + + # 重新应用材质 + if hasattr(self.scene_manager, 'processMaterials'): + self.scene_manager.processMaterials(model_node) + + # 设置默认的基础颜色(如果模型没有颜色) + try: + color = model_node.getColor() + if color and len(color) >= 4 and color == (1, 1, 1, 1): # 默认白色 + model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰 + elif not color: # 如果没有颜色 + model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰 + except: + # 如果getColor失败,直接设置默认颜色 + model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰 + + except Exception as e: + self.add_warning_message(f"材质处理警告: {e}") + + # 设置模型位置 + model_node.setPos(0, 0, 0) + + # 添加到场景管理器的模型列表 + if hasattr(self.scene_manager, 'models'): + self.scene_manager.models.append(model_node) + + # 选中新导入的模型 + if hasattr(self, 'selection') and self.selection: + self.selection.updateSelection(model_node) + + self.add_success_message(f"模型导入成功: {os.path.basename(self.import_file_path)}") + else: + self.add_error_message("模型导入失败") + else: + self.add_error_message("场景管理器未初始化") + + except Exception as e: + self.add_error_message(f"导入模型失败: {e}") + + # 清空导入路径 + self.import_file_path = "" diff --git a/ui/panels/create_actions.py b/ui/panels/create_actions.py new file mode 100644 index 00000000..b1945a3f --- /dev/null +++ b/ui/panels/create_actions.py @@ -0,0 +1,215 @@ +class CreateActions: + """Create and script entry actions extracted from main world.""" + + def __init__(self, app): + self.app = app + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + + def _on_create_empty_object(self): + """创建空对象""" + try: + result = self.createEmptyObject() + if result: + self.add_success_message("空对象创建成功") + else: + self.add_error_message("空对象创建失败") + return result + except Exception as e: + self.add_error_message(f"创建空对象失败: {str(e)}") + + + def _on_create_cube(self): + """创建立方体""" + try: + result = self.createCube() + if result: + self.add_success_message("立方体创建成功") + else: + self.add_error_message("立方体创建失败") + return result + except Exception as e: + self.add_error_message(f"创建立方体失败: {str(e)}") + + + def _on_create_sphere(self): + """创建球体""" + try: + result = self.createSphere() + if result: + self.add_success_message("球体创建成功") + else: + self.add_error_message("球体创建失败") + return result + except Exception as e: + self.add_error_message(f"创建球体失败: {str(e)}") + + + def _on_create_cylinder(self): + """创建圆柱体""" + try: + result = self.createCylinder() + if result: + self.add_success_message("圆柱体创建成功") + else: + self.add_error_message("圆柱体创建失败") + return result + except Exception as e: + self.add_error_message(f"创建圆柱体失败: {str(e)}") + + + def _on_create_plane(self): + """创建平面""" + try: + result = self.createPlane() + if result: + self.add_success_message("平面创建成功") + else: + self.add_error_message("平面创建失败") + return result + except Exception as e: + self.add_error_message(f"创建平面失败: {str(e)}") + + + def _on_create_3d_text(self): + """创建3D文本""" + self.show_3d_text_dialog = True + + + def _on_create_3d_image(self): + """创建3D图片""" + self.show_3d_image_dialog = True + + + def _on_create_gui_button(self): + """创建GUI按钮""" + self.show_gui_button_dialog = True + + + def _on_create_gui_label(self): + """创建GUI标签""" + self.show_gui_label_dialog = True + + + def _on_create_gui_entry(self): + """创建GUI输入框""" + self.show_gui_entry_dialog = True + + + def _on_create_gui_image(self): + """创建GUI图片""" + self.show_gui_image_dialog = True + + + def _on_create_video_screen(self): + """创建视频屏幕""" + self.show_video_screen_dialog = True + + + def _on_create_2d_video_screen(self): + """创建2D视频屏幕""" + self.show_2d_video_screen_dialog = True + + + def _on_create_spherical_video(self): + """创建球形视频""" + self.show_spherical_video_dialog = True + + + def _on_create_virtual_screen(self): + """创建虚拟屏幕""" + self.show_virtual_screen_dialog = True + + + def _on_create_spot_light(self): + """创建聚光灯""" + self.show_spot_light_dialog = True + + + def _on_create_point_light(self): + """创建点光源""" + self.show_point_light_dialog = True + + + def _on_create_flat_terrain(self): + """创建平面地形""" + self.show_terrain_dialog = True + + + def _on_create_heightmap_terrain(self): + """从高度图创建地形""" + self.show_heightmap_browser = True + + + def _on_create_script(self): + """创建脚本""" + self.show_script_dialog = True + + + def _on_load_script(self): + """加载脚本文件""" + self.show_script_browser = True + + + def _on_reload_all_scripts(self): + """重载所有脚本""" + try: + if hasattr(self, 'script_manager') and self.script_manager: + self.script_manager.reloadAllScripts() + self.add_success_message("所有脚本重载成功") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"重载脚本失败: {str(e)}") + + + def _on_open_scripts_manager(self): + """打开脚本管理器""" + self.showScriptPanel = True + self.add_info_message("脚本管理器已打开") + + + def _on_create_2d_sample_panel(self): + """创建2D示例面板""" + try: + result = self.create2DSamplePanel() + if result: + self.add_success_message("2D示例面板创建成功") + else: + self.add_error_message("2D示例面板创建失败") + except Exception as e: + self.add_error_message(f"创建2D示例面板失败: {str(e)}") + + + def _on_create_3d_sample_panel(self): + """创建3D实例面板""" + try: + result = self.create3DSamplePanel() + if result: + self.add_success_message("3D实例面板创建成功") + else: + self.add_error_message("3D实例面板创建失败") + except Exception as e: + self.add_error_message(f"创建3D实例面板失败: {str(e)}") + + + def _on_create_web_panel(self): + """创建Web面板""" + try: + result = self.createWebPanel() + if result: + self.add_success_message("Web面板创建成功") + else: + self.add_error_message("Web面板创建失败") + except Exception as e: + self.add_error_message(f"创建Web面板失败: {str(e)}") + + # ==================== 3D对象和GUI创建方法 ==================== diff --git a/ui/panels/dialog_panels.py b/ui/panels/dialog_panels.py new file mode 100644 index 00000000..425a88d1 --- /dev/null +++ b/ui/panels/dialog_panels.py @@ -0,0 +1,1461 @@ +import os +from imgui_bundle import imgui, imgui_ctx + + +class DialogPanels: + """Project, import, and creation dialog panels.""" + + def __init__(self, app): + self.app = app + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + + def _draw_new_project_dialog(self): + """绘制新建项目对话框""" + if not self.show_new_project_dialog: + return + + # 初始化默认值 + if not hasattr(self, 'new_project_name'): + self.new_project_name = "新项目" + if not hasattr(self, 'new_project_path'): + self.new_project_path = "./projects/" + + # 设置对话框标志 + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + # 获取屏幕尺寸,居中显示对话框 + display_size = imgui.get_io().display_size + dialog_width = 400 + dialog_height = 300 + imgui.set_next_window_size((dialog_width, dialog_height)) + imgui.set_next_window_pos( + ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) + ) + + with imgui_ctx.begin("新建项目", True, flags) as window: + if not window.opened: + self.show_new_project_dialog = False + return + + imgui.text("创建新项目") + imgui.separator() + + # 项目名称输入 + changed, project_name = imgui.input_text("项目名称", self.new_project_name, 256) + if changed: + self.new_project_name = project_name + + # 项目路径输入 + changed, project_path = imgui.input_text("项目路径", self.new_project_path, 256) + if changed: + self.new_project_path = project_path + + imgui.same_line() + if imgui.button("浏览..."): + self.path_browser_mode = "new_project" + self.path_browser_current_path = os.path.dirname(self.new_project_path) if self.new_project_path else os.getcwd() + self.show_path_browser = True + self._refresh_path_browser() + + imgui.separator() + + # 按钮区域 + if imgui.button("创建"): + if self.new_project_name and self.new_project_path: + self._create_new_project(self.new_project_name, self.new_project_path) + self.show_new_project_dialog = False + + imgui.same_line() + if imgui.button("取消"): + self.show_new_project_dialog = False + + + def _draw_open_project_dialog(self): + """绘制打开项目对话框""" + if not self.show_open_project_dialog: + return + + # 初始化默认值 + if not hasattr(self, 'open_project_path'): + self.open_project_path = "./projects/" + + # 设置对话框标志 + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + # 获取屏幕尺寸,居中显示对话框 + display_size = imgui.get_io().display_size + dialog_width = 500 + dialog_height = 400 + imgui.set_next_window_size((dialog_width, dialog_height)) + imgui.set_next_window_pos( + ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) + ) + + with imgui_ctx.begin("打开项目", True, flags) as window: + if not window.opened: + self.show_open_project_dialog = False + return + + imgui.text("选择项目") + imgui.separator() + + imgui.text("项目路径:") + changed, project_path = imgui.input_text("##project_path", self.open_project_path, 512) + if changed: + self.open_project_path = project_path + + imgui.same_line() + if imgui.button("浏览..."): + self.path_browser_mode = "open_project" + self.path_browser_current_path = self.open_project_path if self.open_project_path else os.getcwd() + self.show_path_browser = True + self._refresh_path_browser() + + imgui.separator() + + # 按钮区域 + if imgui.button("打开"): + if self.open_project_path: + self._open_project_path(self.open_project_path) + self.show_open_project_dialog = False + + imgui.same_line() + if imgui.button("取消"): + self.show_open_project_dialog = False + + + def _draw_path_browser(self): + """绘制路径选择对话框""" + if not self.show_path_browser: + return + + # 设置对话框标志 + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + # 获取屏幕尺寸,居中显示对话框 + display_size = imgui.get_io().display_size + dialog_width = 600 + dialog_height = 500 + imgui.set_next_window_size((dialog_width, dialog_height)) + imgui.set_next_window_pos( + ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) + ) + + with imgui_ctx.begin("选择路径", True, flags) as window: + if not window.opened: + self.show_path_browser = False + return + + imgui.text("选择路径") + imgui.separator() + + # 当前路径显示 + imgui.text("当前路径:") + imgui.same_line() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.path_browser_current_path) + + imgui.separator() + + # 路径导航按钮 + if imgui.button("上级目录"): + parent_path = os.path.dirname(self.path_browser_current_path) + if parent_path != self.path_browser_current_path: + self.path_browser_current_path = parent_path + self._refresh_path_browser() + + imgui.same_line() + if imgui.button("主目录"): + self.path_browser_current_path = os.path.expanduser("~") + self._refresh_path_browser() + + imgui.same_line() + if imgui.button("当前目录"): + self.path_browser_current_path = os.getcwd() + self._refresh_path_browser() + + imgui.separator() + + # 文件和目录列表 + if self.path_browser_items: + # 先显示目录 + for item in self.path_browser_items: + if item['is_dir']: + # 尝试使用图标或文本标识目录 + if self.icons.get('property_select_image'): # 使用现有图标作为文件夹图标 + imgui.image(self.icons['property_select_image'], (16, 16)) + imgui.same_line() + else: + imgui.text_colored((0.4, 0.6, 1.0, 1.0), ">") + imgui.same_line() + + if imgui.selectable(item['name'], False)[0]: + self.path_browser_current_path = item['path'] + self._refresh_path_browser() + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + self.path_browser_current_path = item['path'] + self._refresh_path_browser() + +# 显示文件(根据模式显示不同类型的文件) + if self.path_browser_mode == "open_project": + for item in self.path_browser_items: + if not item['is_dir'] and item['name'].endswith('.json'): + imgui.text_colored((1.0, 1.0, 0.7, 1.0), "[FILE]") + imgui.same_line() + if imgui.selectable(item['name'], False)[0]: + self.path_browser_selected_path = item['path'] + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + # 选择包含project.json的目录 + self.path_browser_current_path = os.path.dirname(item['path']) + self._apply_selected_path() + elif self.path_browser_mode == "import_model": + for item in self.path_browser_items: + if not item['is_dir']: + file_ext = os.path.splitext(item['name'])[1].lower() + # 根据文件类型显示不同颜色 + if file_ext in ['.gltf', '.glb']: + color = (0.7, 1.0, 0.7, 1.0) # 绿色 - glTF + elif file_ext == '.fbx': + color = (1.0, 0.7, 0.7, 1.0) # 红色 - FBX + elif file_ext in ['.bam', '.egg']: + color = (0.7, 0.7, 1.0, 1.0) # 蓝色 - Panda3D + elif file_ext == '.obj': + color = (1.0, 1.0, 0.7, 1.0) # 黄色 - OBJ + else: + color = (0.8, 0.8, 0.8, 1.0) # 灰色 - 其他 + + imgui.text_colored(color, f"[{file_ext[1:].upper()}]") + imgui.same_line() + if imgui.selectable(item['name'], False)[0]: + self.path_browser_selected_path = item['path'] + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + self.path_browser_selected_path = item['path'] + self._apply_selected_path() + + imgui.separator() + + # 选中路径显示 + if self.path_browser_selected_path: + imgui.text("选中路径:") + imgui.same_line() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.path_browser_selected_path) + + # 按钮区域 + if imgui.button("确定"): + self._apply_selected_path() + self.show_path_browser = False + + imgui.same_line() + if imgui.button("取消"): + self.show_path_browser = False + + + def _draw_import_dialog(self): + """绘制导入模型对话框""" + if not self.show_import_dialog: + return + + # 设置对话框标志 + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + # 获取屏幕尺寸,居中显示对话框 + display_size = imgui.get_io().display_size + dialog_width = 600 + dialog_height = 500 + imgui.set_next_window_size((dialog_width, dialog_height)) + imgui.set_next_window_pos( + ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) + ) + + with imgui_ctx.begin("导入模型", True, flags) as window: + if not window.opened: + self.show_import_dialog = False + return + + imgui.text("选择要导入的模型文件") + imgui.separator() + + # 文件路径输入 + imgui.text("文件路径:") + changed, file_path = imgui.input_text("##import_file_path", self.import_file_path, 512) + if changed: + self.import_file_path = file_path + + imgui.same_line() + if imgui.button("浏览..."): + self.path_browser_mode = "import_model" + self.path_browser_current_path = os.path.dirname(self.import_file_path) if self.import_file_path else os.getcwd() + self.show_path_browser = True + self._refresh_path_browser() + + imgui.separator() + + # 支持的格式说明 + imgui.text("支持的文件格式:") + formats_text = ", ".join(self.supported_formats) + imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text) + + imgui.separator() + + # 文件预览信息 + if self.import_file_path and os.path.exists(self.import_file_path): + file_size = os.path.getsize(self.import_file_path) + imgui.text(f"文件大小: {file_size / 1024:.2f} KB") + + file_ext = os.path.splitext(self.import_file_path)[1].lower() + if file_ext in self.supported_formats: + imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持") + else: + imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式") + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的文件路径") + + imgui.separator() + + # 按钮区域 + can_import = (self.import_file_path and + os.path.exists(self.import_file_path) and + os.path.splitext(self.import_file_path)[1].lower() in self.supported_formats) + + # 根据状态设置按钮颜色 + if can_import: + if imgui.button("导入"): + self._import_model() + self.show_import_dialog = False + else: + # 禁用状态的按钮(灰色显示) + imgui.push_style_color(imgui.Col_.button, (0.3, 0.3, 0.3, 1.0)) + imgui.button("导入") + imgui.pop_style_color() + + imgui.same_line() + if imgui.button("取消"): + self.show_import_dialog = False + + + def _refresh_path_browser(self): + """刷新路径浏览器内容""" + try: + self.path_browser_items = [] + + if not os.path.exists(self.path_browser_current_path): + self.add_error_message(f"路径不存在: {self.path_browser_current_path}") + return + + # 获取目录中的所有项目 + items = [] + try: + for item_name in os.listdir(self.path_browser_current_path): + item_path = os.path.join(self.path_browser_current_path, item_name) + is_dir = os.path.isdir(item_path) + + items.append({ + 'name': item_name, + 'path': item_path, + 'is_dir': is_dir + }) + except PermissionError: + self.add_error_message(f"无法访问路径: {self.path_browser_current_path}") + return + + # 根据模式过滤文件 + if self.path_browser_mode == "import_model": + # 只显示支持的模型文件 + filtered_items = [] + for item in items: + if item['is_dir']: + filtered_items.append(item) + else: + file_ext = os.path.splitext(item['name'])[1].lower() + if file_ext in self.supported_formats: + filtered_items.append(item) + items = filtered_items + + # 排序:目录在前,文件在后,按名称排序 + items.sort(key=lambda x: (not x['is_dir'], x['name'].lower())) + self.path_browser_items = items + + except Exception as e: + self.add_error_message(f"刷新路径浏览器失败: {e}") + + + def _apply_selected_path(self): + """应用选择的路径""" + try: + if self.path_browser_mode == "new_project": + # 新建项目模式:直接使用当前路径 + self.new_project_path = self.path_browser_current_path + self.add_info_message(f"已选择项目路径: {self.new_project_path}") + elif self.path_browser_mode == "open_project": + # 打开项目模式:使用当前路径 + self.open_project_path = self.path_browser_current_path + self.add_info_message(f"已选择项目路径: {self.open_project_path}") + elif self.path_browser_mode == "import_model": + # 导入模型模式:使用选择的文件路径 + self.import_file_path = self.path_browser_selected_path + self.add_info_message(f"已选择文件: {self.import_file_path}") + except Exception as e: + self.add_error_message(f"应用路径失败: {e}") + + # ==================== 创建功能对话框实现 ==================== + + + def _draw_gui_button_dialog(self): + """绘制GUI按钮创建对话框""" + if not self.show_gui_button_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建GUI按钮", self.show_gui_button_dialog, flags) as window: + if not window: + self.show_gui_button_dialog = False + return + + # 初始化参数 + if 'button_text' not in self.dialog_params: + self.dialog_params['button_text'] = "按钮" + if 'button_pos' not in self.dialog_params: + self.dialog_params['button_pos'] = [0.0, 0.0, 0.0] + if 'button_size' not in self.dialog_params: + self.dialog_params['button_size'] = [0.1, 0.1, 0.1] + + imgui.text("GUI按钮参数设置") + imgui.separator() + + # 文本输入 + changed, self.dialog_params['button_text'] = imgui.input_text("按钮文本", self.dialog_params['button_text'], 256) + + # 位置输入 + changed, x = imgui.input_float("X坐标", self.dialog_params['button_pos'][0]) + if changed: + self.dialog_params['button_pos'][0] = x + changed, y = imgui.input_float("Y坐标", self.dialog_params['button_pos'][1]) + if changed: + self.dialog_params['button_pos'][1] = y + changed, z = imgui.input_float("Z坐标", self.dialog_params['button_pos'][2]) + if changed: + self.dialog_params['button_pos'][2] = z + + # 大小输入 + changed, width = imgui.input_float("宽度", self.dialog_params['button_size'][0]) + if changed: + self.dialog_params['button_size'][0] = width + changed, height = imgui.input_float("高度", self.dialog_params['button_size'][1]) + if changed: + self.dialog_params['button_size'][1] = height + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + pos = tuple(self.dialog_params['button_pos']) + text = self.dialog_params['button_text'] + size = tuple(self.dialog_params['button_size'][:2]) + + result = self.createGUIButton(pos, text, size) + self.add_success_message(f"GUI按钮创建成功: {text}") + self.show_gui_button_dialog = False + except Exception as e: + self.add_error_message(f"创建GUI按钮失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_gui_button_dialog = False + + + def _draw_gui_label_dialog(self): + """绘制GUI标签创建对话框""" + if not self.show_gui_label_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建GUI标签", self.show_gui_label_dialog, flags) as window: + if not window: + self.show_gui_label_dialog = False + return + + # 初始化参数 + if 'label_text' not in self.dialog_params: + self.dialog_params['label_text'] = "标签" + if 'label_pos' not in self.dialog_params: + self.dialog_params['label_pos'] = [0.0, 0.0, 0.0] + if 'label_size' not in self.dialog_params: + self.dialog_params['label_size'] = [0.1, 0.1, 0.1] + + imgui.text("GUI标签参数设置") + imgui.separator() + + # 文本输入 + changed, self.dialog_params['label_text'] = imgui.input_text("标签文本", self.dialog_params['label_text'], 256) + + # 位置输入 + changed, x = imgui.input_float("X坐标", self.dialog_params['label_pos'][0]) + if changed: + self.dialog_params['label_pos'][0] = x + changed, y = imgui.input_float("Y坐标", self.dialog_params['label_pos'][1]) + if changed: + self.dialog_params['label_pos'][1] = y + changed, z = imgui.input_float("Z坐标", self.dialog_params['label_pos'][2]) + if changed: + self.dialog_params['label_pos'][2] = z + + # 大小输入 + changed, width = imgui.input_float("宽度", self.dialog_params['label_size'][0]) + if changed: + self.dialog_params['label_size'][0] = width + changed, height = imgui.input_float("高度", self.dialog_params['label_size'][1]) + if changed: + self.dialog_params['label_size'][1] = height + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + pos = tuple(self.dialog_params['label_pos']) + text = self.dialog_params['label_text'] + size = tuple(self.dialog_params['label_size'][:2]) + + result = self.createGUILabel(pos, text, size) + self.add_success_message(f"GUI标签创建成功: {text}") + self.show_gui_label_dialog = False + except Exception as e: + self.add_error_message(f"创建GUI标签失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_gui_label_dialog = False + + + def _draw_gui_entry_dialog(self): + """绘制GUI输入框创建对话框""" + if not self.show_gui_entry_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建GUI输入框", self.show_gui_entry_dialog, flags) as window: + if not window: + self.show_gui_entry_dialog = False + return + + # 初始化参数 + if 'entry_pos' not in self.dialog_params: + self.dialog_params['entry_pos'] = [0.0, 0.0, 0.0] + if 'entry_size' not in self.dialog_params: + self.dialog_params['entry_size'] = [0.2, 0.05, 0.1] + + imgui.text("GUI输入框参数设置") + imgui.separator() + + # 位置输入 + changed, x = imgui.input_float("X坐标", self.dialog_params['entry_pos'][0]) + if changed: + self.dialog_params['entry_pos'][0] = x + changed, y = imgui.input_float("Y坐标", self.dialog_params['entry_pos'][1]) + if changed: + self.dialog_params['entry_pos'][1] = y + changed, z = imgui.input_float("Z坐标", self.dialog_params['entry_pos'][2]) + if changed: + self.dialog_params['entry_pos'][2] = z + + # 大小输入 + changed, width = imgui.input_float("宽度", self.dialog_params['entry_size'][0]) + if changed: + self.dialog_params['entry_size'][0] = width + changed, height = imgui.input_float("高度", self.dialog_params['entry_size'][1]) + if changed: + self.dialog_params['entry_size'][1] = height + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + pos = tuple(self.dialog_params['entry_pos']) + size = tuple(self.dialog_params['entry_size'][:2]) + + result = self.createGUIEntry(pos, size) + self.add_success_message("GUI输入框创建成功") + self.show_gui_entry_dialog = False + except Exception as e: + self.add_error_message(f"创建GUI输入框失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_gui_entry_dialog = False + + + def _draw_gui_image_dialog(self): + """绘制GUI图片创建对话框""" + if not self.show_gui_image_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建GUI图片", self.show_gui_image_dialog, flags) as window: + if not window: + self.show_gui_image_dialog = False + return + + # 初始化参数 + if 'image_pos' not in self.dialog_params: + self.dialog_params['image_pos'] = [0.0, 0.0, 0.0] + if 'image_size' not in self.dialog_params: + self.dialog_params['image_size'] = [0.2, 0.2, 0.1] + if 'image_path' not in self.dialog_params: + self.dialog_params['image_path'] = "" + + imgui.text("GUI图片参数设置") + imgui.separator() + + # 位置输入 + changed, x = imgui.input_float("X坐标", self.dialog_params['image_pos'][0]) + if changed: + self.dialog_params['image_pos'][0] = x + changed, y = imgui.input_float("Y坐标", self.dialog_params['image_pos'][1]) + if changed: + self.dialog_params['image_pos'][1] = y + changed, z = imgui.input_float("Z坐标", self.dialog_params['image_pos'][2]) + if changed: + self.dialog_params['image_pos'][2] = z + + # 大小输入 + changed, width = imgui.input_float("宽度", self.dialog_params['image_size'][0]) + if changed: + self.dialog_params['image_size'][0] = width + changed, height = imgui.input_float("高度", self.dialog_params['image_size'][1]) + if changed: + self.dialog_params['image_size'][1] = height + + # 图片路径 + changed, self.dialog_params['image_path'] = imgui.input_text("图片路径", self.dialog_params['image_path'], 512) + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + pos = tuple(self.dialog_params['image_pos']) + size = tuple(self.dialog_params['image_size'][:2]) + image_path = self.dialog_params['image_path'] + + result = self.createGUIImage(pos, image_path, size) + self.add_success_message("GUI图片创建成功") + self.show_gui_image_dialog = False + except Exception as e: + self.add_error_message(f"创建GUI图片失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_gui_image_dialog = False + + + def _draw_3d_text_dialog(self): + """绘制3D文本创建对话框""" + if not self.show_3d_text_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建3D文本", self.show_3d_text_dialog, flags) as window: + if not window: + self.show_3d_text_dialog = False + return + + # 初始化参数 + if 'text3d_text' not in self.dialog_params: + self.dialog_params['text3d_text'] = "3D文本" + if 'text3d_pos' not in self.dialog_params: + self.dialog_params['text3d_pos'] = [0.0, 0.0, 0.0] + if 'text3d_size' not in self.dialog_params: + self.dialog_params['text3d_size'] = 1.0 + + imgui.text("3D文本参数设置") + imgui.separator() + + # 文本输入 + changed, self.dialog_params['text3d_text'] = imgui.input_text("文本内容", self.dialog_params['text3d_text'], 256) + + # 位置输入 + changed, x = imgui.input_float("X坐标", self.dialog_params['text3d_pos'][0]) + if changed: + self.dialog_params['text3d_pos'][0] = x + changed, y = imgui.input_float("Y坐标", self.dialog_params['text3d_pos'][1]) + if changed: + self.dialog_params['text3d_pos'][1] = y + changed, z = imgui.input_float("Z坐标", self.dialog_params['text3d_pos'][2]) + if changed: + self.dialog_params['text3d_pos'][2] = z + + # 大小输入 + changed, self.dialog_params['text3d_size'] = imgui.input_float("文本大小", self.dialog_params['text3d_size']) + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + pos = tuple(self.dialog_params['text3d_pos']) + text = self.dialog_params['text3d_text'] + size = self.dialog_params['text3d_size'] + + result = self.create3DText(pos, text, size) + self.add_success_message(f"3D文本创建成功: {text}") + self.show_3d_text_dialog = False + except Exception as e: + self.add_error_message(f"创建3D文本失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_3d_text_dialog = False + + + def _draw_3d_image_dialog(self): + """绘制3D图片创建对话框""" + if not self.show_3d_image_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建3D图片", self.show_3d_image_dialog, flags) as window: + if not window: + self.show_3d_image_dialog = False + return + + # 初始化参数 + if 'image3d_pos' not in self.dialog_params: + self.dialog_params['image3d_pos'] = [0.0, 0.0, 0.0] + if 'image3d_size' not in self.dialog_params: + self.dialog_params['image3d_size'] = [1.0, 1.0, 1.0] + if 'image3d_path' not in self.dialog_params: + self.dialog_params['image3d_path'] = "" + + imgui.text("3D图片参数设置") + imgui.separator() + + # 位置输入 + changed, x = imgui.input_float("X坐标", self.dialog_params['image3d_pos'][0]) + if changed: + self.dialog_params['image3d_pos'][0] = x + changed, y = imgui.input_float("Y坐标", self.dialog_params['image3d_pos'][1]) + if changed: + self.dialog_params['image3d_pos'][1] = y + changed, z = imgui.input_float("Z坐标", self.dialog_params['image3d_pos'][2]) + if changed: + self.dialog_params['image3d_pos'][2] = z + + # 大小输入 + changed, width = imgui.input_float("宽度", self.dialog_params['image3d_size'][0]) + if changed: + self.dialog_params['image3d_size'][0] = width + changed, height = imgui.input_float("高度", self.dialog_params['image3d_size'][1]) + if changed: + self.dialog_params['image3d_size'][1] = height + + # 图片路径 + changed, self.dialog_params['image3d_path'] = imgui.input_text("图片路径", self.dialog_params['image3d_path'], 512) + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + pos = tuple(self.dialog_params['image3d_pos']) + size = tuple(self.dialog_params['image3d_size'][:2]) + image_path = self.dialog_params['image3d_path'] + + result = self.create3DImage(pos, image_path, size) + self.add_success_message("3D图片创建成功") + self.show_3d_image_dialog = False + except Exception as e: + self.add_error_message(f"创建3D图片失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_3d_image_dialog = False + + # 添加其他创建对话框的占位符方法 + + def _draw_video_screen_dialog(self): + """绘制视频屏幕创建对话框""" + if not self.show_video_screen_dialog: + return + self.show_video_screen_dialog = False + self.add_info_message("视频屏幕创建功能开发中...") + + + def _draw_2d_video_screen_dialog(self): + """绘制2D视频屏幕创建对话框""" + if not self.show_2d_video_screen_dialog: + return + self.show_2d_video_screen_dialog = False + self.add_info_message("2D视频屏幕创建功能开发中...") + + + def _draw_spherical_video_dialog(self): + """绘制球形视频创建对话框""" + if not self.show_spherical_video_dialog: + return + self.show_spherical_video_dialog = False + self.add_info_message("球形视频创建功能开发中...") + + + def _draw_virtual_screen_dialog(self): + """绘制虚拟屏幕创建对话框""" + if not self.show_virtual_screen_dialog: + return + self.show_virtual_screen_dialog = False + self.add_info_message("虚拟屏幕创建功能开发中...") + + + def _draw_spot_light_dialog(self): + """绘制聚光灯创建对话框""" + if not self.show_spot_light_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建聚光灯", self.show_spot_light_dialog, flags) as window: + if not window: + self.show_spot_light_dialog = False + return + + # 初始化参数 + if 'spotlight_pos' not in self.dialog_params: + self.dialog_params['spotlight_pos'] = [0.0, 0.0, 5.0] + if 'spotlight_color' not in self.dialog_params: + self.dialog_params['spotlight_color'] = [1.0, 1.0, 1.0] + if 'spotlight_intensity' not in self.dialog_params: + self.dialog_params['spotlight_intensity'] = 1.0 + + imgui.text("聚光灯参数设置") + imgui.separator() + + # 位置输入 + changed, x = imgui.input_float("X坐标", self.dialog_params['spotlight_pos'][0]) + if changed: + self.dialog_params['spotlight_pos'][0] = x + changed, y = imgui.input_float("Y坐标", self.dialog_params['spotlight_pos'][1]) + if changed: + self.dialog_params['spotlight_pos'][1] = y + changed, z = imgui.input_float("Z坐标", self.dialog_params['spotlight_pos'][2]) + if changed: + self.dialog_params['spotlight_pos'][2] = z + + # 颜色输入 + changed, r = imgui.input_float("红色 (R)", self.dialog_params['spotlight_color'][0], 0.0, 1.0) + if changed: + self.dialog_params['spotlight_color'][0] = max(0.0, min(1.0, r)) + changed, g = imgui.input_float("绿色 (G)", self.dialog_params['spotlight_color'][1], 0.0, 1.0) + if changed: + self.dialog_params['spotlight_color'][1] = max(0.0, min(1.0, g)) + changed, b = imgui.input_float("蓝色 (B)", self.dialog_params['spotlight_color'][2], 0.0, 1.0) + if changed: + self.dialog_params['spotlight_color'][2] = max(0.0, min(1.0, b)) + + # 强度输入 + changed, self.dialog_params['spotlight_intensity'] = imgui.input_float("强度", self.dialog_params['spotlight_intensity'], 0.1, 2.0) + if changed: + self.dialog_params['spotlight_intensity'] = max(0.1, self.dialog_params['spotlight_intensity']) + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + pos = tuple(self.dialog_params['spotlight_pos']) + + result = self.createSpotLight(pos) + if result: + # 设置颜色和强度 + light = result.node() + if hasattr(light, 'setColor'): + color = tuple(self.dialog_params['spotlight_color']) + light.setColor(color + (1.0,)) # 添加alpha通道 + if hasattr(light, 'setEnergy'): + light.setEnergy(self.dialog_params['spotlight_intensity']) + + self.add_success_message("聚光灯创建成功") + self.show_spot_light_dialog = False + else: + self.add_error_message("聚光灯创建失败") + except Exception as e: + self.add_error_message(f"创建聚光灯失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_spot_light_dialog = False + + + def _draw_point_light_dialog(self): + """绘制点光源创建对话框""" + if not self.show_point_light_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建点光源", self.show_point_light_dialog, flags) as window: + if not window: + self.show_point_light_dialog = False + return + + # 初始化参数 + if 'pointlight_pos' not in self.dialog_params: + self.dialog_params['pointlight_pos'] = [0.0, 0.0, 5.0] + if 'pointlight_color' not in self.dialog_params: + self.dialog_params['pointlight_color'] = [1.0, 1.0, 1.0] + if 'pointlight_intensity' not in self.dialog_params: + self.dialog_params['pointlight_intensity'] = 1.0 + if 'pointlight_radius' not in self.dialog_params: + self.dialog_params['pointlight_radius'] = 10.0 + + imgui.text("点光源参数设置") + imgui.separator() + + # 位置输入 + changed, x = imgui.input_float("X坐标", self.dialog_params['pointlight_pos'][0]) + if changed: + self.dialog_params['pointlight_pos'][0] = x + changed, y = imgui.input_float("Y坐标", self.dialog_params['pointlight_pos'][1]) + if changed: + self.dialog_params['pointlight_pos'][1] = y + changed, z = imgui.input_float("Z坐标", self.dialog_params['pointlight_pos'][2]) + if changed: + self.dialog_params['pointlight_pos'][2] = z + + # 颜色输入 + changed, r = imgui.input_float("红色 (R)", self.dialog_params['pointlight_color'][0], 0.0, 1.0) + if changed: + self.dialog_params['pointlight_color'][0] = max(0.0, min(1.0, r)) + changed, g = imgui.input_float("绿色 (G)", self.dialog_params['pointlight_color'][1], 0.0, 1.0) + if changed: + self.dialog_params['pointlight_color'][1] = max(0.0, min(1.0, g)) + changed, b = imgui.input_float("蓝色 (B)", self.dialog_params['pointlight_color'][2], 0.0, 1.0) + if changed: + self.dialog_params['pointlight_color'][2] = max(0.0, min(1.0, b)) + + # 强度输入 + changed, self.dialog_params['pointlight_intensity'] = imgui.input_float("强度", self.dialog_params['pointlight_intensity'], 0.1, 2.0) + if changed: + self.dialog_params['pointlight_intensity'] = max(0.1, self.dialog_params['pointlight_intensity']) + + # 半径输入 + changed, self.dialog_params['pointlight_radius'] = imgui.input_float("影响半径", self.dialog_params['pointlight_radius'], 1.0, 50.0) + if changed: + self.dialog_params['pointlight_radius'] = max(1.0, self.dialog_params['pointlight_radius']) + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + pos = tuple(self.dialog_params['pointlight_pos']) + + result = self.createPointLight(pos) + if result: + # 设置颜色和强度 + light = result.node() + if hasattr(light, 'setColor'): + color = tuple(self.dialog_params['pointlight_color']) + light.setColor(color + (1.0,)) # 添加alpha通道 + if hasattr(light, 'setEnergy'): + light.setEnergy(self.dialog_params['pointlight_intensity']) + if hasattr(light, 'setAttenuation'): + # 设置衰减: (constant, linear, quadratic) + radius = self.dialog_params['pointlight_radius'] + light.setAttenuation((1.0, 0.5/radius, 0.5/(radius*radius))) + + self.add_success_message("点光源创建成功") + self.show_point_light_dialog = False + else: + self.add_error_message("点光源创建失败") + except Exception as e: + self.add_error_message(f"创建点光源失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_point_light_dialog = False + + + def _draw_terrain_dialog(self): + """绘制地形创建对话框""" + if not self.show_terrain_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建平面地形", self.show_terrain_dialog, flags) as window: + if not window: + self.show_terrain_dialog = False + return + + # 初始化参数 + if 'terrain_width' not in self.dialog_params: + self.dialog_params['terrain_width'] = 10.0 + if 'terrain_height' not in self.dialog_params: + self.dialog_params['terrain_height'] = 10.0 + if 'terrain_resolution' not in self.dialog_params: + self.dialog_params['terrain_resolution'] = 129 + + imgui.text("平面地形参数设置") + imgui.separator() + + # 尺寸输入 + changed, self.dialog_params['terrain_width'] = imgui.input_float("宽度", self.dialog_params['terrain_width'], 1.0, 100.0) + if changed: + self.dialog_params['terrain_width'] = max(1.0, self.dialog_params['terrain_width']) + + changed, self.dialog_params['terrain_height'] = imgui.input_float("高度", self.dialog_params['terrain_height'], 1.0, 100.0) + if changed: + self.dialog_params['terrain_height'] = max(1.0, self.dialog_params['terrain_height']) + + # 分辨率输入 + changed, self.dialog_params['terrain_resolution'] = imgui.input_int("分辨率", self.dialog_params['terrain_resolution']) + if changed: + # 确保分辨率是有效的 (2的幂次方 + 1) + valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] + if self.dialog_params['terrain_resolution'] not in valid_resolutions: + closest_res = min(valid_resolutions, key=lambda x: abs(x - self.dialog_params['terrain_resolution'])) + self.dialog_params['terrain_resolution'] = closest_res + + imgui.separator() + imgui.text("有效分辨率值: 17, 33, 65, 129, 257, 513, 1025") + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + width = self.dialog_params['terrain_width'] + height = self.dialog_params['terrain_height'] + resolution = self.dialog_params['terrain_resolution'] + + # 转换为地形管理器期望的格式 + size = (width, height) + + result = self.createFlatTerrain(size, resolution) + if result: + self.add_success_message("平面地形创建成功") + self.show_terrain_dialog = False + else: + self.add_error_message("平面地形创建失败") + except Exception as e: + self.add_error_message(f"创建平面地形失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_terrain_dialog = False + + + def _draw_script_dialog(self): + """绘制脚本创建对话框""" + if not self.show_script_dialog: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("创建脚本", self.show_script_dialog, flags) as window: + if not window: + self.show_script_dialog = False + return + + # 初始化参数 + if 'script_name' not in self.dialog_params: + self.dialog_params['script_name'] = "new_script" + if 'script_template' not in self.dialog_params: + self.dialog_params['script_template'] = 0 # 0=basic, 1=movement + + imgui.text("脚本参数设置") + imgui.separator() + + # 脚本名称输入 + changed, self.dialog_params['script_name'] = imgui.input_text("脚本名称", self.dialog_params['script_name'], 256) + + # 模板选择 + templates = ["基础模板", "移动模板"] + changed, self.dialog_params['script_template'] = imgui.combo("模板类型", self.dialog_params['script_template'], templates) + + imgui.separator() + + # 模板说明 + if self.dialog_params['script_template'] == 0: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "基础模板: 包含基本的脚本结构") + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "移动模板: 包含移动相关的基本功能") + + imgui.separator() + + # 按钮 + if imgui.button("创建"): + try: + script_name = self.dialog_params['script_name'] + if not script_name: + self.add_error_message("请输入脚本名称") + return + + template = "basic" if self.dialog_params['script_template'] == 0 else "movement" + + result = self.createScript(script_name, template) + if result: + self.add_success_message(f"脚本创建成功: {script_name}") + + # 如果启用了热重载,自动加载新脚本 + if self.hotReloadEnabled: + try: + self.loadScript(result) + self.add_info_message(f"脚本已自动加载: {script_name}") + except Exception as e: + self.add_warning_message(f"脚本自动加载失败: {str(e)}") + + self.show_script_dialog = False + else: + self.add_error_message("脚本创建失败") + except Exception as e: + self.add_error_message(f"创建脚本失败: {str(e)}") + + imgui.same_line() + if imgui.button("取消"): + self.show_script_dialog = False + + + def _draw_script_browser(self): + """绘制脚本文件浏览器""" + if not self.show_script_browser: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("选择脚本文件", self.show_script_browser, flags) as window: + if not window: + self.show_script_browser = False + return + + imgui.text("选择脚本文件") + imgui.separator() + + # 导航按钮 + if imgui.button("上级目录"): + parent_path = os.path.dirname(self.script_browser_current_path) + if parent_path != self.script_browser_current_path: + self.script_browser_current_path = parent_path + self._refresh_script_browser() + + imgui.same_line() + if imgui.button("脚本目录"): + # 切换到脚本目录 + if hasattr(self, 'script_manager') and self.script_manager: + scripts_dir = self.script_manager.scripts_directory + if os.path.exists(scripts_dir): + self.script_browser_current_path = scripts_dir + self._refresh_script_browser() + + imgui.same_line() + if imgui.button("当前目录"): + self.script_browser_current_path = os.getcwd() + self._refresh_script_browser() + + imgui.separator() + + # 当前路径显示 + imgui.text("当前路径:") + imgui.same_line() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.script_browser_current_path) + + imgui.separator() + + # 文件列表 + if imgui.begin_child("script_file_list", (580, 300)): + for item in self.script_browser_items: + if item['is_dir']: + # 目录 + imgui.text_colored((0.3, 0.8, 1.0, 1.0), f"📁 {item['name']}") + if imgui.is_item_clicked(): + self.script_browser_current_path = item['path'] + self._refresh_script_browser() + else: + # Python文件 + imgui.text(f"📄 {item['name']}") + if imgui.is_item_clicked(): + self.script_browser_selected_path = item['path'] + imgui.end_child() + + imgui.separator() + + # 选中的文件信息 + if self.script_browser_selected_path and os.path.exists(self.script_browser_selected_path): + file_size = os.path.getsize(self.script_browser_selected_path) + imgui.text(f"文件大小: {file_size / 1024:.2f} KB") + + file_ext = os.path.splitext(self.script_browser_selected_path)[1].lower() + if file_ext == '.py': + imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ Python脚本文件") + else: + imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不是Python脚本文件") + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的Python脚本文件") + + imgui.separator() + + # 按钮 + can_load = (self.script_browser_selected_path and + os.path.exists(self.script_browser_selected_path) and + os.path.splitext(self.script_browser_selected_path)[1].lower() == '.py') + + if can_load: + if imgui.button("加载脚本"): + try: + result = self.loadScript(self.script_browser_selected_path) + if result: + script_name = os.path.basename(self.script_browser_selected_path) + self.add_success_message(f"脚本加载成功: {script_name}") + self.show_script_browser = False + else: + self.add_error_message("脚本加载失败") + except Exception as e: + self.add_error_message(f"加载脚本失败: {str(e)}") + else: + imgui.push_style_var(imgui.StyleVar_.alpha, 0.5) + imgui.button("加载脚本") + imgui.pop_style_var() + + imgui.same_line() + if imgui.button("取消"): + self.show_script_browser = False + self.script_browser_selected_path = "" + + + def _refresh_script_browser(self): + """刷新脚本浏览器内容""" + try: + self.script_browser_items = [] + + if not os.path.exists(self.script_browser_current_path): + self.script_browser_current_path = os.getcwd() + + # 获取目录中的所有项目 + items = [] + try: + for item_name in os.listdir(self.script_browser_current_path): + item_path = os.path.join(self.script_browser_current_path, item_name) + + if os.path.isdir(item_path): + items.append({ + 'name': item_name, + 'path': item_path, + 'is_dir': True + }) + elif os.path.isfile(item_path): + file_ext = os.path.splitext(item_name)[1].lower() + if file_ext == '.py': # 只显示Python文件 + items.append({ + 'name': item_name, + 'path': item_path, + 'is_dir': False + }) + except PermissionError: + print(f"权限错误: 无法访问目录 {self.script_browser_current_path}") + + # 排序:目录在前,文件在后 + items.sort(key=lambda x: (not x['is_dir'], x['name'].lower())) + self.script_browser_items = items + + except Exception as e: + print(f"刷新脚本浏览器时出错: {e}") + self.script_browser_items = [] + + + def _draw_heightmap_browser(self): + """绘制高度图文件浏览器""" + if not self.show_heightmap_browser: + return + + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + with imgui_ctx.begin("选择高度图文件", self.show_heightmap_browser, flags) as window: + if not window: + self.show_heightmap_browser = False + return + + imgui.text("选择高度图文件") + imgui.separator() + + # 导航按钮 + if imgui.button("上级目录"): + parent_path = os.path.dirname(self.heightmap_browser_current_path) + if parent_path != self.heightmap_browser_current_path: + self.heightmap_browser_current_path = parent_path + self._refresh_heightmap_browser() + + imgui.same_line() + if imgui.button("主目录"): + self.heightmap_browser_current_path = os.getcwd() + self._refresh_heightmap_browser() + + imgui.same_line() + if imgui.button("当前目录"): + self.heightmap_browser_current_path = os.getcwd() + self._refresh_heightmap_browser() + + imgui.separator() + + # 当前路径显示 + imgui.text("当前路径:") + imgui.same_line() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.heightmap_browser_current_path) + + imgui.separator() + + # 文件列表 + if imgui.begin_child("file_list", (580, 300)): + for item in self.heightmap_browser_items: + if item['is_dir']: + # 目录 + imgui.text_colored((0.3, 0.8, 1.0, 1.0), f"📁 {item['name']}") + if imgui.is_item_clicked(): + self.heightmap_browser_current_path = item['path'] + self._refresh_heightmap_browser() + else: + # 文件 + imgui.text(f"📄 {item['name']}") + if imgui.is_item_clicked(): + self.heightmap_browser_selected_path = item['path'] + self.heightmap_file_path = item['path'] + imgui.end_child() + + imgui.separator() + + # 支持的格式说明 + imgui.text("支持的文件格式:") + formats_text = ", ".join(self.supported_heightmap_formats) + imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text) + + imgui.separator() + + # 选中的文件信息 + if self.heightmap_file_path and os.path.exists(self.heightmap_file_path): + file_size = os.path.getsize(self.heightmap_file_path) + imgui.text(f"文件大小: {file_size / 1024:.2f} KB") + + file_ext = os.path.splitext(self.heightmap_file_path)[1].lower() + if file_ext in self.supported_heightmap_formats: + imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持") + else: + imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式") + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的高度图文件") + + imgui.separator() + + # 按钮 + can_import = (self.heightmap_file_path and + os.path.exists(self.heightmap_file_path) and + os.path.splitext(self.heightmap_file_path)[1].lower() in self.supported_heightmap_formats) + + if can_import: + if imgui.button("创建地形"): + try: + # 使用默认缩放参数创建地形 + scale = (1.0, 1.0, 10.0) # X, Y, Z缩放 + result = self.createTerrainFromHeightMap(self.heightmap_file_path, scale) + if result: + self.add_success_message("高度图地形创建成功") + self.show_heightmap_browser = False + else: + self.add_error_message("高度图地形创建失败") + except Exception as e: + self.add_error_message(f"创建高度图地形失败: {str(e)}") + else: + imgui.push_style_var(imgui.StyleVar_.alpha, 0.5) + imgui.button("创建地形") + imgui.pop_style_var() + + imgui.same_line() + if imgui.button("取消"): + self.show_heightmap_browser = False + self.heightmap_file_path = "" + + + def _refresh_heightmap_browser(self): + """刷新高度图浏览器内容""" + try: + self.heightmap_browser_items = [] + + if not os.path.exists(self.heightmap_browser_current_path): + self.heightmap_browser_current_path = os.getcwd() + + # 获取目录中的所有项目 + items = [] + try: + for item_name in os.listdir(self.heightmap_browser_current_path): + item_path = os.path.join(self.heightmap_browser_current_path, item_name) + + if os.path.isdir(item_path): + items.append({ + 'name': item_name, + 'path': item_path, + 'is_dir': True + }) + elif os.path.isfile(item_path): + file_ext = os.path.splitext(item_name)[1].lower() + if file_ext in self.supported_heightmap_formats: + items.append({ + 'name': item_name, + 'path': item_path, + 'is_dir': False + }) + except PermissionError: + print(f"权限错误: 无法访问目录 {self.heightmap_browser_current_path}") + + # 排序:目录在前,文件在后 + items.sort(key=lambda x: (not x['is_dir'], x['name'].lower())) + self.heightmap_browser_items = items + + except Exception as e: + print(f"刷新高度图浏览器时出错: {e}") + self.heightmap_browser_items = [] + + # ==================== 导入功能实现 ==================== diff --git a/ui/panels/editor_panels.py b/ui/panels/editor_panels.py new file mode 100644 index 00000000..90be29ff --- /dev/null +++ b/ui/panels/editor_panels.py @@ -0,0 +1,2020 @@ +from imgui_bundle import imgui, imgui_ctx +import os +from pathlib import Path + + +class EditorPanels: + """Centralized UI panel renderer for top-level editor panels.""" + + def __init__(self, app): + self.app = app + + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + 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: + if imgui.menu_item("新建项目", "Ctrl+N", False, True)[1]: + self.app._on_new_project() + if imgui.menu_item("打开项目", "Ctrl+O", False, True)[1]: + self.app._on_open_project() + imgui.separator() + if imgui.menu_item("保存", "Ctrl+S", False, True)[1]: + self.app._on_save_project() + if imgui.menu_item("另存为", "", False, True)[1]: + self.app._on_save_as_project() + imgui.separator() + if imgui.menu_item("退出", "Alt+F4", False, True)[1]: + self.app._on_exit() + + # 编辑菜单 + with imgui_ctx.begin_menu("编辑") as edit_menu: + if edit_menu: + if imgui.menu_item("撤销", "Ctrl+Z", False, True)[1]: + self.app._on_undo() + if imgui.menu_item("重做", "Ctrl+Y", False, True)[1]: + self.app._on_redo() + imgui.separator() + if imgui.menu_item("剪切", "Ctrl+X", False, True)[1]: + self.app._on_cut() + if imgui.menu_item("复制", "Ctrl+C", False, True)[1]: + self.app._on_copy() + if imgui.menu_item("粘贴", "Ctrl+V", False, True)[1]: + self.app._on_paste() + imgui.separator() + if imgui.menu_item("删除", "Del", False, True)[1]: + self.app._on_delete() + + # 创建菜单 + with imgui_ctx.begin_menu("创建") as create_menu: + if create_menu: + if imgui.menu_item("导入模型", "", False, True)[1]: + self.app._on_import_model() + + imgui.separator() + + if imgui.menu_item("空对象", "", False, True)[1]: + self.app._on_create_empty_object() + + # 3D对象子菜单 + with imgui_ctx.begin_menu("3D对象") as three_d_menu: + if three_d_menu: + if imgui.menu_item("立方体", "", False, True)[1]: + self.app._on_create_cube() + if imgui.menu_item("球体", "", False, True)[1]: + self.app._on_create_sphere() + if imgui.menu_item("圆柱体", "", False, True)[1]: + self.app._on_create_cylinder() + if imgui.menu_item("平面", "", False, True)[1]: + self.app._on_create_plane() + + # 3D GUI子菜单 + with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu: + if three_d_gui_menu: + if imgui.menu_item("3D文本", "", False, True)[1]: + self.app._on_create_3d_text() + if imgui.menu_item("3D图片", "", False, True)[1]: + self.app._on_create_3d_image() + + # GUI子菜单 + with imgui_ctx.begin_menu("GUI") as gui_menu: + if gui_menu: + if imgui.menu_item("创建按钮", "", False, True)[1]: + self.app._on_create_gui_button() + if imgui.menu_item("创建标签", "", False, True)[1]: + self.app._on_create_gui_label() + if imgui.menu_item("创建输入框", "", False, True)[1]: + self.app._on_create_gui_entry() + if imgui.menu_item("创建图片", "", False, True)[1]: + self.app._on_create_gui_image() + imgui.separator() + if imgui.menu_item("创建视频屏幕", "", False, True)[1]: + self.app._on_create_video_screen() + if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]: + self.app._on_create_2d_video_screen() + if imgui.menu_item("创建球形视频", "", False, True)[1]: + self.app._on_create_spherical_video() + if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]: + self.app._on_create_virtual_screen() + + # 光源子菜单 + with imgui_ctx.begin_menu("光源") as light_menu: + if light_menu: + if imgui.menu_item("聚光灯", "", False, True)[1]: + self.app._on_create_spot_light() + if imgui.menu_item("点光源", "", False, True)[1]: + self.app._on_create_point_light() + + # 地形子菜单 + with imgui_ctx.begin_menu("地形") as terrain_menu: + if terrain_menu: + if imgui.menu_item("创建平面地形", "", False, True)[1]: + self.app._on_create_flat_terrain() + if imgui.menu_item("从高度图创建地形", "", False, True)[1]: + self.app._on_create_heightmap_terrain() + + # 脚本子菜单 + with imgui_ctx.begin_menu("脚本") as script_menu: + if script_menu: + if imgui.menu_item("创建脚本...", "", False, True)[1]: + self.app._on_create_script() + if imgui.menu_item("加载脚本文件...", "", False, True)[1]: + self.app._on_load_script() + imgui.separator() + if imgui.menu_item("重载所有脚本", "", False, True)[1]: + self.app._on_reload_all_scripts() + _, self.app.hotReloadEnabled = imgui.menu_item("启用热重载", "", self.app.hotReloadEnabled, True) + if imgui.menu_item("脚本管理器", "", False, True)[1]: + self.app._on_open_scripts_manager() + + # 信息面板子菜单 + with imgui_ctx.begin_menu("信息面板") as info_panel_menu: + if info_panel_menu: + if imgui.menu_item("创建2D示例面板", "", False, True)[1]: + self.app._on_create_2d_sample_panel() + if imgui.menu_item("创建3D实例面板", "", False, True)[1]: + self.app._on_create_3d_sample_panel() + if imgui.menu_item("Web面板", "", False, True)[1]: + self.app._on_create_web_panel() + + # 视图菜单 + with imgui_ctx.begin_menu("视图") as view_menu: + if view_menu: + _, self.app.showToolbar = imgui.menu_item("工具栏", "", self.app.showToolbar, True) + _, self.app.showSceneTree = imgui.menu_item("场景树", "", self.app.showSceneTree, True) + _, self.app.showResourceManager = imgui.menu_item("资源管理器", "", self.app.showResourceManager, True) + _, self.app.showPropertyPanel = imgui.menu_item("属性面板", "", self.app.showPropertyPanel, True) + _, self.app.showConsole = imgui.menu_item("控制台", "", self.app.showConsole, True) + _, self.app.showScriptPanel = imgui.menu_item("脚本管理", "", self.app.showScriptPanel, True) + _, self.app.showLUIEditor = imgui.menu_item("LUI编辑器", "", self.app.showLUIEditor, True) + + # 工具菜单 + with imgui_ctx.begin_menu("工具") as tools_menu: + if tools_menu: + # 工具切换选项 + if imgui.menu_item("选择工具", "", False, True)[1]: + self.app.tool_manager.setCurrentTool("选择") + if imgui.menu_item("移动工具", "", False, True)[1]: + self.app.tool_manager.setCurrentTool("移动") + if imgui.menu_item("旋转工具", "", False, True)[1]: + self.app.tool_manager.setCurrentTool("旋转") + if imgui.menu_item("缩放工具", "", False, True)[1]: + self.app.tool_manager.setCurrentTool("缩放") + + imgui.separator() + + # 编辑工具 + if imgui.menu_item("光照编辑", "", False, True)[1]: + self.app.tool_manager.setCurrentTool("光照编辑") + if imgui.menu_item("图形编辑", "", False, True)[1]: + self.app.tool_manager.setCurrentTool("图形编辑") + + imgui.separator() + + # VR子菜单 + with imgui_ctx.begin_menu("VR") as vr_menu: + if vr_menu: + if imgui.menu_item("进入VR模式", "", False, True)[1]: + self.app._toggle_vr_mode() + if imgui.menu_item("退出VR模式", "", False, True)[1]: + self.app._exit_vr_mode() + + imgui.separator() + + if imgui.menu_item("VR状态", "", False, True)[1]: + self.app._show_vr_status() + if imgui.menu_item("VR设置", "", False, True)[1]: + self.app._show_vr_settings() + + imgui.separator() + + # VR调试子菜单 + with imgui_ctx.begin_menu("VR调试") as vr_debug_menu: + if vr_debug_menu: + _, self.app.vr_debug_enabled = imgui.menu_item("启用调试输出", "", self.app.vr_debug_enabled, True) + + if imgui.menu_item("立即显示性能报告", "", False, True)[1]: + self.app._show_vr_performance_report() + + imgui.separator() + + # 输出模式 + with imgui_ctx.begin_menu("输出模式") as output_menu: + if output_menu: + if imgui.menu_item("简短模式", "", not self.app.vr_detailed_mode, True)[1]: + self.app.vr_detailed_mode = False + if imgui.menu_item("详细模式", "", self.app.vr_detailed_mode, True)[1]: + self.app.vr_detailed_mode = True + + imgui.separator() + + _, self.app.vr_performance_monitor = imgui.menu_item("启用性能监控", "", self.app.vr_performance_monitor, True) + + # 窗口菜单 - 已隐藏 + # with imgui_ctx.begin_menu("窗口") as window_menu: + # if window_menu: + # _, self.app.showDemoWindow = imgui.menu_item("ImGui演示", "", self.app.showDemoWindow, True) + # if self.app.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): + """绘制工具栏""" + # 工具栏可以保持无标题栏,但允许移动和调整大小 + flags = self.app.style_manager.get_window_flags("toolbar") + + with self.app.style_manager.begin_styled_window("工具栏", self.app.showToolbar, flags): + self.app.showToolbar = True # 确保窗口保持打开 + + # 选择工具按钮 + select_active = self.app.tool_manager.isSelectionTool() + if self.app.icons.get('select'): + tint_col = (1.0, 1.0, 0.0, 1.0) if select_active else (1.0, 1.0, 1.0, 1.0) + if self.app.style_manager.image_button(self.app.icons['select'], (24, 24), tint_col=tint_col): + self.app.tool_manager.setCurrentTool("选择") + if imgui.is_item_hovered(): + imgui.set_tooltip("选择工具 (Q)") + imgui.same_line() + else: + if imgui.button("选择##select_tool"): + self.app.tool_manager.setCurrentTool("选择") + if select_active: + draw_list = imgui.get_window_draw_list() + button_min = imgui.get_item_rect_min() + button_max = imgui.get_item_rect_max() + draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3))) + imgui.same_line() + + # 移动工具按钮 + move_active = self.app.tool_manager.isMoveTool() + if self.app.icons.get('move'): + # 使用不同颜色表示活动状态 + tint_col = (1.0, 1.0, 0.0, 1.0) if move_active else (1.0, 1.0, 1.0, 1.0) # 活动时显示黄色 + if self.app.style_manager.image_button(self.app.icons['move'], (24, 24), tint_col=tint_col): + self.app.tool_manager.setCurrentTool("移动") + if imgui.is_item_hovered(): + imgui.set_tooltip("移动工具 (W)") + imgui.same_line() + else: + if imgui.button("移动##move_tool"): + self.app.tool_manager.setCurrentTool("移动") + if move_active: + # 为活动按钮添加背景色 + draw_list = imgui.get_window_draw_list() + button_min = imgui.get_item_rect_min() + button_max = imgui.get_item_rect_max() + draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3))) + imgui.same_line() + + # 旋转工具按钮 + rotate_active = self.app.tool_manager.isRotateTool() + if self.app.icons.get('rotate'): + tint_col = (1.0, 1.0, 0.0, 1.0) if rotate_active else (1.0, 1.0, 1.0, 1.0) + if self.app.style_manager.image_button(self.app.icons['rotate'], (24, 24), tint_col=tint_col): + self.app.tool_manager.setCurrentTool("旋转") + if imgui.is_item_hovered(): + imgui.set_tooltip("旋转工具 (E)") + imgui.same_line() + else: + if imgui.button("旋转##rotate_tool"): + self.app.tool_manager.setCurrentTool("旋转") + if rotate_active: + draw_list = imgui.get_window_draw_list() + button_min = imgui.get_item_rect_min() + button_max = imgui.get_item_rect_max() + draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3))) + imgui.same_line() + + # 缩放工具按钮 + scale_active = self.app.tool_manager.isScaleTool() + if self.app.icons.get('scale'): + tint_col = (1.0, 1.0, 0.0, 1.0) if scale_active else (1.0, 1.0, 1.0, 1.0) + if self.app.style_manager.image_button(self.app.icons['scale'], (24, 24), tint_col=tint_col): + self.app.tool_manager.setCurrentTool("缩放") + if imgui.is_item_hovered(): + imgui.set_tooltip("缩放工具 (R)") + else: + if imgui.button("缩放##scale_tool"): + self.app.tool_manager.setCurrentTool("缩放") + if scale_active: + draw_list = imgui.get_window_draw_list() + button_min = imgui.get_item_rect_min() + button_max = imgui.get_item_rect_max() + draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3))) + + imgui.same_line() + imgui.separator() + imgui.same_line() + + # 工具按钮已移除(导入、保存、播放) + + + def _draw_scene_tree(self): + """绘制场景树面板""" + # 使用更少的限制性标志,允许docking + flags = (imgui.WindowFlags_.no_collapse) + + with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags): + self.app.showSceneTree = True # 确保窗口保持打开 + + imgui.text("场景层级") + imgui.separator() + + # 构建动态场景树 + self._build_scene_tree() + + + def _build_scene_tree(self): + """构建动态场景树""" + # 渲染节点 + if imgui.tree_node("渲染"): + # 环境光 + if hasattr(self.app, 'ambient_light') and self.app.ambient_light: + self._draw_scene_node(self.app.ambient_light, "环境光", "light") + + # 聚光灯 + if hasattr(self.app, 'scene_manager') and self.app.scene_manager: + if hasattr(self.app.scene_manager, 'Spotlight') and self.app.scene_manager.Spotlight: + for i, spotlight in enumerate(self.app.scene_manager.Spotlight): + self._draw_scene_node(spotlight, f"聚光灯_{i+1}", "light") + if hasattr(self.app.scene_manager, 'Pointlight') and self.app.scene_manager.Pointlight: + for i, pointlight in enumerate(self.app.scene_manager.Pointlight): + self._draw_scene_node(pointlight, f"点光源_{i+1}", "light") + + # 地板 + if hasattr(self.app, 'ground') and self.app.ground: + self._draw_scene_node(self.app.ground, "地板", "geometry") + + imgui.tree_pop() + + # 相机节点 + if imgui.tree_node("相机"): + if hasattr(self.app, 'camera') and self.app.camera: + self._draw_scene_node(self.app.camera, "主相机", "camera") + imgui.tree_pop() + + # 3D模型节点 + if imgui.tree_node("模型"): + models = [] + if hasattr(self.app, 'scene_manager') and self.app.scene_manager and hasattr(self.app.scene_manager, 'models'): + models.extend([m for m in self.app.scene_manager.models if m and not m.isEmpty()]) + + # SSBO模式下,模型可能不在 scene_manager.models 中,补充显示 ssbo_editor.model + ssbo_editor = getattr(self.app, "ssbo_editor", None) + ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None + if ssbo_model and not ssbo_model.isEmpty() and ssbo_model not in models: + models.append(ssbo_model) + + if models: + for i, model in enumerate(models): + self._draw_scene_node(model, model.getName() or f"模型_{i+1}", "model") + else: + imgui.text("(空)") + imgui.tree_pop() + + # if imgui.tree_node("GUI元素"): + # if hasattr(self,'gui_manager') and self.app.gui_manager and hasattr(self.app.gui_manager,'gui_elements'): + # if self.app.gui_manager.gui_elements: + # for gui_element in self.app.gui_manager.gui_elements: + # if gui_element and hasattr(gui_element,'node'): + # gui_type = getattr(gui_element,'gui_type','GUI_UNKNOWN') + # display_name = getattr(gui_element,'name',gui_type) + # self._draw_scene_node(gui_element.node,display_name,"gui",gui_type) + # else: + # imgui.text("(空)") + # else: + # imgui.text("(空)") + # imgui.tree_pop() + + # LUI元素节点 + if imgui.tree_node("GUI元素"): + if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled: + self.app.lui_manager.draw_component_tree() + imgui.tree_pop() + # if imgui.tree_node("LUI元素"): + # if hasattr(self.app, 'lui_manager') and self.app.lui_manager.lui_enabled: + # if self.app.lui_manager.components: + # for comp in self.app.lui_manager.components: + # if 'node' in comp: + # self._draw_scene_node(comp['node'], comp['name'], "ui") + + # if self.app.lui_manager.canvases: + # for canvas in self.app.lui_manager.canvases: + # if imgui.tree_node(f"Canvas: {canvas['name']}"): + # # 实际上组件已经在 node 下了,可以通过 _draw_scene_node 递归显示 + # # 但为了清晰,我们可以手动列出或者依赖递归 + # self._draw_scene_node(canvas['node'], canvas['name'], "geometry") + # imgui.tree_pop() + # else: + # imgui.text("(空)") + # else: + # imgui.text("(空)") + # imgui.tree_pop() + + + def _draw_scene_node(self, node, name, node_type, gui_subtype=None): + """绘制单个场景节点""" + if not node or node.isEmpty(): + return + + # 检查是否被选中 + is_selected = (hasattr(self.app, 'selection') and self.app.selection and + hasattr(self.app.selection, 'selectedNode') and + self.app.selection.selectedNode == node) + + # 节点可见性 + is_visible = node.is_hidden() == False + + # 设置选择颜色 + if is_selected: + imgui.push_style_color(imgui.Col_.text, (0.2, 0.6, 1.0, 1.0)) + + node_open = False + try: + # 显示节点 + node_open = imgui.tree_node(name) + + # 处理节点选择 + if imgui.is_item_clicked(): + if hasattr(self.app, 'selection') and self.app.selection: + self.app.selection.updateSelection(node) + # Clear LUI selection when a scene node is selected + if hasattr(self.app, 'lui_manager'): + self.app.lui_manager.selected_index = -1 + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + self._show_node_context_menu(node, name, node_type) + + # 显示节点属性 + imgui.same_line() + if is_visible: + imgui.text_colored((0.5, 1.0, 0.5, 1.0), "可见") + else: + imgui.text_colored((0.5, 0.5, 0.5, 1.0), "隐藏") + + if node_open: + # SSBO模型使用虚拟层级显示(避免 flatten 后真实子级丢失) + if self._draw_ssbo_virtual_children(node): + pass + elif node.getNumChildren() > 0: + for i in range(node.getNumChildren()): + child = node.getChild(i) + if not child or child.isEmpty(): + continue + child_name = child.getName() or f"child_{i+1}" + # 过滤碰撞辅助节点,避免污染场景树 + if child_name.startswith("modelCollision_"): + continue + self._draw_scene_node(child, child_name, node_type) + # tree_pop moved to finally + except Exception as e: + print(f"绘制场景节点时出错: {e}") + finally: + if node_open: + imgui.tree_pop() + # Ensure style stack is balanced. + if is_selected: + imgui.pop_style_color() + + def _draw_ssbo_virtual_children(self, node): + """Draw SSBO controller nodes as virtual children for scene tree.""" + ssbo_editor = getattr(self.app, "ssbo_editor", None) + if not ssbo_editor: + return False + model = getattr(ssbo_editor, "model", None) + controller = getattr(ssbo_editor, "controller", None) + if not model or model != node or not controller: + return False + + node_list = getattr(controller, "node_list", None) or [] + if not node_list: + imgui.text_disabled("(无可用子节点)") + return True + + selected_key = getattr(ssbo_editor, "selected_name", None) + for idx, key in enumerate(node_list): + display = controller.display_names.get(key, key.split("/")[-1]) + is_selected = (selected_key == key) + if imgui.selectable(f"{display}##ssbo_node_{idx}", is_selected)[0]: + ssbo_editor.select_node(key) + # 与旧系统保持一致:选择场景树项时清理LUI选择 + if hasattr(self.app, "lui_manager"): + self.app.lui_manager.selected_index = -1 + return True + def _show_node_context_menu(self, node, name, node_type): + """显示节点右键菜单""" + self.app._context_menu_node = True + self.app._context_menu_target = node + + + def _draw_resource_manager(self): + """绘制资源管理器面板""" + # 使用面板类型的窗口标志,支持docking + flags = self.app.style_manager.get_window_flags("panel") + + with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags): + self.app.showResourceManager = True # 确保窗口保持打开 + + # 获取资源管理器实例 + rm = self.app.resource_manager + + # 工具栏 + imgui.text("文件浏览器") + imgui.separator() + + # 导航按钮 + if imgui.button("◀"): + rm.navigate_back() + imgui.same_line() + if imgui.button("▶"): + rm.navigate_forward() + imgui.same_line() + if imgui.button("▲"): + rm.navigate_up() + imgui.same_line() + if imgui.button("主页"): + rm.navigate_to(rm.project_root / "Resources") + imgui.same_line() + if imgui.button("刷新"): + rm.force_refresh() + + # 自动刷新开关 + imgui.same_line() + changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled) + if changed: + rm.set_auto_refresh(rm.auto_refresh_enabled) + + imgui.same_line() + imgui.text(" ") + imgui.same_line() + + # 路径输入框 + changed, new_path = imgui.input_text("路径", str(rm.current_path), 256) + if changed: + try: + rm.navigate_to(Path(new_path)) + except: + pass + + # 搜索框 + changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256) + + imgui.separator() + + # 检查自动刷新 + if rm.refresh_if_needed(): + # 目录内容发生变化,可以在这里添加通知逻辑 + pass + + # 获取目录内容 + dirs, files = rm.get_directory_contents(rm.current_path) + + # 显示目录 + for dir_path in dirs: + if not rm.should_show_file(dir_path): + continue + + # 目录图标和名称 + icon_name = rm.get_file_icon(dir_path.name, is_folder=True) + node_open = False + + # 检查是否被选中 + is_selected = dir_path in rm.selected_files + + # 使用TreeNode来显示目录 + if is_selected: + imgui.push_style_color(imgui.Col_.header, (100/255, 150/255, 200/255, 1.0)) + + # 尝试加载PNG图标 + icon_texture = None + try: + # 直接使用图标名称,load_icon会自动添加.png + icon_texture = self.app.style_manager.load_icon(f"file_types/{icon_name}") + except: + pass + + if icon_texture: + # 使用PNG图标 + imgui.image(icon_texture, (16, 16)) + imgui.same_line() + node_open = imgui.tree_node(f"{dir_path.name}") + else: + # 回退到文本标识符 + node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}") + + if is_selected: + imgui.pop_style_color() + + # 处理选择 + if imgui.is_item_clicked(): + if imgui.get_io().key_ctrl: + # 多选模式 + if is_selected: + rm.selected_files.discard(dir_path) + else: + rm.selected_files.add(dir_path) + else: + # 单选模式 + rm.selected_files.clear() + rm.selected_files.add(dir_path) + rm.focused_file = dir_path + + # 双击导航 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + rm.navigate_to(dir_path) + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + rm.show_context_menu = True + rm.context_menu_file = dir_path + rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) + + # 如果节点展开,显示子内容 + if node_open: + # 获取子目录内容 + subdirs, subfiles = rm.get_directory_contents(dir_path) + + # 显示子目录 + for subdir in subdirs: + if not rm.should_show_file(subdir): + continue + + # 初始化变量 + subicon_name = "folder" + sub_is_selected = False + + # 获取子目录图标名称 + subicon_name = rm.get_file_icon(subdir.name, is_folder=True) + sub_is_selected = subdir in rm.selected_files + + # 尝试加载PNG图标 + subicon_texture = None + try: + subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}") + except: + pass + + if subicon_texture: + # 使用PNG图标 + imgui.image(subicon_texture, (16, 16)) + imgui.same_line() + sub_node_open = imgui.tree_node(f" {subdir.name}") + else: + # 回退到文本标识符 + sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}") + + if sub_is_selected: + imgui.pop_style_color() + + # 处理子目录的选择 + if imgui.is_item_clicked(): + if imgui.get_io().key_ctrl: + if sub_is_selected: + rm.selected_files.discard(subdir) + else: + rm.selected_files.add(subdir) + else: + rm.selected_files.clear() + rm.selected_files.add(subdir) + rm.focused_file = subdir + + # 双击子目录导航 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + rm.navigate_to(subdir) + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + rm.show_context_menu = True + rm.context_menu_file = subdir + rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) + + if sub_node_open: + imgui.tree_pop() + + # 显示子文件 + for subfile in subfiles: + if not rm.should_show_file(subfile): + continue + + subicon_name = rm.get_file_icon(subfile.name) + sub_is_selected = subfile in rm.selected_files + + # 尝试加载PNG图标 + subicon_texture = None + try: + subicon_texture = self.app.style_manager.load_icon(f"file_types/{subicon_name}") + except: + pass + + if subicon_texture: + # 使用PNG图标 + imgui.image(subicon_texture, (16, 16)) + imgui.same_line() + selected = imgui.selectable(f" {subfile.name}", sub_is_selected) + else: + # 回退到文本标识符 + selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}", sub_is_selected) + + # 处理子文件的选择 + if selected: + if imgui.get_io().key_ctrl: + if sub_is_selected: + rm.selected_files.discard(subfile) + else: + rm.selected_files.add(subfile) + else: + rm.selected_files.clear() + rm.selected_files.add(subfile) + rm.focused_file = subfile + + # 双击子文件操作 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + if subfile.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + self.app._import_model_for_runtime(str(subfile)) + self.app.add_info_message(f"正在导入模型: {subfile.name}") + else: + rm.open_file(subfile) + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + rm.show_context_menu = True + rm.context_menu_file = subfile + rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) + + # 只有在节点展开时才调用tree_pop + if node_open: + imgui.tree_pop() + + + + # 处理拖拽开始 + if imgui.is_item_active() and imgui.is_item_hovered(): + if imgui.is_mouse_dragging(0): + # 开始拖拽 + drag_files = list(rm.selected_files) if rm.selected_files else [file_path] + rm.start_drag(drag_files) + self.app.is_dragging = True + self.app.show_drag_overlay = True + + # 双击打开文件 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + # 检查是否是支持的3D模型格式 + if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + # 导入3D模型 + self.app.add_info_message(f"正在导入模型: {file_path.name}") + self.app._import_model_for_runtime(str(file_path)) + else: + # 使用系统默认程序打开 + rm.open_file(file_path) + + # 右键菜单 + if imgui.is_item_hovered() and imgui.is_mouse_clicked(1): + rm.show_context_menu = True + rm.context_menu_file = file_path + rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y) + + # 右键菜单 + if rm.show_context_menu and rm.context_menu_file: + imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1])) + with imgui_ctx.begin_popup("context_menu", imgui.WindowFlags_.no_title_bar | + imgui.WindowFlags_.no_resize | imgui.WindowFlags_.always_auto_resize) as popup: + if popup: + if rm.context_menu_file.is_dir(): + if imgui.menu_item("打开"): + rm.navigate_to(rm.context_menu_file) + imgui.separator() + if imgui.menu_item("重命名"): + print(f"重命名文件夹: {rm.context_menu_file.name}") + if imgui.menu_item("删除"): + print(f"删除文件夹: {rm.context_menu_file.name}") + else: + if imgui.menu_item("打开"): + rm.open_file(rm.context_menu_file) + imgui.separator() + if imgui.menu_item("导入到场景"): + if rm.context_menu_file.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + self.app.add_info_message(f"正在导入模型: {rm.context_menu_file.name}") + self.app._import_model_for_runtime(str(rm.context_menu_file)) + if imgui.menu_item("重命名"): + print(f"重命名文件: {rm.context_menu_file.name}") + if imgui.menu_item("删除"): + print(f"删除文件: {rm.context_menu_file.name}") + + imgui.separator() + if imgui.menu_item("复制路径"): + imgui.set_clipboard_text(str(rm.context_menu_file)) + self.app.add_info_message("路径已复制到剪贴板") + if imgui.menu_item("在文件管理器中显示"): + import platform + import subprocess + if platform.system() == "Windows": + subprocess.run(["explorer", "/select,", str(rm.context_menu_file)]) + elif platform.system() == "Darwin": + subprocess.run(["open", "-R", str(rm.context_menu_file)]) + else: + subprocess.run(["xdg-open", str(rm.context_menu_file.parent)]) + + # 如果点击其他地方,关闭菜单 + if imgui.is_mouse_clicked(0) or imgui.is_mouse_clicked(1): + if not imgui.is_window_hovered(): + rm.show_context_menu = False + rm.context_menu_file = None + + + def _draw_property_panel(self): + """绘制属性面板""" + # 使用面板类型的窗口标志,支持docking + flags = self.app.style_manager.get_window_flags("panel") + + with self.app.style_manager.begin_styled_window("属性面板", self.app.showPropertyPanel, flags): + self.app.showPropertyPanel = True # 确保窗口保持打开 + + # --- LUI Component Properties --- + # 优先检查 LUI 组件选择 + if hasattr(self.app, 'lui_manager') and self.app.lui_manager.selected_index >= 0: + if self.app.lui_manager.luiFunction: + self.app.lui_manager.luiFunction._draw_component_properties(self.app.lui_manager, self.app.lui_manager.selected_index) + return + + # --- Scene Node Properties --- + # 获取当前选中的节点 + selected_node = None + if hasattr(self.app, 'selection') and self.app.selection and hasattr(self.app.selection, 'selectedNode'): + selected_node = self.app.selection.selectedNode + + if selected_node and not selected_node.isEmpty(): + self._draw_node_properties(selected_node) + else: + # 无选中对象时显示提示(模仿Qt版本的空状态样式) + imgui.spacing() + imgui.spacing() + + # 居中显示提示信息 + window_width = imgui.get_window_width() + text_width = 200 # 估算文本宽度 + text_pos_x = (window_width - text_width) / 2 + + imgui.set_cursor_pos_x(text_pos_x) + imgui.text_colored((0.5, 0.5, 0.5, 1.0), "🔍 未选择任何对象") + + imgui.set_cursor_pos_x(text_pos_x - 20) + imgui.text("请从场景树中选择一个对象") + + imgui.set_cursor_pos_x(text_pos_x + 10) + imgui.text("以查看其属性") + + imgui.spacing() + imgui.spacing() + + # 添加一些分隔线和装饰 + imgui.separator() + + # 显示快速提示 + imgui.text("💡 快速提示:") + imgui.bullet_text("单击场景树中的对象进行选择") + imgui.bullet_text("使用 F 键快速聚焦到选中对象") + imgui.bullet_text("使用 Delete 键删除选中对象") + + + def _draw_node_properties(self, node): + """绘制节点属性""" + if not node or node.isEmpty(): + # 停止变换监控 + self.app.stop_transform_monitoring() + return + + # 检查是否需要重新启动变换监控 + if self.app._monitored_node != node: + self.app.stop_transform_monitoring() + self.app.start_transform_monitoring(node) + + # 获取节点基本信息 + node_name = node.getName() or "未命名节点" + node_type = self.app._get_node_type_from_node(node) + + # 添加一些间距,模仿Qt版本的布局 + imgui.spacing() + + # 物体名称组(使用Qt版本的样式) + if imgui.collapsing_header("物体名称"): + # 第一行:可见性复选框和名称输入 + user_visible = node.getPythonTag("user_visible") + if user_visible is None: + user_visible = True + node.setPythonTag("user_visible", True) + + # 可见性复选框(模仿Qt版本的样式) + changed, is_visible = imgui.checkbox("##visibility", user_visible) + if changed: + node.setPythonTag("user_visible", is_visible) + if is_visible: + node.show() + else: + node.hide() + + imgui.same_line() + imgui.text("可见") + imgui.same_line() + imgui.spacing() + imgui.same_line() + + # 名称输入框(模仿Qt版本的样式) + imgui.text("名称:") + imgui.same_line() + changed, new_name = imgui.input_text("##name", node_name, 256) + if changed and hasattr(self.app, 'selection'): + # 更新场景树中的名称 + self.app._update_node_name(node, new_name) + + # 添加分隔线 + imgui.separator() + + # 状态徽章(模仿Qt版本的徽章样式) + self.app._draw_status_badges(node) + + imgui.spacing() + + # 变换属性组 + if imgui.collapsing_header("变换 Transform"): + self.app._draw_transform_properties(node) + + # 根据节点类型显示特定属性组 + if node_type == "GUI元素": + if imgui.collapsing_header("GUI信息"): + self.app._draw_gui_properties(node) + elif node_type == "光源": + if imgui.collapsing_header("光源属性"): + self.app._draw_light_properties(node) + elif node_type == "模型": + if imgui.collapsing_header("模型属性"): + self.app._draw_model_properties(node) + + # 动画控制组(只对模型显示) + if imgui.collapsing_header("动画控制"): + self.app._draw_animation_properties(node) + + # 外观属性组(通用) + if imgui.collapsing_header("外观属性"): + self.app._draw_appearance_properties(node) + + # 碰撞检测组 + if imgui.collapsing_header("碰撞检测"): + self.app._draw_collision_properties(node) + + # 操作按钮组 + if imgui.collapsing_header("操作"): + self.app._draw_property_actions(node) + + def _draw_status_badges(self, node): + """绘制状态徽章(模仿Qt版本的徽章样式)""" + imgui.text("状态标签: ") + + # 可见性状态徽章 + is_visible = not node.is_hidden() + visibility_color = (0.176, 1.0, 0.769, 1.0) if is_visible else (0.953, 0.616, 0.471, 1.0) + visibility_text = "可见" if is_visible else "隐藏" + + imgui.same_line() + imgui.text_colored(visibility_color, f"[{visibility_text}]") + + # 节点类型徽章 + node_type = self._get_node_type_from_node(node) + type_colors = { + "GUI元素": (0.188, 0.404, 0.753, 1.0), # 主题蓝色 + "光源": (1.0, 0.8, 0.2, 1.0), # 黄色 + "模型": (0.6, 0.8, 1.0, 1.0), # 浅蓝色 + "相机": (0.8, 0.8, 0.2, 1.0), # 橙色 + "几何体": (0.5, 0.5, 0.5, 1.0), # 灰色 + } + + if node_type in type_colors: + imgui.same_line() + imgui.text_colored(type_colors[node_type], f"[{node_type}]") + + # 功能性徽章 + badges = [] + + # 碰撞体徽章 + has_collision = hasattr(node, 'getChild') and any('Collision' in child.getName() for child in node.getChildren() if child.getName()) + if has_collision: + badges.append(("碰撞", (0.2, 0.4, 0.8, 1.0))) # 蓝色 + + # 脚本徽章 + has_script = hasattr(node, 'getPythonTag') and node.getPythonTag('script') + if has_script: + badges.append(("脚本", (0.8, 0.4, 0.8, 1.0))) # 紫色 + + # 动画徽章(优化检测逻辑,避免重复创建Actor) + has_animation = False + if node_type == "模型": # 只对模型类型进行动画检测 + # 首先检查是否已经缓存了检测结果 + cached_result = node.getPythonTag('animation') + if cached_result is not None: + has_animation = cached_result + else: + # 只有在未缓存时才进行检测 + try: + # 使用轻量级检测:先检查文件扩展名 + model_path = node.getTag("model_path") + if model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx')): + # 对于可能包含动画的格式,才进行Actor检测 + actor = self._getActor(node) + if actor and actor.getAnimNames(): + has_animation = True + # 缓存检测结果 + node.setPythonTag('animation', has_animation) + print(f"[动画检测] {node.getName()}: {'有动画' if has_animation else '无动画'}") + else: + # 对于不太可能有动画的格式,直接标记为无动画 + node.setPythonTag('animation', False) + except Exception as e: + print(f"动画检测失败: {e}") + node.setPythonTag('animation', False) + else: + # 对于非模型类型,检查已有的动画标签 + has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation') + + if has_animation: + badges.append(("动画", (0.4, 0.8, 0.4, 1.0))) # 绿色 + + # 材质徽章 + has_material = hasattr(node, 'getMaterial') and node.getMaterial() + if has_material: + badges.append(("材质", (0.8, 0.6, 0.2, 1.0))) # 金色 + + # 绘制功能性徽章 + for badge_text, badge_color in badges: + imgui.same_line() + imgui.text_colored(badge_color, f"[{badge_text}]") + + # 如果没有特殊徽章,显示默认状态 + if not badges: + imgui.same_line() + imgui.text_colored((0.5, 0.5, 0.5, 1.0), "[标准对象]") + + + def _draw_transform_properties(self, node): + """绘制变换属性""" + # 位置组 + if imgui.collapsing_header("位置 Position"): + # 相对位置 + imgui.text("相对位置") + pos = node.getPos() + + # X坐标 + changed, new_x = imgui.input_float("X##pos_x", pos.x, 0.1, 1.0, "%.3f") + if changed: node.setX(new_x) + + # Y坐标 + changed, new_y = imgui.input_float("Y##pos_y", pos.y, 0.1, 1.0, "%.3f") + if changed: node.setY(new_y) + + # Z坐标 + changed, new_z = imgui.input_float("Z##pos_z", pos.z, 0.1, 1.0, "%.3f") + if changed: node.setZ(new_z) + + # 世界位置 + imgui.text("世界位置") + world_pos = node.getPos(self.render) + + imgui.text(f"世界 X: {world_pos.x:.3f}") + imgui.text(f"世界 Y: {world_pos.y:.3f}") + imgui.text(f"世界 Z: {world_pos.z:.3f}") + + # 位置操作按钮 + if imgui.button("重置位置##reset_pos"): + node.setPos(0, 0, 0) + imgui.same_line() + if imgui.button("复制位置##copy_pos"): + self._clipboard_pos = (pos.x, pos.y, pos.z) + imgui.same_line() + if imgui.button("粘贴位置##paste_pos") and hasattr(self, '_clipboard_pos'): + node.setPos(self._clipboard_pos[0], self._clipboard_pos[1], self._clipboard_pos[2]) + + # 旋转组 + if imgui.collapsing_header("旋转 Rotation"): + hpr = node.getHpr() + + # HPR旋转 + imgui.text("HPR 旋转 (度)") + changed, new_h = imgui.input_float("H##rot_h", hpr.x, 1.0, 10.0, "%.1f") + if changed: node.setH(new_h) + + changed, new_p = imgui.input_float("P##rot_p", hpr.y, 1.0, 10.0, "%.1f") + if changed: node.setP(new_p) + + changed, new_r = imgui.input_float("R##rot_r", hpr.z, 1.0, 10.0, "%.1f") + if changed: node.setR(new_r) + + # 旋转操作按钮 + if imgui.button("重置旋转##reset_rot"): + node.setHpr(0, 0, 0) + imgui.same_line() + if imgui.button("随机旋转##random_rot"): + import random + node.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360)) + + # 缩放组 + if imgui.collapsing_header("缩放 Scale"): + scale = node.getScale() + + # XYZ缩放 + imgui.text("XYZ 缩放") + changed, new_sx = imgui.input_float("X##scale_x", scale.x, 0.1, 1.0, "%.3f") + if changed: node.setSx(new_sx) + + changed, new_sy = imgui.input_float("Y##scale_y", scale.y, 0.1, 1.0, "%.3f") + if changed: node.setSy(new_sy) + + changed, new_sz = imgui.input_float("Z##scale_z", scale.z, 0.1, 1.0, "%.3f") + if changed: node.setSz(new_sz) + + # 统一缩放 + if imgui.button("统一缩放##uniform_scale"): + uniform_scale = (scale.x + scale.y + scale.z) / 3.0 + node.setScale(uniform_scale, uniform_scale, uniform_scale) + imgui.same_line() + if imgui.button("重置缩放##reset_scale"): + node.setScale(1, 1, 1) + imgui.same_line() + if imgui.button("翻倍##double_scale"): + node.setScale(scale.x * 2, scale.y * 2, scale.z * 2) + + + def _draw_gui_properties(self, node): + """绘制GUI元素属性""" + # 获取GUI元素 + gui_element = None + if hasattr(node, 'getPythonTag'): + gui_element = node.getPythonTag('gui_element') + + if not gui_element: + imgui.text("无GUI元素数据") + return + + # GUI类型信息 + gui_type = getattr(gui_element, 'gui_type', 'UNKNOWN') + imgui.text(f"GUI类型: {gui_type}") + + # 基本属性 + if imgui.collapsing_header("基本属性"): + # 文本内容 (适用于按钮、标签等) + if hasattr(gui_element, 'text'): + changed, new_text = imgui.input_text("文本内容", gui_element.text, 256) + if changed and hasattr(self, 'gui_manager'): + self.gui_manager.editGUIElement(gui_element, 'text', new_text) + + # GUI ID + gui_id = getattr(gui_element, 'id', '') + changed, new_id = imgui.input_text("GUI ID", gui_id, 64) + if changed and hasattr(self, 'gui_manager'): + gui_element.id = new_id + + # 变换属性 + if imgui.collapsing_header("变换属性"): + # 位置 + pos = gui_element.getPos() + imgui.text("位置") + + if gui_type in ["button", "label", "entry", "2d_image"]: + # 2D GUI组件使用屏幕坐标 + imgui.text("屏幕坐标") + logical_x = pos.getX() / 0.1 + logical_z = pos.getZ() / 0.1 + + changed, new_x = imgui.input_float("X##gui_pos_x", logical_x, 1.0, 10.0, "%.1f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setX(new_x * 0.1) + + changed, new_z = imgui.input_float("Y##gui_pos_y", logical_z, 1.0, 10.0, "%.1f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setZ(new_z * 0.1) + else: + # 3D GUI组件使用世界坐标 + imgui.text("世界坐标") + changed, new_x = imgui.input_float("X##gui_world_x", pos.getX(), 0.1, 1.0, "%.3f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setX(new_x) + + changed, new_y = imgui.input_float("Y##gui_world_y", pos.getY(), 0.1, 1.0, "%.3f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setY(new_y) + + changed, new_z = imgui.input_float("Z##gui_world_z", pos.getZ(), 0.1, 1.0, "%.3f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setZ(new_z) + + # 缩放 + scale = gui_element.getScale() + imgui.text("缩放") + changed, new_sx = imgui.input_float("X##gui_scale_x", scale.getX(), 0.1, 1.0, "%.3f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setSx(new_sx) + + changed, new_sy = imgui.input_float("Y##gui_scale_y", scale.getY(), 0.1, 1.0, "%.3f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setSy(new_sy) + + changed, new_sz = imgui.input_float("Z##gui_scale_z", scale.getZ(), 0.1, 1.0, "%.3f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setSz(new_sz) + + # 旋转 + hpr = gui_element.getHpr() + imgui.text("旋转") + changed, new_h = imgui.input_float("H##gui_rot_h", hpr.getX(), 1.0, 10.0, "%.1f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setH(new_h) + + changed, new_p = imgui.input_float("P##gui_rot_p", hpr.getY(), 1.0, 10.0, "%.1f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setP(new_p) + + changed, new_r = imgui.input_float("R##gui_rot_r", hpr.getZ(), 1.0, 10.0, "%.1f") + if changed and hasattr(self, 'gui_manager'): + gui_element.setR(new_r) + + # 外观属性 + if imgui.collapsing_header("外观属性"): + # 大小 + if hasattr(gui_element, 'size'): + size = gui_element.size + imgui.text("大小") + changed, new_w = imgui.input_float("宽度", size[0], 1.0, 10.0, "%.1f") + if changed and hasattr(self, 'gui_manager'): + new_size = (new_w, size[1]) + self.gui_manager.editGUIElement(gui_element, 'size', new_size) + + changed, new_h = imgui.input_float("高度", size[1], 1.0, 10.0, "%.1f") + if changed and hasattr(self, 'gui_manager'): + new_size = (size[0], new_h) + self.gui_manager.editGUIElement(gui_element, 'size', new_size) + + # 颜色 + if hasattr(gui_element, 'getColor'): + try: + color = gui_element.getColor() + # 确保颜色是有效的 + if not color or (hasattr(color, '__len__') and len(color) < 3): + color = (1.0, 1.0, 1.0, 1.0) # 默认白色 + except: + color = (1.0, 1.0, 1.0, 1.0) # 默认白色 + + imgui.text("颜色") + # 获取颜色值 + if hasattr(color, 'getX'): + # 如果是Panda3D的Vec4对象 + r, g, b = color.getX(), color.getY(), color.getZ() + else: + # 如果是元组或其他格式 + r, g, b = color[0], color[1], color[2] + + changed, new_r = imgui.slider_float("R##gui_color_r", r, 0.0, 1.0) + if changed: gui_element.setColor(new_r, g, b, 1.0) + + changed, new_g = imgui.slider_float("G##gui_color_g", g, 0.0, 1.0) + if changed: gui_element.setColor(r, new_g, b, 1.0) + + changed, new_b = imgui.slider_float("B##gui_color_b", b, 0.0, 1.0) + if changed: gui_element.setColor(r, g, new_b, 1.0) + # 透明度 + imgui.text("透明度") + current_alpha = getattr(gui_element, 'alpha', 1.0) + changed, new_alpha = imgui.slider_float("Alpha", current_alpha, 0.0, 1.0) + if changed: + gui_element.alpha = new_alpha + if hasattr(gui_element, 'setTransparency'): + # 将0.0-1.0范围转换为Panda3D的透明度格式 + panda_transparency = int((1.0 - new_alpha) * 255) + gui_element.setTransparency(panda_transparency) + + # 渲染顺序 + imgui.text("渲染顺序") + current_sort = getattr(gui_element, 'sort', 0) + changed, new_sort = imgui.input_int("Sort Order", current_sort) + if changed: + gui_element.sort = new_sort + if hasattr(gui_element, 'setBin'): + gui_element.setBin('fixed', new_sort) + + # 字体设置(适用于文本类型的GUI元素) + if gui_type in ["button", "label", "entry"]: + imgui.text("字体设置") + + # 字体选择 + current_font = getattr(gui_element, 'font_path', '') + if imgui.button(f"字体: {Path(current_font).name if current_font else '默认'}##font_select"): + self.show_font_selector( + gui_element, + 'font_path', + current_font, + lambda font_path: self._apply_gui_font(gui_element, font_path) + ) + + # 字体大小 + current_size = getattr(gui_element, 'font_size', 12) + changed, new_size = imgui.slider_float("字体大小", current_size, 8.0, 72.0) + if changed: + gui_element.font_size = new_size + self._apply_gui_font_size(gui_element, new_size) + + # 字体样式 + imgui.text("字体样式") + is_bold = getattr(gui_element, 'font_bold', False) + changed, new_bold = imgui.checkbox("粗体", is_bold) + if changed: + gui_element.font_bold = new_bold + self._apply_gui_font_style(gui_element) + + imgui.same_line() + is_italic = getattr(gui_element, 'font_italic', False) + changed, new_italic = imgui.checkbox("斜体", is_italic) + if changed: + gui_element.font_italic = new_italic + self._apply_gui_font_style(gui_element) + + + def _draw_light_properties(self, node): + """绘制光源属性""" + imgui.text("光源属性") + + # 光源颜色 + if hasattr(node, 'getColor'): + try: + color = node.getColor() + # 确保颜色是有效的 + if not color or len(color) < 3: + color = (1.0, 1.0, 1.0, 1.0) # 默认白色 + except: + color = (1.0, 1.0, 1.0, 1.0) # 默认白色 + + changed, new_r = imgui.drag_float("颜色 R", color[0], 0.01, 0.0, 1.0) + if changed: node.setColor(new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0) + + changed, new_g = imgui.drag_float("颜色 G", color[1], 0.01, 0.0, 1.0) + if changed: node.setColor(color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0) + + changed, new_b = imgui.drag_float("颜色 B", color[2], 0.01, 0.0, 1.0) + if changed: node.setColor(color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0) + + # 光源强度 + imgui.text("光源强度: (暂不支持编辑)") + + + def _draw_model_properties(self, node): + """绘制模型属性""" + # 获取模型信息 + model_path = node.getTag("model_path") if node.hasTag("model_path") else "未知" + + imgui.text("模型路径:") + imgui.same_line() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_path) + + # 模型基本信息 + imgui.text("模型名称:") + imgui.same_line() + model_name = node.getName() or "未命名模型" + imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_name) + + # 模型位置信息 + imgui.text("位置:") + imgui.same_line() + pos = node.getPos() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{pos.x:.2f} Y:{pos.y:.2f} Z:{pos.z:.2f}") + + # 模型缩放信息 + imgui.text("缩放:") + imgui.same_line() + scale = node.getScale() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{scale.x:.2f} Y:{scale.y:.2f} Z:{scale.z:.2f}") + + # 模型旋转信息 + imgui.text("旋转:") + imgui.same_line() + hpr = node.getHpr() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"H:{hpr.x:.1f}° P:{hpr.y:.1f}° R:{hpr.z:.1f}°") + + + def _draw_animation_properties(self, node): + """绘制动画控制属性面板(优化版本,使用缓存避免重复计算)""" + # 检查是否已经缓存了动画信息 + cached_anim_info = node.getPythonTag("cached_anim_info") + cached_processed_names = node.getPythonTag("cached_processed_names") + + # 只有在没有缓存时才进行完整的动画检测和处理 + if cached_anim_info is None or cached_processed_names is None: + # 获取Actor + actor = self._getActor(node) + if not actor: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画") + return + + # 获取和分析动画名称 + anim_names = actor.getAnimNames() + processed_names = self._processAnimationNames(node, anim_names) + + if not processed_names: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列") + # 缓存空结果 + node.setPythonTag("cached_processed_names", []) + node.setPythonTag("cached_anim_info", "无动画") + return + + # 计算并缓存动画信息 + format_info = self._getModelFormat(node) + animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info) + info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}" + if animation_info: + info_text += f" | {animation_info}" + + # 缓存结果 + node.setPythonTag("cached_anim_info", info_text) + node.setPythonTag("cached_processed_names", processed_names) + + else: + # 使用缓存的数据 + info_text = cached_anim_info + processed_names = cached_processed_names + + # 如果缓存的空结果,直接返回 + if not processed_names: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列") + return + + # 显示动画信息(使用缓存的数据) + imgui.text("信息:") + imgui.same_line() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), info_text) + + imgui.spacing() + + # 动画选择下拉框 + imgui.text("动画名称:") + imgui.same_line() + + # 获取当前选中的动画 + current_anim = node.getPythonTag("selected_animation") + if current_anim is None: + current_anim = processed_names[0][1] if processed_names else "" + node.setPythonTag("selected_animation", current_anim) + + # 查找当前动画的索引 + current_index = 0 + for i, (display_name, original_name) in enumerate(processed_names): + if original_name == current_anim: + current_index = i + break + + # 创建下拉框选项 + animation_options = [display_name for display_name, _ in processed_names] + changed, new_index = imgui.combo("##animation_combo", current_index, animation_options) + + if changed and new_index < len(processed_names): + selected_display, selected_original = processed_names[new_index] + node.setPythonTag("selected_animation", selected_original) + print(f"选择动画: {selected_display} (原始名称: {selected_original})") + + imgui.spacing() + + # 控制按钮组 + imgui.text("控制:") + + # 播放按钮 + if imgui.button("播放##play_animation"): + self._playAnimation(node) + imgui.same_line() + + # 暂停按钮 + if imgui.button("暂停##pause_animation"): + self._pauseAnimation(node) + imgui.same_line() + + # 停止按钮 + if imgui.button("停止##stop_animation"): + self._stopAnimation(node) + imgui.same_line() + + # 循环按钮 + if imgui.button("循环##loop_animation"): + self._loopAnimation(node) + + imgui.spacing() + + # 播放速度控制 + imgui.text("播放速度:") + imgui.same_line() + + # 获取当前速度 + current_speed = node.getPythonTag("anim_speed") + if current_speed is None: + current_speed = 1.0 + node.setPythonTag("anim_speed", current_speed) + + # 速度滑块 + changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f") + if changed: + node.setPythonTag("anim_speed", new_speed) + self._setAnimationSpeed(node, new_speed) + + imgui.same_line() + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速") + + + def _draw_collision_properties(self, node): + """绘制碰撞检测属性""" + if not node or node.isEmpty(): + return + + try: + # 检查节点是否已有碰撞 + has_collision = self._has_collision(node) + + # 碰撞状态徽章 + imgui.text("状态:") + imgui.same_line() + if has_collision: + imgui.text_colored((0.0, 0.8, 0.0, 1.0), "🟢 已启用") + else: + imgui.text_colored((0.8, 0.0, 0.0, 1.0), "🔴 未启用") + + imgui.separator() + + # 碰撞形状选择 + imgui.text("碰撞形状:") + imgui.same_line() + + # 碰撞形状选项 + collision_shapes = ["球形 (Sphere)", "盒型 (Box)", "胶囊体 (Capsule)", "平面 (Plane)", "自动选择 (Auto)"] + + # 获取当前形状 + current_shape = self._get_current_collision_shape(node) if has_collision else "球形 (Sphere)" + + # 形状选择下拉框 + current_index = collision_shapes.index(current_shape) if current_shape in collision_shapes else 0 + changed, selected_index = imgui.combo("##collision_shape", current_index, collision_shapes) + if changed: + # 始终更新选择的形状 + selected_shape = collision_shapes[selected_index] + self._selected_collision_shape = selected_shape + # 如果已经有碰撞体,询问用户是否要重新创建 + if has_collision: + print(f"形状已更改为 {selected_shape},点击'移除碰撞'后'添加碰撞'来应用新形状") + + imgui.separator() + + # 位置偏移控件 + imgui.text("位置偏移:") + + # 获取当前位置偏移 + pos_offset = self._get_collision_position_offset(node) + + # X位置 + changed, new_x = imgui.drag_float("X##collision_pos_x", pos_offset[0], 0.1, -100.0, 100.0, "%.2f") + if changed and has_collision: + self._update_collision_position(node, 'x', new_x) + + # Y位置 + changed, new_y = imgui.drag_float("Y##collision_pos_y", pos_offset[1], 0.1, -100.0, 100.0, "%.2f") + if changed and has_collision: + self._update_collision_position(node, 'y', new_y) + + # Z位置 + changed, new_z = imgui.drag_float("Z##collision_pos_z", pos_offset[2], 0.1, -100.0, 100.0, "%.2f") + if changed and has_collision: + self._update_collision_position(node, 'z', new_z) + + # 形状特定参数(始终显示,但根据状态启用/禁用) + shape_type = self._get_current_collision_shape_type(node) + self._draw_shape_specific_parameters(node, shape_type, has_collision) + + imgui.separator() + + # 操作按钮 + if has_collision: + # 显示/隐藏碰撞体按钮 + is_visible = self._is_collision_visible(node) + visibility_text = "隐藏碰撞体" if is_visible else "显示碰撞体" + if imgui.button(visibility_text): + self._toggle_collision_visibility(node) + + imgui.same_line() + + # 移除碰撞按钮 + if imgui.button("移除碰撞"): + self._remove_collision_from_node(node) + else: + # 添加碰撞按钮 + if imgui.button("添加碰撞"): + self._add_collision_to_node(node) + + imgui.separator() + + # 碰撞检测触发模式 + imgui.text("触发模式:") + + # 自动检测开关 + auto_enabled = self.collision_manager.model_collision_enabled if hasattr(self, 'collision_manager') else False + changed, new_auto = imgui.checkbox("自动检测", auto_enabled) + if changed and hasattr(self, 'collision_manager'): + self.collision_manager.enableModelCollisionDetection(new_auto, 0.1, 0.5) + + imgui.same_line() + + # 手动检测按钮 + if imgui.button("立即检测"): + if hasattr(self, 'collision_manager'): + self._manual_collision_detection() + + except Exception as e: + print(f"绘制碰撞属性失败: {e}") + import traceback + traceback.print_exc() + + + def _draw_shape_specific_parameters(self, node, shape_type, has_collision=True): + """绘制形状特定参数""" + try: + if shape_type == "sphere": + self._draw_sphere_parameters(node, has_collision) + elif shape_type == "box": + self._draw_box_parameters(node, has_collision) + elif shape_type == "capsule": + self._draw_capsule_parameters(node, has_collision) + elif shape_type == "plane": + self._draw_plane_parameters(node, has_collision) + except Exception as e: + print(f"绘制形状参数失败: {e}") + + + def _draw_sphere_parameters(self, node, has_collision=True): + """绘制球形参数""" + try: + imgui.text("球形参数:") + imgui.same_line() + + # 获取当前半径 + radius = self._get_sphere_radius(node) + + # 半径调整 + changed, new_radius = imgui.drag_float("半径##sphere_radius", radius, 0.1, 0.1, 100.0, "%.2f") + if changed and has_collision: + self._update_sphere_radius(node, new_radius) + + except Exception as e: + print(f"绘制球形参数失败: {e}") + + + def _draw_box_parameters(self, node, has_collision=True): + """绘制盒型参数""" + try: + imgui.text("盒型参数:") + + # 获取当前尺寸 + size = self._get_box_size(node) + + # 尺寸调整 + changed, new_x = imgui.drag_float("长度##box_length", size[0], 0.1, 0.1, 100.0, "%.2f") + if changed and has_collision: + self._update_box_size(node, 'x', new_x) + + changed, new_y = imgui.drag_float("宽度##box_width", size[1], 0.1, 0.1, 100.0, "%.2f") + if changed and has_collision: + self._update_box_size(node, 'y', new_y) + + changed, new_z = imgui.drag_float("高度##box_height", size[2], 0.1, 0.1, 100.0, "%.2f") + if changed and has_collision: + self._update_box_size(node, 'z', new_z) + + except Exception as e: + print(f"绘制盒型参数失败: {e}") + + + def _draw_capsule_parameters(self, node, has_collision=True): + """绘制胶囊体参数""" + try: + imgui.text("胶囊体参数:") + + # 获取当前参数 + radius = self._get_capsule_radius(node) + height = self._get_capsule_height(node) + + # 半径调整 + changed, new_radius = imgui.drag_float("半径##capsule_radius", radius, 0.1, 0.1, 100.0, "%.2f") + if changed and has_collision: + self._update_capsule_radius(node, new_radius) + + # 高度调整 + changed, new_height = imgui.drag_float("高度##capsule_height", height, 0.1, 0.1, 100.0, "%.2f") + if changed and has_collision: + self._update_capsule_height(node, new_height) + + except Exception as e: + print(f"绘制胶囊体参数失败: {e}") + + + def _draw_plane_parameters(self, node, has_collision=True): + """绘制平面参数""" + try: + imgui.text("平面参数:") + + # 获取当前法向量 + normal = self._get_plane_normal(node) + + # 法向量调整 + changed, new_x = imgui.drag_float("法向量 X##plane_normal_x", normal[0], 0.1, -1.0, 1.0, "%.2f") + if changed and has_collision: + self._update_plane_normal(node, 'x', new_x) + + changed, new_y = imgui.drag_float("法向量 Y##plane_normal_y", normal[1], 0.1, -1.0, 1.0, "%.2f") + if changed and has_collision: + self._update_plane_normal(node, 'y', new_y) + + changed, new_z = imgui.drag_float("法向量 Z##plane_normal_z", normal[2], 0.1, -1.0, 1.0, "%.2f") + if changed and has_collision: + self._update_plane_normal(node, 'z', new_z) + + except Exception as e: + print(f"绘制平面参数失败: {e}") + + + def _draw_property_actions(self, node): + """绘制属性操作按钮""" + # 重置变换 + if imgui.button("重置变换"): + node.setPos(0, 0, 0) + node.setHpr(0, 0, 0) + node.setScale(1, 1, 1) + + imgui.same_line() + + # 切换可见性 + is_visible = not node.is_hidden() + visibility_text = "隐藏" if is_visible else "显示" + if imgui.button(visibility_text): + if is_visible: + node.hide() + else: + node.show() + + imgui.same_line() + + # 聚焦到对象 + if imgui.button("聚焦"): + if hasattr(self, 'selection') and self.selection: + self.selection.focusCameraOnSelectedNodeAdvanced() + + # 删除对象 + imgui.same_line() + if imgui.button("删除"): + if hasattr(self, 'selection') and self.selection: + self.selection.deleteSelectedNode() + + + def _draw_appearance_properties(self, node): + """绘制外观属性""" + # 颜色属性 + if hasattr(node, 'getColor'): + imgui.text("颜色") + try: + color = node.getColor() + # 确保颜色是有效的 + if not color or len(color) < 3: + color = (1.0, 1.0, 1.0, 1.0) # 默认白色 + except: + color = (1.0, 1.0, 1.0, 1.0) # 默认白色 + + # 颜色滑块 + changed, new_r = imgui.slider_float("R##color_r", color[0], 0.0, 1.0) + if changed: + new_color = (new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0) + node.setColor(new_color) + color = new_color + + changed, new_g = imgui.slider_float("G##color_g", color[1], 0.0, 1.0) + if changed: + new_color = (color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0) + node.setColor(new_color) + color = new_color + + changed, new_b = imgui.slider_float("B##color_b", color[2], 0.0, 1.0) + if changed: + new_color = (color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0) + node.setColor(new_color) + color = new_color + + # 只有当颜色有alpha通道时才显示alpha滑块 + if len(color) > 3: + changed, new_a = imgui.slider_float("A##color_a", color[3], 0.0, 1.0) + if changed: + new_color = (color[0], color[1], color[2], new_a) + node.setColor(new_color) + color = new_color + + # 颜色预览和选择器 + imgui.text("颜色预览") + color_with_alpha = (color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0) + if imgui.color_button("颜色预览##preview", color_with_alpha, 0, (100, 30)): + # 点击颜色按钮打开颜色选择器 + self.show_color_picker(node, 'color', color_with_alpha) + + imgui.same_line() + if imgui.button("选择颜色##color_picker_btn"): + self.show_color_picker(node, 'color', (color.x, color.y, color.z, color.w)) + + # 透明度 + if hasattr(node, 'setTransparency') and hasattr(node, 'getTransparency'): + imgui.text("透明度") + current_transparency = node.getTransparency() + # 将当前的透明度值转换为0.0-1.0范围用于显示 + display_transparency = 1.0 - current_transparency if current_transparency <= 1 else 0.0 + changed, new_transparency = imgui.slider_float("透明度", display_transparency, 0.0, 1.0) + if changed: + # 将0.0-1.0范围转换回Panda3D的透明度格式 + panda_transparency = int((1.0 - new_transparency) * 255) + node.setTransparency(panda_transparency) + + # 材质属性 + self._draw_material_properties(node) + + # 渲染状态 + imgui.text("渲染状态") + if imgui.button("应用材质"): + self._apply_material_to_node(node) + + imgui.same_line() + if imgui.button("重置材质"): + self._reset_material(node) + + + def _draw_material_properties(self, node): + """绘制材质属性""" + materials = node.find_all_materials() + + if not materials: + imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质") + return + + for i, material in enumerate(materials): + material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" + + if imgui.collapsing_header(f"材质: {material_name}"): + # 材质基础颜色 + base_color = self._get_material_base_color(material) + if base_color: + imgui.text("基础颜色") + changed, new_r = imgui.slider_float(f"R##mat_r_{i}", base_color[0], 0.0, 1.0) + if changed: + self._update_material_base_color(material, 'r', new_r) + base_color = (new_r, base_color[1], base_color[2], base_color[3]) + + changed, new_g = imgui.slider_float(f"G##mat_g_{i}", base_color[1], 0.0, 1.0) + if changed: + self._update_material_base_color(material, 'g', new_g) + base_color = (base_color[0], new_g, base_color[2], base_color[3]) + + changed, new_b = imgui.slider_float(f"B##mat_b_{i}", base_color[2], 0.0, 1.0) + if changed: + self._update_material_base_color(material, 'b', new_b) + base_color = (base_color[0], base_color[1], new_b, base_color[3]) + + changed, new_a = imgui.slider_float(f"A##mat_a_{i}", base_color[3], 0.0, 1.0) + if changed: + self._update_material_base_color(material, 'a', new_a) + base_color = (base_color[0], base_color[1], base_color[2], new_a) + + # PBR属性 + if hasattr(material, 'roughness') and material.roughness is not None: + imgui.text("PBR属性") + try: + roughness_value = float(material.roughness) + changed, new_roughness = imgui.slider_float(f"粗糙度##rough_{i}", roughness_value, 0.0, 1.0) + if changed: + self._update_material_roughness(material, new_roughness) + except: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "粗糙度: 不可用") + + if hasattr(material, 'metallic') and material.metallic is not None: + try: + metallic_value = float(material.metallic) + changed, new_metallic = imgui.slider_float(f"金属性##metal_{i}", metallic_value, 0.0, 1.0) + if changed: + self._update_material_metallic(material, new_metallic) + except: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "金属性: 不可用") + + if hasattr(material, 'refractive_index') and material.refractive_index is not None: + try: + ior_value = float(material.refractive_index) + changed, new_ior = imgui.slider_float(f"折射率##ior_{i}", ior_value, 1.0, 3.0) + if changed: + self._update_material_ior(material, new_ior) + except: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "折射率: 不可用") + + # 材质预设 + imgui.text("材质预设") + presets = ["默认", "金属", "塑料", "玻璃", "木材", "混凝土"] + current_preset = 0 # 默认选择 + + if imgui.begin_combo(f"预设##preset_{i}", presets[current_preset]): + for j, preset_name in enumerate(presets): + if imgui.selectable(preset_name, j == current_preset): + self._apply_material_preset(material, preset_name) + imgui.end_combo() + + # 纹理信息 + imgui.text("纹理贴图") + if imgui.button(f"选择漫反射贴图##diffuse_{i}"): + self._select_texture_for_material(node, material, "diffuse") + + imgui.same_line() + if imgui.button(f"选择法线贴图##normal_{i}"): + self._select_texture_for_material(node, material, "normal") + + imgui.same_line() + if imgui.button(f"选择粗糙度贴图##roughness_{i}"): + self._select_texture_for_material(node, material, "roughness") + + if imgui.button(f"选择金属性贴图##metallic_{i}"): + self._select_texture_for_material(node, material, "metallic") + + imgui.same_line() + if imgui.button(f"选择自发光贴图##emission_{i}"): + self._select_texture_for_material(node, material, "emission") + + imgui.same_line() + if imgui.button(f"清除所有贴图##clear_{i}"): + self._clear_all_textures(node) + + # 着色模型选择 + self._draw_shading_model_panel(material, i) + + # 显示当前纹理信息 + self._display_current_textures(node, material) + + + def _draw_shading_model_panel(self, material, material_index): + """绘制着色模型选择面板""" + try: + imgui.text("着色模型") + + # RenderPipeline支持的着色模型 + shading_models = ["默认", "自发光", "透明"] + current_model = 0 # 默认选择 + + # 安全地获取当前着色模型 + try: + if hasattr(material, 'emission') and material.emission is not None: + current_model = int(material.emission.x) + except: + current_model = 0 + + # 着色模型选择 + if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_model]): + for j, model_name in enumerate(shading_models): + if imgui.selectable(model_name, j == current_model): + self._update_shading_model(material, j) + imgui.end_combo() + + # 如果是透明着色模型,添加透明度控制 + if current_model == 3: # 透明着色模型 + imgui.text("透明度设置") + try: + if hasattr(material, 'shading_model_param0'): + current_opacity = material.shading_model_param0 + else: + current_opacity = 1.0 + + changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0) + if changed: + self._update_transparency(material, new_opacity) + except: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用") + + except Exception as e: + print(f"绘制着色模型面板失败: {e}") diff --git a/ui/panels/interaction_panels.py b/ui/panels/interaction_panels.py new file mode 100644 index 00000000..7e12dca8 --- /dev/null +++ b/ui/panels/interaction_panels.py @@ -0,0 +1,253 @@ +import os +from imgui_bundle import imgui, imgui_ctx + + +class InteractionPanels: + """Drag-and-drop and context menu interaction panels.""" + + def __init__(self, app): + self.app = app + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + + def _draw_drag_drop_interface(self): + """绘制拖拽界面""" + # 检查资源管理器的拖拽状态 + if self.resource_manager.is_dragging(): + self.is_dragging = True + self.dragged_files = self.resource_manager.get_dragged_files() + self.show_drag_overlay = True + + # 绘制拖拽覆盖层 + if self.show_drag_overlay: + self._draw_drag_overlay() + + # 检查是否有拖拽的文件需要处理 + if self.is_dragging and self.dragged_files: + # 显示拖拽状态 + self._draw_drag_status() + + # 检查是否释放鼠标(结束拖拽) + if imgui.is_mouse_released(0): + self._handle_drag_drop_completion() + + + def _handle_drag_drop_completion(self): + """处理拖拽完成""" + # 检查是否在3D视图中释放 + mouse_pos = imgui.get_mouse_pos() + viewport = imgui.get_main_viewport() + + # 简单检查:如果不在任何ImGui窗口上,则认为是在3D视图中 + if not imgui.is_any_window_hovered(): + # 导入支持的3D模型文件 + imported_count = 0 + for file_path in self.dragged_files: + if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']: + try: + self._import_model_for_runtime(str(file_path)) + self.add_success_message(f"成功导入模型: {file_path.name}") + imported_count += 1 + except Exception as e: + self.add_error_message(f"导入模型失败 {file_path.name}: {e}") + + if imported_count > 0: + self.add_success_message(f"共导入 {imported_count} 个模型") + + # 清除拖拽状态 + self.is_dragging = False + self.dragged_files.clear() + self.show_drag_overlay = False + self.resource_manager.clear_drag() + + + def _draw_drag_overlay(self): + """绘制拖拽覆盖层""" + viewport = imgui.get_main_viewport() + imgui.set_next_window_pos((0, 0)) + imgui.set_next_window_size(viewport.work_size) + + flags = ( + imgui.WindowFlags_.no_title_bar | + imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_move | + imgui.WindowFlags_.no_scrollbar | + imgui.WindowFlags_.no_saved_settings | + imgui.WindowFlags_.no_background | + imgui.WindowFlags_.no_focus_on_appearing + ) + + imgui.begin("##DragOverlay", True, flags) + + # 绘制半透明背景 + draw_list = imgui.get_window_draw_list() + draw_list.add_rect_filled( + (0, 0), viewport.work_size, + imgui.get_color_u32((0, 0, 0, 0.1)) + ) + + # 绘制提示文本 + text_size = imgui.calc_text_size("释放以导入文件") + text_pos = ( + (viewport.work_size.x - text_size.x) / 2, + (viewport.work_size.y - text_size.y) / 2 + ) + + draw_list.add_text( + text_pos, + imgui.get_color_u32((1, 1, 1, 1)), + "释放以导入文件" + ) + + imgui.end() + + + def _draw_drag_status(self): + """绘制拖拽状态""" + viewport = imgui.get_main_viewport() + + # 在右下角显示拖拽状态 + imgui.set_next_window_pos( + (viewport.work_size.x - 300, viewport.work_size.y - 150), + imgui.Cond_.first_use_ever + ) + + flags = ( + imgui.WindowFlags_.no_title_bar | + imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_move | + imgui.WindowFlags_.no_scrollbar | + imgui.WindowFlags_.no_saved_settings + ) + + with imgui_ctx.begin("拖拽状态", True, flags): + imgui.text("拖拽的文件:") + for file_path in self.dragged_files: + filename = os.path.basename(file_path) + imgui.text(f" • {filename}") + + imgui.separator() + + if imgui.button("导入所有文件"): + self.process_dragged_files() + + imgui.same_line() + if imgui.button("取消"): + self.clear_dragged_files() + + + def _draw_context_menus(self): + """绘制右键菜单""" + # 节点右键菜单 + if hasattr(self, '_context_menu_node') and self._context_menu_node: + imgui.open_popup("节点右键菜单") + self._context_menu_node = None + + if imgui.begin_popup("节点右键菜单"): + if imgui.menu_item("删除节点", "", False, True)[1]: + if hasattr(self, '_context_menu_target') and self._context_menu_target: + self._delete_node(self._context_menu_target) + imgui.close_current_popup() + + if imgui.menu_item("重命名", "", False, True)[1]: + self._renaming_node = True + imgui.close_current_popup() + + imgui.separator() + + if imgui.menu_item("复制", "", False, True)[1]: + if hasattr(self, '_context_menu_target') and self._context_menu_target: + self._copy_node(self._context_menu_target) + imgui.close_current_popup() + + if imgui.menu_item("聚焦", "", False, True)[1]: + if hasattr(self, '_context_menu_target') and self._context_menu_target: + if hasattr(self, 'selection') and self.selection: + self.selection.updateSelection(self._context_menu_target) + self.selection.focusCameraOnSelectedNodeAdvanced() + imgui.close_current_popup() + + imgui.end_popup() + + # 重命名对话框 + if hasattr(self, '_renaming_node') and self._renaming_node: + imgui.open_popup("重命名节点") + if not hasattr(self, '_rename_buffer'): + self._rename_buffer = "" + if hasattr(self, '_context_menu_target') and self._context_menu_target: + self._rename_buffer = self._context_menu_target.getName() or "" + + if imgui.begin_popup("重命名节点"): + changed, new_name = imgui.input_text("新名称", self._rename_buffer, 256) + if changed: + self._rename_buffer = new_name + + if imgui.button("确定"): + if hasattr(self, '_context_menu_target') and self._context_menu_target: + self._context_menu_target.setName(self._rename_buffer) + self._renaming_node = False + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("取消"): + self._renaming_node = False + imgui.close_current_popup() + + imgui.end_popup() + + + def _delete_node_simple(self, node): + """删除节点 - 简化版本""" + if not node or node.isEmpty(): + return + + # 从场景管理器中删除 + if hasattr(self, 'scene_manager') and self.scene_manager: + if hasattr(self.scene_manager, 'models') and node in self.scene_manager.models: + self.scene_manager.models.remove(node) + + # 从GUI管理器中删除 + if hasattr(self, 'gui_manager') and self.gui_manager: + gui_element = None + if hasattr(node, 'getPythonTag'): + gui_element = node.getPythonTag('gui_element') + if gui_element and hasattr(self.gui_manager, 'gui_elements'): + if gui_element in self.gui_manager.gui_elements: + self.gui_manager.gui_elements.remove(gui_element) + + # 使用主删除方法 + self._delete_node(node) + + # 获取节点名称(在删除之前) + node_name = node.getName() if not node.isEmpty() else '未命名节点' + + # 删除节点本身 + node.removeNode() + + # 清除选择 + if hasattr(self, 'selection') and self.selection: + if self.selection.selectedNode == node: + self.selection.clearSelection() + + # 添加成功消息 + self.add_success_message(f"已删除节点: {node_name}") + + + def _copy_node(self, node): + """复制节点""" + if not node or node.isEmpty(): + return + + # 这里可以实现节点复制逻辑 + # 暂时只显示消息 + self.add_info_message(f"复制功能暂未实现: {node.getName() or '未命名节点'}") + + # ==================== 创建功能实现 ==================== diff --git a/ui/panels/object_factory.py b/ui/panels/object_factory.py new file mode 100644 index 00000000..9fb711c7 --- /dev/null +++ b/ui/panels/object_factory.py @@ -0,0 +1,406 @@ +import os +import time + + +class ObjectFactory: + """Scene object and sample panel factory methods.""" + + def __init__(self, app): + self.app = app + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + + def createEmptyObject(self): + """创建空对象""" + try: + from panda3d.core import NodePath + # 创建一个空节点 + empty_node = NodePath("EmptyObject") + empty_node.reparentTo(self.render) + empty_node.setPos(0, 0, 0) + + # 添加到场景管理器 + if hasattr(self, 'scene_manager') and self.scene_manager: + self.scene_manager.models.append(empty_node) + + # 更新场景树 + if hasattr(self, 'updateSceneTree'): + self.updateSceneTree() + + print("✓ 空对象创建成功") + return empty_node + except Exception as e: + print(f"✗ 创建空对象失败: {e}") + return None + + + def create3DText(self, pos=(0, 0, 0), text="3D Text", scale=1.0): + """创建3D文本""" + try: + from panda3d.core import TextNode, NodePath + + # 创建文本节点 + text_node = TextNode("3DText") + text_node.setText(text) + text_node.setAlign(TextNode.ACenter) + text_node.setTextColor(1, 1, 1, 1) # 白色 + + # 设置中文字体 + chinese_font = self._get_chinese_font() + if chinese_font: + text_node.setFont(chinese_font) + + # 创建节点路径并设置位置 + text_np = NodePath(text_node) + text_np.reparentTo(self.render) + text_np.setPos(pos) + text_np.setScale(scale) + + # 添加到场景管理器 + if hasattr(self, 'scene_manager') and self.scene_manager: + self.scene_manager.models.append(text_np) + + # 更新场景树 + if hasattr(self, 'updateSceneTree'): + self.updateSceneTree() + + print(f"✓ 3D文本创建成功: {text}") + return text_np + except Exception as e: + print(f"✗ 创建3D文本失败: {e}") + return None + + + def create3DImage(self, pos=(0, 0, 0), image_path="", size=(1, 1)): + """创建3D图片""" + try: + from panda3d.core import CardMaker, NodePath + + if not image_path or not os.path.exists(image_path): + print("✗ 图片文件不存在") + return None + + # 创建卡片几何体 + card_maker = CardMaker("3DImage") + card_maker.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + card = card_maker.generate() + + # 创建节点路径 + image_np = NodePath(card) + image_np.reparentTo(self.render) + image_np.setPos(pos) + + # 加载纹理 + from panda3d.core import Texture, Filename + tex = self.loader.loadTexture(Filename.fromOsSpecific(image_path)) + if tex: + image_np.setTexture(tex, 1) + else: + print("✗ 加载纹理失败") + image_np.removeNode() + return None + + # 添加到场景管理器 + if hasattr(self, 'scene_manager') and self.scene_manager: + self.scene_manager.models.append(image_np) + + # 更新场景树 + if hasattr(self, 'updateSceneTree'): + self.updateSceneTree() + + print(f"✓ 3D图片创建成功: {os.path.basename(image_path)}") + return image_np + except Exception as e: + print(f"✗ 创建3D图片失败: {e}") + return None + + + def createCube(self, pos=(0, 0, 0), size=1.0): + """创建立方体""" + try: + # 尝试使用Panda3D的内置几何体 + from panda3d.core import NodePath, GeomNode, Geom, GeomVertexFormat, GeomVertexData, GeomVertexWriter, GeomTriangles, GeomPoints + from panda3d.core import Vec3, Vec4, RenderState, ShadeModelAttrib + + # 创建顶点数据格式 + format = GeomVertexFormat.getV3n3cpt2() + vdata = GeomVertexData('cube', format, Geom.UHStatic) + vdata.setNumRows(24) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + color = GeomVertexWriter(vdata, 'color') + + # 立方体的8个顶点 + s = size / 2 + vertices = [ + (-s, -s, -s), (s, -s, -s), (s, s, -s), (-s, s, -s), # 底面 + (-s, -s, s), (s, -s, s), (s, s, s), (-s, s, s) # 顶面 + ] + + # 立方体的6个面,每个面4个顶点 + faces = [ + (0, 1, 2, 3), # 底面 + (4, 7, 6, 5), # 顶面 + (0, 4, 5, 1), # 前面 + (2, 6, 7, 3), # 后面 + (0, 3, 7, 4), # 左面 + (1, 5, 6, 2) # 右面 + ] + + # 法线 + normals = [ + (0, 0, -1), (0, 0, 1), (0, -1, 0), (0, 1, 0), (-1, 0, 0), (1, 0, 0) + ] + + # 添加顶点数据 + for face_idx, face in enumerate(faces): + n = normals[face_idx] + for vertex_idx in face: + v = vertices[vertex_idx] + vertex.addData3f(*v) + normal.addData3f(*n) + color.addData4f(0.8, 0.8, 0.8, 1.0) # 灰色 + + # 创建几何体 + geom = Geom(vdata) + + # 添加三角形 + for face_idx in range(6): + base = face_idx * 4 + # 每个面分成2个三角形 + tri = GeomTriangles(Geom.UHStatic) + tri.addConsecutiveVertices(base, 3) + tri.closePrimitive() + + tri2 = GeomTriangles(Geom.UHStatic) + tri2.addVertices(base + 2, base + 3, base) + tri2.closePrimitive() + + geom.addPrimitive(tri) + geom.addPrimitive(tri2) + + # 创建节点 + geom_node = GeomNode('cube') + geom_node.addGeom(geom) + + cube = NodePath(geom_node) + cube.reparentTo(self.render) + cube.setPos(pos) + + # 设置渲染状态 + state = RenderState.make(ShadeModelAttrib.make(ShadeModelAttrib.MSmooth)) + cube.set_state(state) + + # 添加到场景管理器 + if hasattr(self, 'scene_manager') and self.scene_manager: + self.scene_manager.models.append(cube) + + # 更新场景树 + if hasattr(self, 'updateSceneTree'): + self.updateSceneTree() + + print("✓ 立方体创建成功") + return cube + except Exception as e: + print(f"✗ 创建立方体失败: {e}") + return None + + + def createSphere(self, pos=(0, 0, 0), radius=1.0): + """创建球体""" + try: + from panda3d.core import CardMaker, NodePath + + # 创建一个简单的球体(使用多个卡片近似) + sphere_root = NodePath("Sphere") + + # 创建球体的多个面来近似球形 + num_segments = 6 + for i in range(num_segments): + angle1 = (i / num_segments) * 360 + + # 创建球体片段 + segment_maker = CardMaker(f"SphereSegment{i}") + segment_maker.setFrame(-radius, radius, -radius, radius) + segment = NodePath(segment_maker.generate()) + segment.reparentTo(sphere_root) + + # 设置位置和旋转来形成球体 + segment.setPos(0, 0, 0) + segment.setH(angle1) + segment.setP(angle1/2) # 倾斜角度 + segment.setScale(radius) + + # 设置颜色 + from panda3d.core import Vec4 + sphere_root.setColor(Vec4(0.8, 0.6, 0.4, 1.0)) # 棕色 + + sphere_root.reparentTo(self.render) + sphere_root.setPos(pos) + + # 添加到场景管理器 + if hasattr(self, 'scene_manager') and self.scene_manager: + self.scene_manager.models.append(sphere_root) + + # 更新场景树 + if hasattr(self, 'updateSceneTree'): + self.updateSceneTree() + + print("✓ 球体创建成功") + return sphere_root + except Exception as e: + print(f"✗ 创建球体失败: {e}") + return None + + + def createCylinder(self, pos=(0, 0, 0), radius=1.0, height=2.0): + """创建圆柱体""" + try: + import math + from panda3d.core import CardMaker, NodePath + + # 创建圆柱体 + cylinder_root = NodePath("Cylinder") + + # 创建圆柱体的多个侧面 + num_segments = 12 + for i in range(num_segments): + angle1 = (i / num_segments) * 360 + angle2 = ((i + 1) / num_segments) * 360 + + # 计算圆柱体侧面的位置 + x1 = radius * math.cos(math.radians(angle1)) + y1 = radius * math.sin(math.radians(angle1)) + + # 创建圆柱体侧面 + segment_maker = CardMaker(f"CylinderSegment{i}") + segment = NodePath(segment_maker.generate()) + segment.reparentTo(cylinder_root) + + # 设置位置和旋转 + segment.setPos(x1, y1, 0) + segment.setH(angle1) + segment.setScale(0.5, height, 1) # 调整宽度和高度 + + # 设置颜色 + from panda3d.core import Vec4 + cylinder_root.setColor(Vec4(0.4, 0.8, 0.4, 1.0)) # 绿色 + + cylinder_root.reparentTo(self.render) + cylinder_root.setPos(pos) + + # 添加到场景管理器 + if hasattr(self, 'scene_manager') and self.scene_manager: + self.scene_manager.models.append(cylinder_root) + + # 更新场景树 + if hasattr(self, 'updateSceneTree'): + self.updateSceneTree() + + print("✓ 圆柱体创建成功") + return cylinder_root + except Exception as e: + print(f"✗ 创建圆柱体失败: {e}") + return None + + + def createPlane(self, pos=(0, 0, 0), size=(1, 1)): + """创建平面""" + try: + from panda3d.core import CardMaker, NodePath + + # 创建平面 + card_maker = CardMaker("Plane") + card_maker.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + plane = NodePath(card_maker.generate()) + + plane.reparentTo(self.render) + plane.setPos(pos) + + # 添加到场景管理器 + if hasattr(self, 'scene_manager') and self.scene_manager: + self.scene_manager.models.append(plane) + + # 更新场景树 + if hasattr(self, 'updateSceneTree'): + self.updateSceneTree() + + print("✓ 平面创建成功") + return plane + except Exception as e: + print(f"✗ 创建平面失败: {e}") + return None + + + def create2DSamplePanel(self): + """创建2D示例面板""" + try: + from core.InfoPanelManager import createSampleInfoPanel + result = createSampleInfoPanel(self.render) + return result + except Exception as e: + print(f"创建2D示例面板失败: {e}") + return None + + + def create3DSamplePanel(self): + """创建3D实例面板""" + try: + if hasattr(self, 'info_panel_manager') and self.info_panel_manager: + # 创建3D信息面板 + panel_id = f"3d_sample_{int(time.time())}" + result = self.info_panel_manager.create3DInfoPanel( + panel_id=panel_id, + position=(0, 0, 2), + size=(1.0, 0.6), + bg_color=(0.15, 0.25, 0.35, 0.95), + border_color=(0.3, 0.5, 0.7, 1.0), + title_color=(0.7, 0.9, 1.0, 1.0), + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 添加示例内容 + if result: + sample_data = { + "标题": "3D信息面板", + "状态": "运行中", + "创建时间": time.strftime("%Y-%m-%d %H:%M:%S"), + "位置": f"X:0, Y:0, Z:2" + } + self.info_panel_manager.updatePanelContent(panel_id, content=sample_data) + + return result + return None + except Exception as e: + print(f"创建3D示例面板失败: {e}") + return None + + + def createWebPanel(self, url="https://www.example.com"): + """创建Web面板""" + try: + if hasattr(self, 'info_panel_manager') and self.info_panel_manager: + panel_id = f"web_panel_{int(time.time())}" + result = self.info_panel_manager.createHTTPInfoPanel( + panel_id=panel_id, + url=url, + position=(0.8, 0.0), + size=(0.35, 0.4), + update_interval=5.0 # 每5秒更新一次 + ) + return result + return None + except Exception as e: + print(f"创建Web面板失败: {e}") + return None + + # ==================== GUI创建方法 ==================== diff --git a/ui/panels/property_helpers.py b/ui/panels/property_helpers.py new file mode 100644 index 00000000..3a59c40a --- /dev/null +++ b/ui/panels/property_helpers.py @@ -0,0 +1,1460 @@ +import os +from pathlib import Path + +from imgui_bundle import imgui, imgui_ctx + + +class PropertyHelpers: + """Property, collision, material, and picker helper methods.""" + + def __init__(self, app): + self.app = app + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + + def _apply_gui_font(self, gui_element, font_path): + """应用GUI元素的字体""" + try: + if hasattr(gui_element, 'setFont') and font_path: + gui_element.setFont(font_path) + gui_element.font_path = font_path + except Exception as e: + print(f"应用GUI字体失败: {e}") + + + def _apply_gui_font_size(self, gui_element, font_size): + """应用GUI元素的字体大小""" + try: + if hasattr(gui_element, 'setFontSize'): + gui_element.setFontSize(font_size) + gui_element.font_size = font_size + except Exception as e: + print(f"应用GUI字体大小失败: {e}") + + + def _apply_gui_font_style(self, gui_element): + """应用GUI元素的字体样式""" + try: + if hasattr(gui_element, 'setFontStyle'): + style = 0 + if getattr(gui_element, 'font_bold', False): + style |= 1 # 粗体 + if getattr(gui_element, 'font_italic', False): + style |= 2 # 斜体 + gui_element.setFontStyle(style) + except Exception as e: + print(f"应用GUI字体样式失败: {e}") + + # 特定类型的属性 + if gui_type == "button": + if imgui.collapsing_header("按钮属性"): + # 按钮状态 + is_pressed = getattr(gui_element, 'pressed', False) + changed, new_pressed = imgui.checkbox("按下状态", is_pressed) + if changed: + gui_element.pressed = new_pressed + + # 按钮回调 + callback_name = getattr(gui_element, 'callback_name', '') + changed, new_callback = imgui.input_text("回调函数", callback_name, 64) + if changed: + gui_element.callback_name = new_callback + + elif gui_type == "entry": + if imgui.collapsing_header("输入框属性"): + # 输入框内容 + entry_text = getattr(gui_element, 'entry_text', '') + changed, new_text = imgui.input_text("输入内容", entry_text, 256) + if changed: + gui_element.entry_text = new_text + if hasattr(gui_element, 'set'): + gui_element.set(new_text) + + # 最大长度 + max_length = getattr(gui_element, 'max_length', 256) + changed, new_max = imgui.input_int("最大长度", max_length) + if changed: + gui_element.max_length = max(max_length, 1) + + # 密码模式 + is_password = getattr(gui_element, 'is_password', False) + changed, new_password = imgui.checkbox("密码模式", is_password) + if changed: + gui_element.is_password = new_password + if hasattr(gui_element, 'obscure'): + gui_element.obscure(new_password) + + elif gui_type in ["2d_image", "3d_image"]: + if imgui.collapsing_header("图像属性"): + # 图像路径 + image_path = getattr(gui_element, 'image_path', '') + changed, new_path = imgui.input_text("图像路径", image_path, 256) + if changed and hasattr(self, 'gui_manager'): + gui_element.image_path = new_path + # TODO: 重新加载图像 + + # 图像缩放模式 + scale_mode = getattr(gui_element, 'scale_mode', 'stretch') + if imgui.begin_combo("缩放模式", scale_mode): + if imgui.selectable("拉伸##stretch"): + gui_element.scale_mode = 'stretch' + if imgui.selectable("适应##fit"): + gui_element.scale_mode = 'fit' + if imgui.selectable("填充##fill"): + gui_element.scale_mode = 'fill' + imgui.end_combo() + + + def _has_collision(self, node): + """检查节点是否有碰撞体""" + try: + if not node or node.isEmpty(): + return False + + # 检查是否有碰撞节点 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + return True + + return False + except Exception as e: + print(f"检查碰撞状态失败: {e}") + return False + + + def _get_current_collision_shape(self, node): + """获取当前碰撞形状""" + try: + if not self._has_collision(node): + return "球形 (Sphere)" + + # 查找碰撞节点并判断形状 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + # 根据碰撞节点名称判断形状 + if 'sphere' in name.lower(): + return "球形 (Sphere)" + elif 'box' in name.lower(): + return "盒型 (Box)" + elif 'capsule' in name.lower(): + return "胶囊体 (Capsule)" + elif 'plane' in name.lower(): + return "平面 (Plane)" + + return "球形 (Sphere)" # 默认 + except Exception as e: + print(f"获取碰撞形状失败: {e}") + return "球形 (Sphere)" + + + def _get_current_collision_shape_type(self, node): + """获取当前碰撞形状类型(内部标识)""" + try: + shape_name = self._get_current_collision_shape(node) + if "Sphere" in shape_name: + return "sphere" + elif "Box" in shape_name: + return "box" + elif "Capsule" in shape_name: + return "capsule" + elif "Plane" in shape_name: + return "plane" + else: + return "sphere" + except Exception as e: + print(f"获取碰撞形状类型失败: {e}") + return "sphere" + + + def _get_collision_position_offset(self, node): + """获取碰撞体位置偏移""" + try: + if not self._has_collision(node): + return (0.0, 0.0, 0.0) + + # 查找碰撞节点并获取位置 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + pos = child.getPos() + return (pos.x, pos.y, pos.z) + + return (0.0, 0.0, 0.0) + except Exception as e: + print(f"获取碰撞位置失败: {e}") + return (0.0, 0.0, 0.0) + + + def _is_collision_visible(self, node): + """检查碰撞体是否可见""" + try: + if not self._has_collision(node): + return False + + # 查找碰撞节点并检查可见性 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + return child.isHidden() == False + + return False + except Exception as e: + print(f"检查碰撞可见性失败: {e}") + return False + + + def _add_collision_to_node(self, node): + """为节点添加碰撞体""" + try: + if not node or node.isEmpty(): + print("无效的节点") + return + + if self._has_collision(node): + print("节点已有碰撞体") + return + + # 获取选择的形状 + shape_name = getattr(self, '_selected_collision_shape', '球形 (Sphere)') + + if hasattr(self, 'collision_manager'): + # 使用碰撞管理器添加碰撞体 + shape_type = self._get_shape_type_from_name(shape_name) + collision_node = self.collision_manager.setupAdvancedCollision( + node, + shape_type=shape_type, + mask_type='MODEL_COLLISION' + ) + + if collision_node: + print(f"成功为节点 {node.getName()} 添加 {shape_name} 碰撞体") + else: + print(f"添加碰撞体失败") + else: + print("碰撞管理器未初始化") + + except Exception as e: + print(f"添加碰撞体失败: {e}") + import traceback + traceback.print_exc() + + + def _remove_collision_from_node(self, node): + """从节点移除碰撞体""" + try: + if not node or node.isEmpty(): + print("无效的节点") + return + + if not self._has_collision(node): + print("节点没有碰撞体") + return + + # 查找并移除碰撞节点 + children_to_remove = [] + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + children_to_remove.append(child) + + # 移除找到的碰撞节点 + for child in children_to_remove: + child.removeNode() + + if children_to_remove: + print(f"成功移除节点 {node.getName()} 的碰撞体") + else: + print(f"未找到碰撞体") + + except Exception as e: + print(f"移除碰撞体失败: {e}") + import traceback + traceback.print_exc() + + + def _toggle_collision_visibility(self, node): + """切换碰撞体可见性""" + try: + if not node or node.isEmpty(): + return + + # 查找碰撞节点并切换可见性 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + if child.isHidden(): + child.show() + else: + child.hide() + break + + except Exception as e: + print(f"切换碰撞可见性失败: {e}") + + + def _update_collision_position(self, node, axis, value): + """更新碰撞体位置""" + try: + if not node or node.isEmpty(): + return + + # 查找碰撞节点并更新位置 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + current_pos = child.getPos() + if axis == 'x': + child.setPos(value, current_pos.y, current_pos.z) + elif axis == 'y': + child.setPos(current_pos.x, value, current_pos.z) + elif axis == 'z': + child.setPos(current_pos.x, current_pos.y, value) + break + + except Exception as e: + print(f"更新碰撞位置失败: {e}") + + + def _get_shape_type_from_name(self, shape_name): + """从形状名称获取形状类型""" + if "Sphere" in shape_name: + return "sphere" + elif "Box" in shape_name: + return "box" + elif "Capsule" in shape_name: + return "capsule" + elif "Plane" in shape_name: + return "plane" + else: + return "sphere" + + + def _get_sphere_radius(self, node): + """获取球形半径""" + try: + # 从碰撞节点获取半径信息 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: + solid = child.node().getSolid(0) + from panda3d.core import CollisionSphere + if isinstance(solid, CollisionSphere): + return solid.getRadius() + return 1.0 + except Exception as e: + print(f"获取球形半径失败: {e}") + return 1.0 + + + def _update_sphere_radius(self, node, radius): + """更新球形半径""" + try: + # 重新创建碰撞体来更新参数 + if hasattr(self, 'collision_manager'): + # 先移除旧的碰撞体 + self._remove_collision_from_node(node) + # 重新创建带有新参数的碰撞体 + self.collision_manager.setupAdvancedCollision( + node, + shape_type='sphere', + mask_type='MODEL_COLLISION', + radius=radius + ) + print(f"更新球形半径为: {radius}") + except Exception as e: + print(f"更新球形半径失败: {e}") + + + def _get_box_size(self, node): + """获取盒型尺寸""" + try: + # 从碰撞节点获取尺寸信息 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + # 尝试从碰撞体获取尺寸 + if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: + solid = child.node().getSolid(0) + from panda3d.core import CollisionBox + if isinstance(solid, CollisionBox): + min_p = solid.getMin() + max_p = solid.getMax() + return ( + max_p.x - min_p.x, + max_p.y - min_p.y, + max_p.z - min_p.z + ) + return (1.0, 1.0, 1.0) + except Exception as e: + print(f"获取盒型尺寸失败: {e}") + return (1.0, 1.0, 1.0) + + + def _update_box_size(self, node, axis, value): + """更新盒型尺寸""" + try: + # 获取当前尺寸 + current_size = self._get_box_size(node) + new_size = list(current_size) + + # 更新指定轴的尺寸 + if axis == 'x': + new_size[0] = value + elif axis == 'y': + new_size[1] = value + elif axis == 'z': + new_size[2] = value + + # 重新创建碰撞体 + if hasattr(self, 'collision_manager'): + self._remove_collision_from_node(node) + self.collision_manager.setupAdvancedCollision( + node, + shape_type='box', + mask_type='MODEL_COLLISION', + width=new_size[0], + length=new_size[1], + height=new_size[2] + ) + print(f"更新盒型尺寸: {new_size}") + except Exception as e: + print(f"更新盒型尺寸失败: {e}") + + + def _get_capsule_radius(self, node): + """获取胶囊体半径""" + try: + # 从碰撞节点获取半径信息 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: + solid = child.node().getSolid(0) + from panda3d.core import CollisionCapsule + if isinstance(solid, CollisionCapsule): + return solid.getRadius() + return 1.0 + except Exception as e: + print(f"获取胶囊体半径失败: {e}") + return 1.0 + + + def _update_capsule_radius(self, node, radius): + """更新胶囊体半径""" + try: + # 获取当前高度 + height = self._get_capsule_height(node) + + # 重新创建碰撞体 + if hasattr(self, 'collision_manager'): + self._remove_collision_from_node(node) + self.collision_manager.setupAdvancedCollision( + node, + shape_type='capsule', + mask_type='MODEL_COLLISION', + radius=radius, + height=height + ) + print(f"更新胶囊体半径为: {radius}") + except Exception as e: + print(f"更新胶囊体半径失败: {e}") + + + def _get_capsule_height(self, node): + """获取胶囊体高度""" + try: + # 从碰撞节点获取高度信息 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: + solid = child.node().getSolid(0) + from panda3d.core import CollisionCapsule + if isinstance(solid, CollisionCapsule): + point_a = solid.getPointA() + point_b = solid.getPointB() + return (point_b - point_a).length() + 2 * solid.getRadius() + return 2.0 + except Exception as e: + print(f"获取胶囊体高度失败: {e}") + return 2.0 + + + def _update_capsule_height(self, node, height): + """更新胶囊体高度""" + try: + # 获取当前半径 + radius = self._get_capsule_radius(node) + + # 重新创建碰撞体 + if hasattr(self, 'collision_manager'): + self._remove_collision_from_node(node) + self.collision_manager.setupAdvancedCollision( + node, + shape_type='capsule', + mask_type='MODEL_COLLISION', + radius=radius, + height=height + ) + print(f"更新胶囊体高度为: {height}") + except Exception as e: + print(f"更新胶囊体高度失败: {e}") + + + def _get_plane_normal(self, node): + """获取平面法向量""" + try: + # 从碰撞节点获取法向量信息 + for child in node.getChildren(): + if hasattr(child, 'getName') and child.getName(): + name = child.getName() + if 'collision' in name.lower() or 'Collision' in name: + if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0: + solid = child.node().getSolid(0) + from panda3d.core import CollisionPlane + if isinstance(solid, CollisionPlane): + plane = solid.getPlane() + normal = plane.getNormal() + return (normal.x, normal.y, normal.z) + return (0.0, 0.0, 1.0) + except Exception as e: + print(f"获取平面法向量失败: {e}") + return (0.0, 0.0, 1.0) + + + def _update_plane_normal(self, node, axis, value): + """更新平面法向量""" + try: + # 获取当前法向量 + current_normal = self._get_plane_normal(node) + new_normal = list(current_normal) + + # 更新指定轴的值 + if axis == 'x': + new_normal[0] = value + elif axis == 'y': + new_normal[1] = value + elif axis == 'z': + new_normal[2] = value + + # 标准化法向量 + from panda3d.core import Vec3 + normal_vec = Vec3(*new_normal) + normal_vec.normalize() + + # 重新创建碰撞体 + if hasattr(self, 'collision_manager'): + self._remove_collision_from_node(node) + self.collision_manager.setupAdvancedCollision( + node, + shape_type='plane', + mask_type='MODEL_COLLISION', + normal=normal_vec + ) + print(f"更新平面法向量为: ({normal_vec.x:.2f}, {normal_vec.y:.2f}, {normal_vec.z:.2f})") + except Exception as e: + print(f"更新平面法向量失败: {e}") + + + def _manual_collision_detection(self): + """手动执行碰撞检测""" + try: + if hasattr(self, 'collision_manager'): + results = self.collision_manager.detectModelCollisions(log_results=True) + if results: + print(f"手动碰撞检测完成,发现 {len(results)} 个碰撞") + else: + print("手动碰撞检测完成,未发现碰撞") + except Exception as e: + print(f"手动碰撞检测失败: {e}") + + def _update_node_name(self, node, new_name): + """更新节点名称""" + if new_name and new_name != node.getName(): + node.setName(new_name) + # 更新场景树显示 + if hasattr(self, 'scene_tree'): + self.scene_tree.refresh() + + + def _get_material_base_color(self, material): + """获取材质基础颜色""" + try: + if hasattr(material, 'base_color') and material.base_color is not None: + return (material.base_color.x, material.base_color.y, material.base_color.z, material.base_color.w) + elif hasattr(material, 'get_base_color'): + color = material.get_base_color() + return (color.x, color.y, color.z, color.w) + elif hasattr(material, 'getDiffuse'): + color = material.getDiffuse() + return (color.x, color.y, color.z, color.w if hasattr(color, 'w') else 1.0) + else: + return (1.0, 1.0, 1.0, 1.0) # 默认白色 + except: + return (1.0, 1.0, 1.0, 1.0) # 默认白色 + + + def _update_material_base_color(self, material, component, value): + """更新材质基础颜色""" + try: + base_color = self._get_material_base_color(material) + new_color = list(base_color) + + if component == 'r': + new_color[0] = value + elif component == 'g': + new_color[1] = value + elif component == 'b': + new_color[2] = value + elif component == 'a': + new_color[3] = value + + new_color_tuple = tuple(new_color) + + if hasattr(material, 'set_base_color'): + from panda3d.core import Vec4 + material.set_base_color(Vec4(*new_color_tuple)) + elif hasattr(material, 'setDiffuse'): + from panda3d.core import Vec4 + material.setDiffuse(Vec4(*new_color_tuple)) + except Exception as e: + print(f"更新材质基础颜色失败: {e}") + + + def _update_material_roughness(self, material, value): + """更新材质粗糙度""" + try: + if hasattr(material, 'set_roughness'): + material.set_roughness(value) + except Exception as e: + print(f"更新材质粗糙度失败: {e}") + + + def _update_material_metallic(self, material, value): + """更新材质金属性""" + try: + if hasattr(material, 'set_metallic'): + material.set_metallic(value) + except Exception as e: + print(f"更新材质金属性失败: {e}") + + + def _update_material_ior(self, material, value): + """更新材质折射率""" + try: + if hasattr(material, 'set_refractive_index'): + material.set_refractive_index(value) + except Exception as e: + print(f"更新材质折射率失败: {e}") + + + def _apply_material_preset(self, material, preset_name): + """应用材质预设""" + try: + from panda3d.core import Vec4, Material + + presets = { + "默认": { + "base_color": Vec4(0.8, 0.8, 0.8, 1.0), + "roughness": 0.5, + "metallic": 0.0, + "ior": 1.5 + }, + "金属": { + "base_color": Vec4(0.7, 0.7, 0.8, 1.0), + "roughness": 0.2, + "metallic": 1.0, + "ior": 2.5 + }, + "塑料": { + "base_color": Vec4(0.9, 0.9, 0.9, 1.0), + "roughness": 0.8, + "metallic": 0.0, + "ior": 1.45 + }, + "玻璃": { + "base_color": Vec4(0.9, 0.9, 1.0, 0.2), + "roughness": 0.0, + "metallic": 0.0, + "ior": 1.5 + }, + "木材": { + "base_color": Vec4(0.6, 0.4, 0.2, 1.0), + "roughness": 0.9, + "metallic": 0.0, + "ior": 1.55 + }, + "混凝土": { + "base_color": Vec4(0.5, 0.5, 0.5, 1.0), + "roughness": 1.0, + "metallic": 0.0, + "ior": 1.5 + } + } + + if preset_name in presets: + preset = presets[preset_name] + + # 应用基础颜色 + if hasattr(material, 'set_base_color'): + material.set_base_color(preset["base_color"]) + elif hasattr(material, 'setDiffuse'): + material.setDiffuse(preset["base_color"]) + + # 应用PBR属性 + if hasattr(material, 'set_roughness'): + material.set_roughness(preset["roughness"]) + if hasattr(material, 'set_metallic'): + material.set_metallic(preset["metallic"]) + if hasattr(material, 'set_refractive_index'): + material.set_refractive_index(preset["ior"]) + + print(f"已应用材质预设: {preset_name}") + except Exception as e: + print(f"应用材质预设失败: {e}") + + + def _apply_material_to_node(self, node): + """为节点应用材质""" + try: + from panda3d.core import Material, Vec4 + + # 检查是否已有材质 + materials = node.find_all_materials() + + if not materials: + # 创建新材质 + material = Material(f"default-material-{node.getName()}") + material.setBaseColor(Vec4(0.8, 0.8, 0.8, 1.0)) + material.setDiffuse(Vec4(0.8, 0.8, 0.8, 1.0)) + material.setAmbient(Vec4(0.4, 0.4, 0.4, 1.0)) + material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + node.setMaterial(material, 1) + print(f"已为新节点创建默认材质") + else: + print(f"节点已有 {len(materials)} 个材质") + except Exception as e: + print(f"应用材质失败: {e}") + + + def _reset_material(self, node): + """重置节点材质""" + try: + materials = node.find_all_materials() + + for material in materials: + # 重置为默认材质属性 + from panda3d.core import Vec4 + default_color = Vec4(0.8, 0.8, 0.8, 1.0) + + if hasattr(material, 'set_base_color'): + material.set_base_color(default_color) + elif hasattr(material, 'setDiffuse'): + material.setDiffuse(default_color) + + if hasattr(material, 'set_roughness'): + material.set_roughness(0.5) + if hasattr(material, 'set_metallic'): + material.set_metallic(0.0) + if hasattr(material, 'set_refractive_index'): + material.set_refractive_index(1.5) + + print(f"已重置材质") + except Exception as e: + print(f"重置材质失败: {e}") + + + def _select_texture_for_material(self, node, material, texture_type): + """为材质选择纹理""" + try: + # 设置当前纹理对话框状态 + self._current_texture_dialog = { + 'node': node, + 'material': material, + 'texture_type': texture_type + } + + # 初始化路径 + if not hasattr(self, '_texture_dialog_path'): + self._texture_dialog_path = '/home/hello/EG/Resources' + + # 设置文件类型过滤 + self._texture_dialog_filter = "*.png" + + except Exception as e: + print(f"选择纹理失败: {e}") + + + def _apply_texture_to_material(self, node, material, texture_type, texture_path): + """应用纹理到材质""" + try: + from panda3d.core import TextureStage + from direct.showbase import Loader + + # 加载纹理 + loader = Loader.Loader(self) + texture = loader.loadTexture(texture_path) + + if not texture: + print(f"无法加载纹理: {texture_path}") + return + + # 设置纹理属性 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 纹理槽位映射 + texture_slots = { + "diffuse": 0, # p3d_Texture0 + "ior": 2, # p3d_Texture2 + "normal": 1, # p3d_Texture1 + "roughness": 3, # p3d_Texture3 + "parallax": 4, # p3d_Texture4 + "metallic": 5, # p3d_Texture5 + "emission": 6, # p3d_Texture6 + "ao": 7, # p3d_Texture7 + "alpha": 8, # p3d_Texture8 + "detail": 9, # p3d_Texture9 + "gloss": 10 # p3d_Texture10 + } + + slot = texture_slots.get(texture_type, 0) + + # 创建纹理阶段 + texture_stage = TextureStage(f"{texture_type}_map") + texture_stage.setSort(slot) + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到节点 + node.setTexture(texture_stage, texture) + + print(f"已应用{texture_type}纹理: {texture_path}") + + except Exception as e: + print(f"应用纹理失败: {e}") + + + def _clear_all_textures(self, node): + """清除节点所有纹理""" + try: + # 清除所有纹理阶段 + node.clearTexture() + node.clearTexture() + print("已清除所有纹理") + except Exception as e: + print(f"清除纹理失败: {e}") + + + def _display_current_textures(self, node, material): + """显示当前纹理信息""" + try: + from panda3d.core import TextureStage + + # 获取所有纹理阶段 + texture_stages = node.findAllTextureStages() + + if not texture_stages: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "当前无纹理") + return + + imgui.text("当前纹理:") + for stage in texture_stages: + texture = node.getTexture(stage) + if texture: + texture_name = texture.getName() or "未命名" + stage_name = stage.getName() or "未命名" + imgui.text(f" {stage_name}: {texture_name}") + except Exception as e: + print(f"显示纹理信息失败: {e}") + + + def _update_shading_model(self, material, model_index): + """更新着色模型""" + try: + from panda3d.core import Vec4 + + # 根据不同的着色模型设置相应的参数 + if model_index == 1: # 自发光着色模型 + print("设置自发光着色模型...") + if hasattr(material, 'set_emission'): + current_emission = material.emission or Vec4(0, 0, 0, 0) + new_emission = Vec4(1.0, current_emission.y, current_emission.z, current_emission.w) + material.set_emission(new_emission) + print("自发光着色模型设置完成") + + elif model_index == 3: # 透明着色模型 + print("设置透明着色模型...") + if hasattr(material, 'set_emission'): + current_emission = material.emission or Vec4(0, 0, 0, 0) + new_emission = Vec4(3.0, 0, 0, 0) # 3表示透明着色模型 + material.set_emission(new_emission) + + # 设置默认透明度 + if hasattr(material, 'shading_model_param0'): + material.shading_model_param0 = 0.8 # 默认80%不透明度 + + print("透明着色模型设置完成") + + else: # 默认着色模型 + print("设置默认着色模型...") + if hasattr(material, 'set_emission'): + current_emission = material.emission or Vec4(0, 0, 0, 0) + new_emission = Vec4(0.0, current_emission.y, current_emission.z, current_emission.w) + material.set_emission(new_emission) + print("默认着色模型设置完成") + + print(f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})") + + except Exception as e: + print(f"更新着色模型失败: {e}") + + + def _update_transparency(self, material, opacity_value): + """更新透明度""" + try: + if hasattr(material, 'shading_model_param0'): + material.shading_model_param0 = opacity_value + print(f"透明度已更新: {opacity_value}") + except Exception as e: + print(f"更新透明度失败: {e}") + + + def _draw_texture_file_dialog(self): + """绘制纹理文件选择对话框""" + if not hasattr(self, '_current_texture_dialog') or not self._current_texture_dialog: + return + + try: + dialog_data = self._current_texture_dialog + node = dialog_data['node'] + material = dialog_data['material'] + texture_type = dialog_data['texture_type'] + + # 设置对话框标志 + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + # 获取屏幕尺寸,居中显示对话框 + display_size = imgui.get_io().display_size + dialog_width = 600 + dialog_height = 400 + imgui.set_next_window_size((dialog_width, dialog_height)) + imgui.set_next_window_pos( + ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) + ) + + # 显示文件选择对话框 + opened, window_open = imgui.begin(f"选择{texture_type}纹理文件##texture_dialog", True, flags) + if not window_open: + self._current_texture_dialog = None + imgui.end() + return + + imgui.text(f"选择{texture_type}纹理文件") + imgui.separator() + + # 当前路径显示 + current_path = getattr(self, '_texture_dialog_path', '/home/hello/EG/Resources') + imgui.text(f"当前路径: {current_path}") + + imgui.separator() + + # 文件类型过滤 + imgui.text("支持的纹理格式:") + file_types = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.tga", "*.dds"] + + current_filter = getattr(self, '_texture_dialog_filter', "*.png") + if imgui.begin_combo("文件类型##texture_filter", current_filter): + for i, file_type in enumerate(file_types): + if imgui.selectable(file_type, i == file_types.index(current_filter)): + self._texture_dialog_filter = file_type + imgui.end_combo() + + imgui.separator() + + # 路径导航按钮 + if imgui.button("上级目录##up_dir"): + current_path = os.path.dirname(current_path) + self._texture_dialog_path = current_path + + imgui.same_line() + if imgui.button("主目录##home_dir"): + self._texture_dialog_path = '/home/hello/EG/Resources' + + imgui.same_line() + if imgui.button("当前目录##current_dir"): + self._texture_dialog_path = '/home/hello/EG/Resources' + + imgui.same_line() + if imgui.button("纹理目录##textures_dir"): + self._texture_dialog_path = '/home/hello/EG/Resources/textures' + + imgui.separator() + + # 文件列表 + if imgui.begin_child("file_list##texture_files", (580, 200)): + try: + # 列出目录和文件 + items = [] + if os.path.exists(current_path): + for item in os.listdir(current_path): + item_path = os.path.join(current_path, item) + if os.path.isdir(item_path): + items.append(('dir', item, item_path)) + elif any(item.lower().endswith(ext[1:]) for ext in file_types): + items.append(('file', item, item_path)) + + # 排序:目录在前,文件在后 + items.sort(key=lambda x: (x[0], x[1].lower())) + + for item_type, item_name, item_path in items: + if item_type == 'dir': + if imgui.selectable(f"📁 {item_name}##dir_{item_name}", False)[0]: + self._texture_dialog_path = item_path + else: + selected, _ = imgui.selectable(f"📄 {item_name}##file_{item_name}", False) + if selected: + # 应用选择的纹理 + self._apply_texture_to_material(node, material, texture_type, item_path) + # 关闭对话框 + self._current_texture_dialog = None + break + + except Exception as e: + imgui.text_colored((1.0, 0.5, 0.5, 1.0), f"读取目录失败: {e}") + + imgui.end_child() + + imgui.separator() + + # 路径输入框 + changed, new_path = imgui.input_text("文件路径##texture_path", current_path, 512) + if changed: + self._texture_dialog_path = new_path + + imgui.same_line() + if imgui.button("确认路径##confirm_path"): + if os.path.isfile(new_path): + # 检查文件扩展名 + file_ext = os.path.splitext(new_path)[1].lower() + if file_ext in [ext[1:] for ext in file_types]: + self._apply_texture_to_material(node, material, texture_type, new_path) + self._current_texture_dialog = None + else: + print(f"不支持的文件格式: {file_ext}") + else: + print("请选择有效的文件") + + imgui.separator() + + # 按钮 + if imgui.button("取消##cancel_texture"): + self._current_texture_dialog = None + + imgui.end() + + except Exception as e: + print(f"绘制纹理对话框失败: {e}") + # 确保在异常情况下也调用 imgui.end() + try: + imgui.end() + except: + pass + + def start_transform_monitoring(self, node): + """开始变换监控""" + if node and not node.isEmpty(): + self._monitored_node = node + self._transform_monitoring = True + self._transform_update_timer = 0 + + # 记录初始变换值 + self._update_last_transform_values() + + + def stop_transform_monitoring(self): + """停止变换监控""" + self._transform_monitoring = False + self._monitored_node = None + self._last_transform_values = {} + + + def _update_last_transform_values(self): + """更新最后记录的变换值""" + if self._monitored_node and not self._monitored_node.isEmpty(): + try: + pos = self._monitored_node.getPos() + hpr = self._monitored_node.getHpr() + scale = self._monitored_node.getScale() + + self._last_transform_values = { + 'pos': (pos.x, pos.y, pos.z), + 'hpr': (hpr.x, hpr.y, hpr.z), + 'scale': (scale.x, scale.y, scale.z) + } + except Exception as e: + print(f"更新变换值失败: {e}") + + + def _check_transform_changes(self): + """检查变换变化""" + if not self._transform_monitoring or not self._monitored_node: + return + + try: + pos = self._monitored_node.getPos() + hpr = self._monitored_node.getHpr() + scale = self._monitored_node.getScale() + + current_values = { + 'pos': (pos.x, pos.y, pos.z), + 'hpr': (hpr.x, hpr.y, hpr.z), + 'scale': (scale.x, scale.y, scale.z) + } + + # 检查是否有变化 + if current_values != self._last_transform_values: + # 更新记录的值 + self._last_transform_values = current_values + # 触发属性面板更新(通过设置更新标志) + self.property_panel_update_timer = 0 + + except Exception as e: + print(f"检查变换变化失败: {e}") + + + def update_transform_monitoring(self, dt): + """更新变换监控(在主循环中调用)""" + if not self._transform_monitoring: + return + + self._transform_update_timer += dt + if self._transform_update_timer >= self._transform_update_interval: + self._transform_update_timer = 0 + self._check_transform_changes() + + + def show_color_picker(self, target_object, property_name, initial_color, callback=None): + """显示颜色选择器""" + self._color_picker_active = True + self._color_picker_target = (target_object, property_name) + self._color_picker_current_color = initial_color + self._color_picker_callback = callback + + + def _draw_color_picker(self): + """绘制颜色选择器对话框""" + if not self._color_picker_active: + return + + # 设置对话框标志 + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + # 获取屏幕尺寸,居中显示对话框 + display_size = imgui.get_io().display_size + dialog_width = 300 + dialog_height = 400 + imgui.set_next_window_size((dialog_width, dialog_height)) + imgui.set_next_window_pos( + ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) + ) + + with imgui_ctx.begin("颜色选择器", True, flags) as window: + if not window.opened: + self._color_picker_active = False + self._color_picker_target = None + return + + imgui.text("选择颜色") + imgui.separator() + + # 颜色编辑器 + changed, new_color = imgui.color_edit4( + "颜色##color_picker", + self._color_picker_current_color + ) + + if changed: + self._color_picker_current_color = new_color + + # 预设颜色 + imgui.text("预设颜色") + preset_colors = [ + (1.0, 0.0, 0.0, 1.0), # 红色 + (0.0, 1.0, 0.0, 1.0), # 绿色 + (0.0, 0.0, 1.0, 1.0), # 蓝色 + (1.0, 1.0, 0.0, 1.0), # 黄色 + (1.0, 0.0, 1.0, 1.0), # 洋红 + (0.0, 1.0, 1.0, 1.0), # 青色 + (1.0, 1.0, 1.0, 1.0), # 白色 + (0.0, 0.0, 0.0, 1.0), # 黑色 + (0.5, 0.5, 0.5, 1.0), # 灰色 + (0.188, 0.404, 0.753, 1.0), # 主题蓝色 + (0.176, 1.0, 0.769, 1.0), # 主题绿色 + (0.953, 0.616, 0.471, 1.0), # 主题橙色 + ] + + # 绘制预设颜色按钮 + colors_per_row = 6 + for i, color in enumerate(preset_colors): + if i % colors_per_row != 0: + imgui.same_line() + + imgui.color_button(f"preset_{i}", color, 0, (30, 30)) + if imgui.is_item_clicked(): + self._color_picker_current_color = color + + imgui.separator() + + # 按钮区域 + if imgui.button("确定"): + self._apply_color_selection() + self._color_picker_active = False + self._color_picker_target = None + + imgui.same_line() + if imgui.button("取消"): + self._color_picker_active = False + self._color_picker_target = None + + + def _apply_color_selection(self): + """应用颜色选择""" + if not self._color_picker_target: + return + + target_object, property_name = self._color_picker_target + + try: + # 应用颜色到目标对象 + if hasattr(target_object, 'setColor'): + target_object.setColor(self._color_picker_current_color) + elif hasattr(target_object, property_name): + setattr(target_object, property_name, self._color_picker_current_color) + + # 调用回调函数 + if self._color_picker_callback: + self._color_picker_callback(self._color_picker_current_color) + + except Exception as e: + print(f"应用颜色失败: {e}") + + + def _draw_color_button(self, label, color, size=(50, 20)): + """绘制颜色按钮并支持点击打开颜色选择器""" + imgui.color_button(label, color, 0, size) + if imgui.is_item_clicked(): + # 打开颜色选择器 + self.show_color_picker(None, None, color) + + + def _refresh_available_fonts(self): + """刷新可用字体列表""" + try: + import platform + from pathlib import Path + + system = platform.system().lower() + font_paths = [] + + if system == "linux": + font_dirs = [ + "/usr/share/fonts/truetype/", + "/usr/share/fonts/opentype/", + "/usr/local/share/fonts/", + "~/.fonts/" + ] + elif system == "windows": + font_dirs = [ + "C:/Windows/Fonts/", + ] + elif system == "darwin": + font_dirs = [ + "/System/Library/Fonts/", + "/Library/Fonts/", + "~/Library/Fonts/" + ] + else: + font_dirs = [] + + # 扫描字体目录 + common_fonts = [] + for font_dir in font_dirs: + font_path = Path(font_dir).expanduser() + if font_path.exists(): + for font_file in font_path.rglob("*.ttf"): + common_fonts.append(str(font_file)) + for font_file in font_path.rglob("*.otf"): + common_fonts.append(str(font_file)) + for font_file in font_path.rglob("*.ttc"): + common_fonts.append(str(font_file)) + + # 过滤常见字体 + font_keywords = [ + "arial", "helvetica", "times", "courier", "verdana", "georgia", + "comic", "impact", "trebuchet", "palatino", "garamond", + "noto", "dejavu", "liberation", "ubuntu", "roboto", "open", + "droid", "source", "wenquanyi", "wqy", "pingfang", "stheiti", + "microsoft", "msyh", "simsun", "simhei", "kaiti", "fangsong" + ] + + self._available_fonts = [] + for font_path in common_fonts: + font_name = Path(font_path).name.lower() + if any(keyword in font_name for keyword in font_keywords): + self._available_fonts.append(font_path) + + # 添加一些默认字体路径 + default_fonts = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf", + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/msyh.ttc", + "/System/Library/Fonts/PingFang.ttc" + ] + + for font_path in default_fonts: + if Path(font_path).exists() and font_path not in self._available_fonts: + self._available_fonts.append(font_path) + + print(f"✓ 找到 {len(self._available_fonts)} 个可用字体") + + except Exception as e: + print(f"⚠ 字体扫描失败: {e}") + self._available_fonts = [] + + + def show_font_selector(self, target_object, property_name, current_font, callback=None): + """显示字体选择器""" + self._font_selector_active = True + self._font_selector_target = (target_object, property_name) + self._font_selector_current_font = current_font or "" + self._font_selector_callback = callback + + + def _draw_font_selector(self): + """绘制字体选择器对话框""" + if not self._font_selector_active: + return + + # 设置对话框标志 + flags = (imgui.WindowFlags_.no_resize | + imgui.WindowFlags_.no_collapse | + imgui.WindowFlags_.modal) + + # 获取屏幕尺寸,居中显示对话框 + display_size = imgui.get_io().display_size + dialog_width = 400 + dialog_height = 500 + imgui.set_next_window_size((dialog_width, dialog_height)) + imgui.set_next_window_pos( + ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2) + ) + + with imgui_ctx.begin("字体选择器", True, flags) as window: + if not window.opened: + self._font_selector_active = False + self._font_selector_target = None + return + + imgui.text("选择字体") + imgui.separator() + + # 当前字体显示 + imgui.text(f"当前字体: {self._font_selector_current_font or '默认'}") + + # 字体搜索框 + changed, search_text = imgui.input_text("搜索", "", 256) + imgui.separator() + + # 字体列表 + if imgui.begin_child("font_list", (380, 300)): + for font_path in self._available_fonts: + font_name = Path(font_path).name + + # 搜索过滤 + if search_text and search_text.lower() not in font_name.lower(): + continue + + # 字体项 + if imgui.selectable(font_name, font_path == self._font_selector_current_font): + self._font_selector_current_font = font_path + + # 显示字体路径作为工具提示 + if imgui.is_item_hovered(): + imgui.set_tooltip(font_path) + + imgui.end_child() + + imgui.separator() + + # 按钮区域 + if imgui.button("确定"): + self._apply_font_selection() + self._font_selector_active = False + self._font_selector_target = None + + imgui.same_line() + if imgui.button("取消"): + self._font_selector_active = False + self._font_selector_target = None + + imgui.same_line() + if imgui.button("刷新字体"): + self._refresh_available_fonts() + + + def _apply_font_selection(self): + """应用字体选择""" + if not self._font_selector_target: + return + + target_object, property_name = self._font_selector_target + + try: + # 应用字体到目标对象 + if hasattr(target_object, property_name): + setattr(target_object, property_name, self._font_selector_current_font) + + # 调用回调函数 + if self._font_selector_callback: + self._font_selector_callback(self._font_selector_current_font) + + except Exception as e: + print(f"应用字体失败: {e}") + + + def _draw_font_selector_button(self, label, current_font): + """绘制字体选择器按钮""" + font_name = Path(current_font).name if current_font else "默认字体" + display_text = f"{font_name[:20]}..." if len(font_name) > 20 else font_name + + if imgui.button(f"{label}: {display_text}##font_selector"): + self.show_font_selector(None, None, current_font) diff --git a/ui/panels/script_panels.py b/ui/panels/script_panels.py new file mode 100644 index 00000000..13f446f3 --- /dev/null +++ b/ui/panels/script_panels.py @@ -0,0 +1,299 @@ +from imgui_bundle import imgui, imgui_ctx + + +class ScriptPanels: + """Script and console related editor panels.""" + + def __init__(self, app): + self.app = app + + def __getattr__(self, name): + return getattr(self.app, name) + + def __setattr__(self, name, value): + if name == "app" or name in self.__dict__ or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + setattr(self.app, name, value) + + + def _draw_console(self): + """绘制控制台面板""" + # 使用面板类型的窗口标志,支持docking + flags = self.style_manager.get_window_flags("panel") + + with self.style_manager.begin_styled_window("控制台", self.app.showConsole, flags): + self.app.showConsole = True # 确保窗口保持打开 + + imgui.text("控制台输出") + imgui.separator() + + # 显示消息系统中的消息 + if hasattr(self, 'messages') and self.messages: + for message in self.messages: + # 显示时间戳 + imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"[{message['timestamp']}]") + imgui.same_line() + + # 根据消息类型显示图标 + if message['text'].startswith('✓'): + if self.icons.get('success'): + imgui.image(self.icons['success'], (12, 12)) + imgui.same_line() + elif message['text'].startswith('✗'): + if self.icons.get('delete_fail_icon'): + imgui.image(self.icons['delete_fail_icon'], (12, 12)) + imgui.same_line() + elif message['text'].startswith('⚠'): + if self.icons.get('warning'): + imgui.image(self.icons['warning'], (12, 12)) + imgui.same_line() + + # 显示消息文本 + imgui.text_colored(message['color'], message['text']) + else: + # 默认消息 + imgui.text_colored((0.157, 0.620, 1.0, 1.0), "[系统]") + imgui.same_line() + imgui.text("引擎已就绪") + + # 输入框 + imgui.separator() + changed, command = imgui.input_text(">", "", 256) + if changed and command: + self.add_info_message(f"执行命令: {command}") + # TODO: 实现命令执行逻辑 + + imgui.separator() + + # 视角控制信息 + imgui.text("视角控制:") + imgui.text(" WASD - 移动") + imgui.text(" Q/E - 上下") + imgui.text(" 右键拖拽 - 旋转视角") + imgui.text(" 滚轮 - 前进/后退") + + # 相机位置信息 + cam_pos = self.camera.getPos() + cam_hpr = self.camera.getHpr() + imgui.text(f"位置: X={cam_pos.x:.1f}, Y={cam_pos.y:.1f}, Z={cam_pos.z:.1f}") + imgui.text(f"旋转: H={cam_hpr.x:.1f}, P={cam_hpr.y:.1f}, R={cam_hpr.z:.1f}") + + # 控制状态 + imgui.checkbox("启用视角控制", self.camera_control_enabled) + + # 重置按钮 + if imgui.button("重置相机"): + self.camera.setPos(0, -20, 5) + self.camera.setHpr(0, 0, 0) + self.add_info_message("相机位置已重置") + + + def _draw_script_panel(self): + """绘制脚本管理面板(与Qt版本功能一致)""" + # 使用面板类型的窗口标志,支持docking + flags = self.style_manager.get_window_flags("panel") + + with self.style_manager.begin_styled_window("脚本管理", self.app.showScriptPanel, flags): + self.app.showScriptPanel = True # 确保窗口保持打开 + + # 1. 脚本系统状态组 + self._draw_script_status_group() + + imgui.spacing() + + # 2. 创建脚本组 + self._draw_create_script_group() + + imgui.spacing() + + # 3. 可用脚本组 + self._draw_available_scripts_group() + + imgui.spacing() + + # 4. 脚本挂载组 + self._draw_script_mounting_group() + + + def _draw_script_status_group(self): + """绘制脚本系统状态组""" + if imgui.collapsing_header("脚本系统状态", imgui.TreeNodeFlags_.default_open): + # 脚本系统状态 + imgui.text("脚本引擎状态:") + imgui.same_line() + + # 检查脚本管理器是否正常工作 + if hasattr(self, 'script_manager') and self.script_manager: + if hasattr(self.script_manager, 'engine') and self.script_manager.engine: + imgui.text_colored((0.0, 1.0, 0.0, 1.0), "✓ 已启动") + else: + imgui.text_colored((1.0, 0.5, 0.0, 1.0), "⚠ 引擎未初始化") + else: + imgui.text_colored((1.0, 0.0, 0.0, 1.0), "✗ 未启动") + + # 热重载状态 + imgui.text("热重载状态:") + imgui.same_line() + + hot_reload_enabled = False + if hasattr(self, 'script_manager') and self.script_manager: + hot_reload_enabled = getattr(self.script_manager, 'hot_reload_enabled', False) + + if hot_reload_enabled: + imgui.text_colored((0.0, 1.0, 0.0, 1.0), "✓ 已启用") + else: + imgui.text_colored((1.0, 0.5, 0.0, 1.0), "✗ 已禁用") + + imgui.same_line() + if imgui.button("切换热重载##toggle_hot_reload"): + self._toggle_hot_reload() + + + def _draw_create_script_group(self): + """绘制创建脚本组""" + if imgui.collapsing_header("创建脚本"): + # 脚本名称输入 + imgui.text("脚本名称:") + imgui.same_line() + + # 获取当前脚本名称 + if not hasattr(self, '_new_script_name'): + self._new_script_name = "new_script" + + changed, new_name = imgui.input_text("##script_name", self._new_script_name, 256) + if changed: + self._new_script_name = new_name + + # 模板选择 + imgui.text("脚本模板:") + imgui.same_line() + + if not hasattr(self, '_selected_template'): + self._selected_template = 0 + + templates = ["基础脚本", "移动脚本", "旋转脚本", "缩放脚本", "动画脚本"] + changed, selected = imgui.combo("##script_template", self._selected_template, templates) + if changed: + self._selected_template = selected + + # 创建按钮 + if imgui.button("创建脚本##create_script"): + self._create_new_script() + + imgui.same_line() + if imgui.button("从文件创建##create_from_file"): + self._on_create_script() + + + def _draw_available_scripts_group(self): + """绘制可用脚本组""" + if imgui.collapsing_header("可用脚本"): + # 刷新脚本列表 + if imgui.button("刷新列表##refresh_scripts"): + self._refresh_scripts_list() + + imgui.same_line() + if imgui.button("重载全部##reload_all_scripts"): + self._reload_all_scripts() + + imgui.separator() + + # 获取可用脚本列表 + available_scripts = [] + if hasattr(self, 'script_manager') and self.script_manager: + try: + available_scripts = self.script_manager.get_available_scripts() + except Exception as e: + print(f"获取脚本列表失败: {e}") + + # 显示脚本列表 + if available_scripts: + for i, script_name in enumerate(available_scripts): + selected, _ = imgui.selectable(f"{script_name}##script_{i}", False) + if selected: + self._on_script_selected(script_name) + + # 双击编辑 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + self._edit_script(script_name) + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无可用脚本") + + + def _draw_script_mounting_group(self): + """绘制脚本挂载组""" + if imgui.collapsing_header("脚本挂载"): + # 显示当前选中对象 + selected_node = None + if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): + selected_node = self.selection.selectedNode + + if selected_node and not selected_node.isEmpty(): + imgui.text("选中对象:") + imgui.same_line() + imgui.text_colored((0.0, 1.0, 0.0, 1.0), selected_node.getName() or "未命名对象") + + imgui.spacing() + + # 脚本选择和挂载 + imgui.text("选择脚本:") + imgui.same_line() + + # 获取可用脚本 + available_scripts = [] + if hasattr(self, 'script_manager') and self.script_manager: + try: + available_scripts = self.script_manager.get_available_scripts() + except Exception as e: + print(f"获取脚本列表失败: {e}") + + if available_scripts: + if not hasattr(self, '_mount_script_index'): + self._mount_script_index = 0 + + changed, selected = imgui.combo("##mount_script", self._mount_script_index, available_scripts) + if changed: + self._mount_script_index = selected + + imgui.same_line() + if imgui.button("挂载##mount_script"): + if self._mount_script_index < len(available_scripts): + script_name = available_scripts[self._mount_script_index] + self._mount_script_to_selected(script_name) + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无可用脚本") + + imgui.spacing() + + # 显示已挂载的脚本 + imgui.text("已挂载脚本:") + + mounted_scripts = [] + if hasattr(self, 'script_manager') and self.script_manager: + try: + mounted_scripts = self.script_manager.get_scripts_on_object(selected_node) + except Exception as e: + print(f"获取已挂载脚本失败: {e}") + + if mounted_scripts: + for i, script_component in enumerate(mounted_scripts): + # 从ScriptComponent获取脚本名称 + script_name = getattr(script_component, 'script_name', None) + if not script_name and hasattr(script_component, '__class__'): + script_name = script_component.__class__.__name__ + + if not script_name: + script_name = f"Script_{i}" + + imgui.text(f"• {script_name}") + imgui.same_line() + if imgui.button(f"卸载##unmount_{i}"): + self._unmount_script_from_selected(script_name) + imgui.same_line() + if imgui.button(f"编辑##edit_mounted_{i}"): + self._edit_script(script_name) + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无挂载脚本") + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请先选择一个对象")