767 lines
34 KiB
Python
767 lines
34 KiB
Python
from imgui_bundle import imgui, imgui_ctx
|
||
|
||
from ui.panels.editor_panels_right_collision import EditorPanelsRightCollisionMixin
|
||
from ui.panels.editor_panels_right_material import EditorPanelsRightMaterialMixin
|
||
from ui.panels.editor_panels_right_transform import EditorPanelsRightTransformMixin
|
||
|
||
|
||
class EditorPanelsRightMixin(
|
||
EditorPanelsRightTransformMixin,
|
||
EditorPanelsRightMaterialMixin,
|
||
EditorPanelsRightCollisionMixin,
|
||
):
|
||
"""Right panel aggregator mixin."""
|
||
|
||
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'):
|
||
lui_selected_index = getattr(self.app.lui_manager, "selected_index", -1)
|
||
if lui_selected_index >= 0 and self.app.lui_manager.luiFunction:
|
||
self.app.lui_manager.luiFunction._draw_component_properties(
|
||
self.app.lui_manager,
|
||
lui_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
|
||
|
||
# SSBO mode may select a proxy node for gizmo operations.
|
||
# Resolve proxy back to a real scene node so property panel stays meaningful.
|
||
try:
|
||
if (selected_node and not selected_node.isEmpty() and
|
||
selected_node.hasTag("is_ssbo_proxy")):
|
||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||
if ssbo_editor and controller:
|
||
resolved = None
|
||
if getattr(ssbo_editor, "selected_ids", None):
|
||
first_gid = ssbo_editor.selected_ids[0]
|
||
key = controller.id_to_name.get(first_gid)
|
||
if key:
|
||
resolved = controller.key_to_node.get(key)
|
||
if (resolved is None or resolved.isEmpty()) and getattr(ssbo_editor, "selected_name", None):
|
||
resolved = controller.key_to_node.get(ssbo_editor.selected_name)
|
||
if resolved and not resolved.isEmpty():
|
||
selected_node = resolved
|
||
except Exception:
|
||
pass
|
||
|
||
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 == "模型": # 只对模型类型进行动画检测
|
||
model_path = node.getTag("model_path") if node.hasTag("model_path") else ""
|
||
likely_anim_format = bool(model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx', '.bam', '.egg')))
|
||
|
||
# 优先使用场景标签(导入/加载时会写入)
|
||
if node.hasTag("has_animations"):
|
||
has_animation = node.getTag("has_animations").lower() == "true"
|
||
|
||
# 再做轻量结构检测(不依赖 Actor)
|
||
if not has_animation:
|
||
try:
|
||
has_character = node.findAllMatches("**/+Character").getNumPaths() > 0
|
||
has_bundle = node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||
has_animation = has_character or has_bundle
|
||
if has_animation:
|
||
node.setTag("has_animations", "true")
|
||
node.setTag("can_create_actor_from_memory", "true")
|
||
except Exception:
|
||
pass
|
||
|
||
# 最后才尝试 Actor 检测(只缓存“有动画”,避免把失败结果永久缓存)
|
||
cached_result = node.getPythonTag('animation')
|
||
if cached_result is True:
|
||
has_animation = True
|
||
elif not has_animation and likely_anim_format:
|
||
try:
|
||
actor = self._getActor(node)
|
||
if actor and actor.getAnimNames():
|
||
has_animation = True
|
||
node.setTag("has_animations", "true")
|
||
node.setPythonTag('animation', True)
|
||
print(f"[动画检测] {node.getName()}: 有动画")
|
||
except Exception as e:
|
||
print(f"动画检测失败: {e}")
|
||
elif cached_result is False and not likely_anim_format:
|
||
has_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_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):
|
||
"""绘制动画控制属性面板(优化版本,使用缓存避免重复计算)"""
|
||
anim_node = node
|
||
try:
|
||
if hasattr(self, "_resolve_animation_owner_model"):
|
||
resolved = self._resolve_animation_owner_model(node)
|
||
if resolved and not resolved.isEmpty():
|
||
anim_node = resolved
|
||
except Exception:
|
||
pass
|
||
|
||
# 路径兜底:当 anim_node 缺少路径时,从 scene_manager.models 中反查祖先模型
|
||
try:
|
||
needs_path = (not anim_node.hasTag("model_path")) or (not anim_node.getTag("model_path"))
|
||
if needs_path and hasattr(self, "scene_manager") and self.scene_manager:
|
||
models = getattr(self.scene_manager, "models", [])
|
||
for model in list(models):
|
||
try:
|
||
if not model or model.isEmpty():
|
||
continue
|
||
if (model == anim_node or model.isAncestorOf(anim_node) or anim_node.isAncestorOf(model)):
|
||
if model.hasTag("model_path") and model.getTag("model_path"):
|
||
anim_node.setTag("model_path", model.getTag("model_path"))
|
||
if model.hasTag("original_path") and model.getTag("original_path"):
|
||
anim_node.setTag("original_path", model.getTag("original_path"))
|
||
break
|
||
if model.hasTag("saved_model_path") and model.getTag("saved_model_path"):
|
||
anim_node.setTag("model_path", model.getTag("saved_model_path"))
|
||
break
|
||
except Exception:
|
||
continue
|
||
except Exception:
|
||
pass
|
||
|
||
# 先刷新一次模型动画标签,避免“导入后未初始化”导致误判
|
||
try:
|
||
if hasattr(self, "scene_manager") and self.scene_manager and hasattr(self.scene_manager, "_processModelAnimations"):
|
||
self.scene_manager._processModelAnimations(anim_node)
|
||
except Exception:
|
||
pass
|
||
|
||
has_animation_tag = anim_node.hasTag("has_animations") and anim_node.getTag("has_animations").lower() == "true"
|
||
has_animation_nodes = False
|
||
try:
|
||
has_animation_nodes = (
|
||
anim_node.findAllMatches("**/+Character").getNumPaths() > 0 or
|
||
anim_node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
# 检查是否已经缓存了动画信息
|
||
cached_anim_info = anim_node.getPythonTag("cached_anim_info")
|
||
cached_processed_names = anim_node.getPythonTag("cached_processed_names")
|
||
|
||
# 如果之前缓存的是“格式未知”,但现在已有路径,强制重建缓存
|
||
try:
|
||
now_has_path = anim_node.hasTag("model_path") and bool(anim_node.getTag("model_path"))
|
||
if now_has_path and isinstance(cached_anim_info, str) and "格式: 未知" in cached_anim_info:
|
||
cached_anim_info = None
|
||
cached_processed_names = None
|
||
anim_node.setPythonTag("cached_anim_info", None)
|
||
anim_node.setPythonTag("cached_processed_names", None)
|
||
anim_node.setPythonTag("animation", None)
|
||
self._clear_animation_cache(anim_node)
|
||
except Exception:
|
||
pass
|
||
|
||
# 如果节点已被检测为有动画,但缓存是“无动画”,强制重新检测一次
|
||
if (has_animation_tag or has_animation_nodes) and (cached_anim_info == "无动画" or cached_processed_names == []):
|
||
cached_anim_info = None
|
||
cached_processed_names = None
|
||
anim_node.setPythonTag("cached_anim_info", None)
|
||
anim_node.setPythonTag("cached_processed_names", None)
|
||
anim_node.setPythonTag("animation", None)
|
||
|
||
# 只有在没有缓存时才进行完整的动画检测和处理
|
||
if cached_anim_info is None or cached_processed_names is None:
|
||
# 获取Actor
|
||
actor = self._getActor(anim_node)
|
||
if not actor:
|
||
if has_animation_tag or has_animation_nodes:
|
||
imgui.text_colored((1.0, 0.7, 0.3, 1.0), "检测到动画结构,但当前未成功绑定Actor")
|
||
else:
|
||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画")
|
||
return
|
||
|
||
# 获取和分析动画名称
|
||
anim_names = actor.getAnimNames()
|
||
processed_names = self._processAnimationNames(anim_node, anim_names)
|
||
|
||
if not processed_names:
|
||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
|
||
# 只在明确无动画时缓存空结果,避免误缓存导致后续无法重试
|
||
if not (has_animation_tag or has_animation_nodes):
|
||
anim_node.setPythonTag("cached_processed_names", [])
|
||
anim_node.setPythonTag("cached_anim_info", "无动画")
|
||
return
|
||
|
||
anim_node.setTag("has_animations", "true")
|
||
anim_node.setPythonTag("animation", True)
|
||
|
||
# 计算并缓存动画信息
|
||
format_info = self._getModelFormat(anim_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}"
|
||
|
||
# 缓存结果
|
||
anim_node.setPythonTag("cached_anim_info", info_text)
|
||
anim_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 = anim_node.getPythonTag("selected_animation")
|
||
valid_original_names = [original_name for _, original_name in processed_names]
|
||
if current_anim is None or current_anim not in valid_original_names:
|
||
current_anim = processed_names[0][1] if processed_names else ""
|
||
anim_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]
|
||
anim_node.setPythonTag("selected_animation", selected_original)
|
||
print(f"选择动画: {selected_display} (原始名称: {selected_original})")
|
||
|
||
imgui.spacing()
|
||
|
||
# 控制按钮组
|
||
imgui.text("控制:")
|
||
|
||
# 播放按钮
|
||
if imgui.button("播放##play_animation"):
|
||
self._playAnimation(anim_node)
|
||
imgui.same_line()
|
||
|
||
# 暂停按钮
|
||
if imgui.button("暂停##pause_animation"):
|
||
self._pauseAnimation(anim_node)
|
||
imgui.same_line()
|
||
|
||
# 停止按钮
|
||
if imgui.button("停止##stop_animation"):
|
||
self._stopAnimation(anim_node)
|
||
imgui.same_line()
|
||
|
||
# 循环按钮
|
||
if imgui.button("循环##loop_animation"):
|
||
self._loopAnimation(anim_node)
|
||
|
||
imgui.spacing()
|
||
|
||
# 播放速度控制
|
||
imgui.text("播放速度:")
|
||
imgui.same_line()
|
||
|
||
# 获取当前速度
|
||
current_speed = anim_node.getPythonTag("anim_speed")
|
||
if current_speed is None:
|
||
current_speed = 1.0
|
||
anim_node.setPythonTag("anim_speed", current_speed)
|
||
|
||
# 速度滑块
|
||
changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f")
|
||
if changed:
|
||
anim_node.setPythonTag("anim_speed", new_speed)
|
||
self._setAnimationSpeed(anim_node, new_speed)
|
||
|
||
imgui.same_line()
|
||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速")
|
||
|
||
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()
|
||
|