902 lines
39 KiB
Python
902 lines
39 KiB
Python
from pathlib import Path
|
||
|
||
from imgui_bundle import imgui, imgui_ctx
|
||
from panda3d.core import TransparencyAttrib
|
||
|
||
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 _apply_gui_alpha(self, gui_element, alpha_value):
|
||
alpha_value = max(0.0, min(1.0, float(alpha_value)))
|
||
gui_element.alpha = alpha_value
|
||
|
||
if hasattr(gui_element, "setTransparency"):
|
||
gui_element.setTransparency(
|
||
TransparencyAttrib.MAlpha if alpha_value < 0.999 else TransparencyAttrib.MNone
|
||
)
|
||
|
||
color_scale = None
|
||
if hasattr(gui_element, "getColorScale"):
|
||
try:
|
||
color_scale = gui_element.getColorScale()
|
||
except Exception:
|
||
color_scale = None
|
||
|
||
if color_scale is not None and hasattr(gui_element, "setColorScale"):
|
||
try:
|
||
gui_element.setColorScale(
|
||
float(color_scale[0]),
|
||
float(color_scale[1]),
|
||
float(color_scale[2]),
|
||
alpha_value,
|
||
)
|
||
return
|
||
except Exception:
|
||
pass
|
||
|
||
if hasattr(gui_element, "setAlphaScale"):
|
||
try:
|
||
gui_element.setAlphaScale(alpha_value)
|
||
return
|
||
except Exception:
|
||
pass
|
||
|
||
if hasattr(gui_element, "setColorScale"):
|
||
try:
|
||
gui_element.setColorScale(1.0, 1.0, 1.0, alpha_value)
|
||
except Exception:
|
||
pass
|
||
|
||
def _get_cached_animation_structure_state(self, anim_node, force_refresh=False):
|
||
if not force_refresh:
|
||
cached_state = anim_node.getPythonTag("cached_has_animation_nodes")
|
||
if cached_state is not None:
|
||
return bool(cached_state)
|
||
|
||
has_animation_nodes = False
|
||
try:
|
||
has_animation_nodes = (
|
||
anim_node.findAllMatches("**/+Character").getNumPaths() > 0 or
|
||
anim_node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
|
||
)
|
||
except Exception:
|
||
has_animation_nodes = False
|
||
|
||
anim_node.setPythonTag("cached_has_animation_nodes", bool(has_animation_nodes))
|
||
return has_animation_nodes
|
||
|
||
def _record_visibility_change(self, node, old_visible, new_visible):
|
||
if not hasattr(self.app, "command_manager") or not self.app.command_manager:
|
||
node.setPythonTag("user_visible", bool(new_visible))
|
||
if new_visible:
|
||
node.show()
|
||
else:
|
||
node.hide()
|
||
return
|
||
from core.Command_System import VisibilityNodeCommand
|
||
self.app.command_manager.execute_command(
|
||
VisibilityNodeCommand(node, old_visible, new_visible, world=self.app)
|
||
)
|
||
|
||
def _record_name_change(self, node, old_name, new_name):
|
||
if old_name == new_name:
|
||
return
|
||
if hasattr(self.app, "command_manager") and self.app.command_manager:
|
||
from core.Command_System import RenameNodeCommand
|
||
self.app.command_manager.record_command(
|
||
RenameNodeCommand(node, old_name, new_name, world=self.app)
|
||
)
|
||
else:
|
||
self.app._update_node_name(node, new_name)
|
||
|
||
def _ensure_light_edit_sessions(self):
|
||
if not hasattr(self, "_light_edit_sessions"):
|
||
self._light_edit_sessions = {}
|
||
return self._light_edit_sessions
|
||
|
||
def _begin_light_edit_session(self, node, session_key):
|
||
sessions = self._ensure_light_edit_sessions()
|
||
sessions.setdefault(session_key, self.app._capture_light_snapshot(node))
|
||
|
||
def _record_light_snapshot_command(self, node, before_snapshot, after_snapshot):
|
||
if not hasattr(self.app, "command_manager") or not self.app.command_manager:
|
||
return
|
||
if self.app._light_snapshots_equal(before_snapshot, after_snapshot):
|
||
return
|
||
try:
|
||
from core.Command_System import SnapshotStateCommand
|
||
|
||
self.app.command_manager.record_command(
|
||
SnapshotStateCommand(
|
||
lambda state, target_node=node: self.app._apply_light_snapshot(target_node, state),
|
||
before_snapshot,
|
||
after_snapshot,
|
||
)
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
def _finish_light_edit_session(self, node, session_key):
|
||
if not imgui.is_item_deactivated_after_edit():
|
||
return
|
||
sessions = self._ensure_light_edit_sessions()
|
||
before_snapshot = sessions.pop(session_key, None)
|
||
if before_snapshot is None:
|
||
return
|
||
after_snapshot = self.app._capture_light_snapshot(node)
|
||
self._record_light_snapshot_command(node, before_snapshot, after_snapshot)
|
||
|
||
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) as (_, opened):
|
||
if not opened:
|
||
self.app.showPropertyPanel = False
|
||
self.app._property_panel_window_rect = None
|
||
return
|
||
|
||
self.app.showPropertyPanel = opened
|
||
window_pos = imgui.get_window_pos()
|
||
window_size = imgui.get_window_size()
|
||
self.app._property_panel_window_rect = (
|
||
float(window_pos.x),
|
||
float(window_pos.y),
|
||
float(window_size.x),
|
||
float(window_size.y),
|
||
)
|
||
|
||
# --- 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 = self.app._get_selection_source_node()
|
||
|
||
if selected_node and not selected_node.isEmpty():
|
||
self._draw_node_properties(selected_node)
|
||
else:
|
||
ssbo_summary = self.app._get_ssbo_selection_summary()
|
||
if ssbo_summary:
|
||
self._draw_ssbo_selection_summary(ssbo_summary)
|
||
return
|
||
self._draw_empty_property_state()
|
||
|
||
def _draw_empty_property_state(self):
|
||
imgui.separator_text("属性")
|
||
imgui.text_disabled("当前没有选中对象")
|
||
imgui.spacing()
|
||
imgui.bullet_text("从左侧场景树或场景视口中选择一个对象")
|
||
imgui.bullet_text("按 F 聚焦到对象,按 Delete 删除对象")
|
||
|
||
def _draw_property_section(self, title, draw_callback, default_open=False):
|
||
flags = imgui.TreeNodeFlags_.span_avail_width.value
|
||
if default_open:
|
||
flags |= imgui.TreeNodeFlags_.default_open.value
|
||
if imgui.collapsing_header(title, flags):
|
||
draw_callback()
|
||
imgui.spacing()
|
||
|
||
def _draw_ssbo_selection_summary(self, summary):
|
||
"""Render a safe summary for SSBO group selections without exposing wrong node properties."""
|
||
imgui.separator_text("SSBO 选择")
|
||
imgui.text(f"名称: {summary.get('display_name') or '未命名'}")
|
||
imgui.text(f"对象数量: {summary.get('object_count', 0)}")
|
||
if summary.get("is_root"):
|
||
imgui.text_colored((0.5, 0.8, 1.0, 1.0), "当前选中的是 SSBO 根模型")
|
||
elif summary.get("is_group"):
|
||
imgui.text_colored((1.0, 0.8, 0.3, 1.0), "当前选中的是 SSBO 组合节点")
|
||
imgui.text_wrapped("这个选择对应多个动态对象。这里先显示摘要,避免误改到错误节点。")
|
||
imgui.separator()
|
||
imgui.bullet_text("在左侧展开到叶子节点后查看单对象属性")
|
||
imgui.bullet_text("继续使用场景中的 Gizmo 做组合移动")
|
||
else:
|
||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "当前 SSBO 选择没有可直接映射的单个场景节点")
|
||
|
||
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)
|
||
self._draw_object_overview(node, node_name, node_type)
|
||
imgui.spacing()
|
||
|
||
self._draw_property_section("变换", lambda: self.app._draw_transform_properties(node), default_open=True)
|
||
|
||
if node_type == "GUI元素":
|
||
self._draw_property_section("GUI", lambda: self.app._draw_gui_properties(node), default_open=True)
|
||
elif node_type == "光源":
|
||
self._draw_property_section("光源", lambda: self.app._draw_light_properties(node), default_open=True)
|
||
elif node_type == "模型":
|
||
has_animation = False
|
||
try:
|
||
if node.hasTag("has_animations"):
|
||
has_animation = node.getTag("has_animations").lower() == "true"
|
||
if not has_animation:
|
||
has_animation = node.getPythonTag("animation") is True
|
||
except Exception:
|
||
has_animation = False
|
||
self._draw_property_section("模型", lambda: self.app._draw_model_properties(node), default_open=True)
|
||
self._draw_property_section("动画", lambda: self.app._draw_animation_properties(node), default_open=has_animation)
|
||
|
||
material_target = self.app._get_selection_material_node()
|
||
if not material_target or material_target.isEmpty():
|
||
material_target = node
|
||
self._draw_property_section("材质", lambda target=material_target: self.app._draw_appearance_properties(target), default_open=True)
|
||
self._draw_property_section("碰撞", lambda: self.app._draw_collision_properties(node), default_open=False)
|
||
self._draw_property_section("操作", lambda: self.app._draw_property_actions(node), default_open=False)
|
||
|
||
def _draw_object_overview(self, node, node_name, node_type):
|
||
imgui.separator_text("对象")
|
||
|
||
user_visible = node.getPythonTag("user_visible")
|
||
if user_visible is None:
|
||
user_visible = True
|
||
node.setPythonTag("user_visible", True)
|
||
|
||
changed, is_visible = imgui.checkbox("##node_enabled", user_visible)
|
||
if changed:
|
||
self._record_visibility_change(node, user_visible, is_visible)
|
||
|
||
imgui.same_line()
|
||
imgui.text_disabled("启用")
|
||
imgui.same_line()
|
||
imgui.set_next_item_width(-1)
|
||
changed, new_name = imgui.input_text("##name", node_name, 256)
|
||
if changed and hasattr(self.app, "selection"):
|
||
if not hasattr(self.app, "_name_edit_session"):
|
||
self.app._name_edit_session = None
|
||
if not self.app._name_edit_session or self.app._name_edit_session[0] != node:
|
||
self.app._name_edit_session = (node, node_name)
|
||
self.app._update_node_name(node, new_name)
|
||
if imgui.is_item_deactivated_after_edit():
|
||
session = getattr(self.app, "_name_edit_session", None)
|
||
if session and session[0] == node:
|
||
_, original_name = session
|
||
self._record_name_change(node, original_name, node.getName())
|
||
self.app._name_edit_session = None
|
||
|
||
parent_name = "SceneRoot"
|
||
try:
|
||
parent = node.getParent()
|
||
if parent and not parent.isEmpty():
|
||
parent_name = parent.getName() or "SceneRoot"
|
||
except Exception:
|
||
parent = None
|
||
|
||
imgui.text_disabled(f"{node_type} · 父级: {parent_name}")
|
||
self._draw_status_badges(node, node_type)
|
||
|
||
def _node_has_collision_badge(self, node):
|
||
child_count = 0
|
||
try:
|
||
child_count = int(node.getNumChildren())
|
||
except Exception:
|
||
child_count = 0
|
||
|
||
cached = None
|
||
try:
|
||
cached = node.getPythonTag("cached_collision_badge_state")
|
||
except Exception:
|
||
cached = None
|
||
|
||
if isinstance(cached, dict) and cached.get("child_count") == child_count:
|
||
return bool(cached.get("has_collision", False))
|
||
|
||
has_collision = False
|
||
try:
|
||
for child in node.getChildren():
|
||
child_name = child.getName()
|
||
if child_name and "Collision" in child_name:
|
||
has_collision = True
|
||
break
|
||
except Exception:
|
||
has_collision = False
|
||
|
||
try:
|
||
node.setPythonTag(
|
||
"cached_collision_badge_state",
|
||
{"child_count": child_count, "has_collision": bool(has_collision)},
|
||
)
|
||
except Exception:
|
||
pass
|
||
return has_collision
|
||
|
||
def _draw_status_badges(self, node, node_type=None):
|
||
"""绘制精简后的对象状态徽章行。"""
|
||
if node_type is None:
|
||
node_type = self.app._get_node_type_from_node(node)
|
||
|
||
badges = []
|
||
|
||
if node.is_hidden():
|
||
badges.append(("已隐藏", (0.65, 0.65, 0.65, 1.0)))
|
||
|
||
has_collision = hasattr(node, "getChild") and self._node_has_collision_badge(node)
|
||
if has_collision:
|
||
badges.append(("碰撞", (0.35, 0.65, 1.0, 1.0)))
|
||
|
||
has_script = hasattr(node, "getPythonTag") and node.getPythonTag("script")
|
||
if has_script:
|
||
badges.append(("脚本", (0.86, 0.48, 0.86, 1.0)))
|
||
|
||
has_animation = False
|
||
if node_type == "模型":
|
||
if node.hasTag("has_animations"):
|
||
has_animation = node.getTag("has_animations").lower() == "true"
|
||
if not has_animation:
|
||
cached_result = node.getPythonTag("animation")
|
||
if cached_result is True:
|
||
has_animation = True
|
||
elif cached_result is False:
|
||
has_animation = False
|
||
else:
|
||
has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation")
|
||
|
||
if has_animation:
|
||
badges.append(("动画", (0.45, 0.85, 0.55, 1.0)))
|
||
|
||
has_material = hasattr(node, "getMaterial") and node.getMaterial()
|
||
if has_material:
|
||
badges.append(("材质", (0.9, 0.72, 0.35, 1.0)))
|
||
|
||
if not badges:
|
||
return
|
||
|
||
for index, (badge_text, badge_color) in enumerate(badges):
|
||
if index > 0:
|
||
imgui.same_line()
|
||
imgui.text_colored(badge_color, f"[{badge_text}]")
|
||
|
||
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:
|
||
self._apply_gui_alpha(gui_element, new_alpha)
|
||
|
||
# 渲染顺序
|
||
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("光源属性")
|
||
|
||
color = self.app._get_light_color(node)
|
||
changed, new_color = imgui.color_edit3(
|
||
"颜色##light_color",
|
||
(color[0], color[1], color[2]),
|
||
imgui.ColorEditFlags_.display_rgb.value,
|
||
)
|
||
if imgui.is_item_activated():
|
||
self._begin_light_edit_session(node, "light_color")
|
||
if changed:
|
||
self.app._apply_light_color(node, (new_color[0], new_color[1], new_color[2], color[3]))
|
||
self._finish_light_edit_session(node, "light_color")
|
||
|
||
# 光源强度
|
||
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
|
||
|
||
has_animation_checked = (
|
||
anim_node.hasTag("has_animations_checked") and
|
||
anim_node.getTag("has_animations_checked").lower() == "true"
|
||
)
|
||
|
||
# 只在模型还没做过动画探测时刷新标签,避免属性面板每帧重复扫描。
|
||
try:
|
||
if (
|
||
not has_animation_checked and
|
||
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_cached_animation = anim_node.getPythonTag("animation") is True
|
||
has_animation_nodes = (
|
||
has_animation_tag or
|
||
has_cached_animation or
|
||
self._get_cached_animation_structure_state(anim_node)
|
||
)
|
||
|
||
# 检查是否已经缓存了动画信息
|
||
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)
|
||
|
||
should_force_probe = False
|
||
if not (has_animation_tag or has_animation_nodes or has_cached_animation):
|
||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型未检测到动画结构")
|
||
if self.app.style_manager.draw_toolbar_button(
|
||
"尝试强制检测",
|
||
size=(108, 26),
|
||
tooltip="重新扫描当前模型的动画结构",
|
||
):
|
||
should_force_probe = True
|
||
anim_node.setPythonTag("cached_anim_info", None)
|
||
anim_node.setPythonTag("cached_processed_names", None)
|
||
has_animation_nodes = self._get_cached_animation_structure_state(anim_node, force_refresh=True)
|
||
else:
|
||
return
|
||
|
||
# 只有在没有缓存时才进行完整的动画检测和处理
|
||
if cached_anim_info is None or cached_processed_names is None:
|
||
# 获取Actor
|
||
actor = self._getActor(anim_node) if (should_force_probe or has_animation_tag or has_animation_nodes or has_cached_animation) else None
|
||
if not actor:
|
||
if has_animation_tag or has_animation_nodes:
|
||
imgui.text_colored((1.0, 0.7, 0.3, 1.0), "检测到动画结构,但当前未成功绑定Actor")
|
||
elif should_force_probe:
|
||
imgui.text_colored((0.9, 0.6, 0.3, 1.0), "强制检测未发现可播放动画")
|
||
anim_node.setPythonTag("animation", False)
|
||
anim_node.setPythonTag("cached_processed_names", [])
|
||
anim_node.setPythonTag("cached_anim_info", "无动画")
|
||
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 self.app.style_manager.draw_toolbar_button("播放", size=(60, 26), tooltip="播放当前动画"):
|
||
self._playAnimation(anim_node)
|
||
imgui.same_line()
|
||
|
||
# 暂停按钮
|
||
if self.app.style_manager.draw_toolbar_button("暂停", size=(60, 26), tooltip="暂停当前动画"):
|
||
self._pauseAnimation(anim_node)
|
||
imgui.same_line()
|
||
|
||
# 停止按钮
|
||
if self.app.style_manager.draw_toolbar_button("停止", size=(60, 26), tooltip="停止当前动画"):
|
||
self._stopAnimation(anim_node)
|
||
imgui.same_line()
|
||
|
||
# 循环按钮
|
||
if self.app.style_manager.draw_toolbar_button("循环", size=(60, 26), tooltip="循环播放当前动画"):
|
||
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 self.app.style_manager.draw_toolbar_button("重置变换", size=(84, 28), tooltip="将位置、旋转、缩放恢复为默认值"):
|
||
if hasattr(self.app, "command_manager") and self.app.command_manager:
|
||
from core.Command_System import (
|
||
CompositeCommand,
|
||
MoveNodeCommand,
|
||
RotateNodeCommand,
|
||
ScaleNodeCommand,
|
||
)
|
||
|
||
current_pos = tuple(float(v) for v in node.getPos())
|
||
current_hpr = tuple(float(v) for v in node.getHpr())
|
||
current_scale = tuple(float(v) for v in node.getScale())
|
||
self.app.command_manager.execute_command(
|
||
CompositeCommand(
|
||
[
|
||
MoveNodeCommand(node, current_pos, (0.0, 0.0, 0.0)),
|
||
RotateNodeCommand(node, current_hpr, (0.0, 0.0, 0.0)),
|
||
ScaleNodeCommand(node, current_scale, (1.0, 1.0, 1.0)),
|
||
]
|
||
)
|
||
)
|
||
else:
|
||
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 self.app.style_manager.draw_toolbar_button(
|
||
visibility_text,
|
||
size=(64, 28),
|
||
tooltip="切换当前对象的可见状态",
|
||
):
|
||
self._record_visibility_change(node, is_visible, not is_visible)
|
||
|
||
imgui.same_line()
|
||
|
||
# 聚焦到对象
|
||
if self.app.style_manager.draw_toolbar_button("聚焦", size=(64, 28), tooltip="将相机聚焦到当前对象"):
|
||
if hasattr(self, 'selection') and self.selection:
|
||
self.selection.focusCameraOnSelectedNodeAdvanced()
|
||
|
||
# 删除对象
|
||
imgui.same_line()
|
||
if self.app.style_manager.draw_toolbar_button("删除", size=(64, 28), tooltip="删除当前对象"):
|
||
if hasattr(self, 'selection') and self.selection:
|
||
self.selection.deleteSelectedNode()
|
||
|