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), "请先选择一个对象")