Merge pull request 'addRender' (#18) from addRender into main_ch_eg

Reviewed-on: Hector/EG#18
This commit is contained in:
Hector 2025-08-21 03:34:01 +00:00
commit a189d027f0
92 changed files with 723 additions and 1015 deletions

BIN
.gitignore vendored

Binary file not shown.

3
.idea/.gitignore generated vendored
View File

@ -1,3 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml

2
.idea/EG.iml generated
View File

@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (EG)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.12 (PythonProject)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">

View File

@ -1,15 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ourVersions">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="2.7" />
<item index="1" class="java.lang.String" itemvalue="3.14" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

2
.idea/misc.xml generated
View File

@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.12 (PythonProject)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (EG)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (PythonProject)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/EG.iml" filepath="$PROJECT_DIR$/.idea/EG.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
core/TranslateArrowHandle.fbx Executable file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -369,9 +369,6 @@ class MyWorld(CoreWorld):
def updatePropertyPanel(self, item):
"""更新属性面板显示 - 代理到property_panel"""
return self.property_panel.updatePropertyPanel(item)
# def addAnimationPanel(self,originmodel,filepath):
# return self.property_panel._addAnimationPanel(originmodel,filepath)
def updateGUIPropertyPanel(self, gui_element):
"""更新GUI元素属性面板 - 代理到property_panel"""
@ -380,6 +377,9 @@ class MyWorld(CoreWorld):
def removeActorForModel(self,model):
return self.property_panel.removeActorForModel( model)
def updateNodeVisibilityAfterDrag(self,item):
return self.property_panel.updateNodeVisibilityAfterDrag(item)
# ==================== 工具管理代理 ====================
def setCurrentTool(self, tool):

View File

@ -631,6 +631,7 @@ class SceneManager:
# 将碰撞节点附加到模型上
cNodePath = model.attachNewNode(cNode)
#cNodePath.hide()
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
# ==================== 场景树管理 ====================

View File

@ -1,5 +1,6 @@
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu
from PyQt5.QtCore import Qt
from PyQt5.sip import delete
from panda3d.core import GeomNode, ModelRoot
@ -26,8 +27,11 @@ class InterfaceManager:
def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件"""
if not item:
self.world.selection.updateSelection(None)
return
self.world.property_panel.updatePropertyPanel(item)
# 获取节点对象
nodePath = item.data(0, Qt.UserRole)
if nodePath:
@ -36,7 +40,7 @@ class InterfaceManager:
self.world.selection.updateSelection(nodePath)
# 更新属性面板
self.world.property_panel.updatePropertyPanel(item)
#self.world.property_panel.updatePropertyPanel(item)
print(f"树形控件点击: {item.text(0)}")
else:
@ -77,6 +81,9 @@ class InterfaceManager:
if self.isModelOrChild(item):
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
else:
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
# 显示菜单
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
@ -94,12 +101,33 @@ class InterfaceManager:
try:
# 从场景中移除
self.world.property_panel.removeActorForModel(nodePath)
if hasattr(nodePath,'getPythonTag'):
light_object = nodePath.getPythonTag('rp_light_object')
if light_object and hasattr(self.world,'render_pipeline'):
self.world.render_pipeline.remove_light(light_object)
if hasattr(self.world,'selection'):
if self.world.selection.selectedNode == nodePath:
self.world.selection.clearSelectionBox()
self.world.selection.clearGizmo()
self.world.selection.selectedNode = None
self.world.selection.selectedObject = None
if nodePath in self.world.Spotlight:
self.world.Spotlight.remove(nodePath)
if nodePath in self.world.Pointlight:
self.world.Pointlight.remove(nodePath)
nodePath.removeNode()
if hasattr(self.world,'selection'):
self.world.selection.checkAndClearIfTargetDeleted()
# 如果是模型根节点,从模型列表中移除
if item.parent().text(0) == "模型":
if nodePath in self.world.models:
self.world.models.remove(nodePath)
#if item.parent().text(0) == "模型":
if nodePath in self.world.models:
self.world.models.remove(nodePath)
# 从树形控件中移除
parentItem = item.parent()
@ -239,4 +267,35 @@ class InterfaceManager:
item.setExpanded(True)
for i in range(item.childCount()):
self._restore_expanded(item.child(i),path)
self._restore_expanded(item.child(i),path)
def syncVisibilityDownward(self):
from collections import deque
if not self.world.models:
return
q = deque()
for root in self.world.models:
q.append(root)
while q:
node = q.popleft()
visible = node.getPythonTag("visible")
if visible is None:
visible = True
if not visible:
stack = [node]
while stack:
cur = stack.pop()
cur.hide()
for child in cur.getChildren():
stack.append(child)
continue
node.show()
for child in node.getChildren():
q.append(child)

View File

@ -1,3 +1,4 @@
from collections import deque
from traceback import print_exc
from types import new_class
from typing import Hashable
@ -69,6 +70,20 @@ class PropertyPanelManager:
if item.widget():
item.widget().deleteLater()
def updateNodeVisibilityAfterDrag(self, item):
"""拖拽结束后更新节点的可见性状态"""
node = item.data(0, Qt.UserRole)
if not node:
return
# 当节点被拖拽后,需要根据新父节点的状态来更新可见性
self._syncEffectiveVisibility(node)
self._syncSceneVisibility()
def _syncSceneVisibility(self):
scene_root = self.world.render
self._syncEffectiveVisibility(scene_root)
def updatePropertyPanel(self, item):
"""更新属性面板显示"""
if not self._propertyLayout or not self._propertyLayout.parent():
@ -91,15 +106,34 @@ class PropertyPanelManager:
self._propertyLayout.addWidget(tipLabel)
return
model = item.data(0, Qt.UserRole)
user_visible = True
if model:
user_visible = model.getPythonTag("user_visible")
if user_visible is None:
user_visible = True
model.setPythonTag("user_visible", True)
self.name_group = QGroupBox("物体名称")
name_layout = QHBoxLayout()
self.active_check = QCheckBox()
self.active_check.setChecked(True) # 默认激活
# 根据模型的实际可见性状态设置复选框
self.active_check.setChecked(user_visible)
self.name_input = QLineEdit(itemText)
name_layout.addWidget(self.active_check)
name_layout.addWidget(self.name_input)
self.name_group.setLayout(name_layout)
self._propertyLayout.addWidget(self.name_group)
if model:
try:
self.active_check.stateChanged.disconnect()
except TypeError:
pass
self.active_check.stateChanged.connect(
lambda state,m = model:self._setUserVisible(m,state == Qt.Checked)
)
# nameLabel = QLabel("名称:")
# nameEdit = QLineEdit(itemText)
# self._propertyLayout.addRow(nameLabel, nameEdit)
@ -130,6 +164,59 @@ class PropertyPanelManager:
if propertyWidget:
propertyWidget.update()
def _setUserVisible(self,node,visible):
node.setPythonTag("user_visible",visible)
self._syncEffectiveVisibility(node)
def _syncEffectiveVisibility(self, start_node):
"""广度优先,确保父隐藏则子一定隐藏"""
# 获取起始节点的父节点
parent_node = start_node.getParent()
# 确定父节点的有效可见性
parent_effective_visible = True
if parent_node:
parent_effective_visible = parent_node.getPythonTag("effective_visible")
if parent_effective_visible is None:
parent_effective_visible = True
q = deque([(start_node, parent_effective_visible)]) # (node, parent_effective_visible)
while q:
node, parent_eff = q.popleft()
user = node.getPythonTag("user_visible")
if user is None:
user = True
eff = parent_eff and user
node.setPythonTag("effective_visible", eff)
# 特殊处理:检查是否为碰撞体节点
if node.getName().startswith("modelCollision_"):
node.hide()
else:
if eff:
node.show()
else:
node.hide()
for child in node.getChildren():
q.append((child, eff))
def _toggleModelVisibility(self, model, state):
"""切换模型可见性状态"""
try:
# 用我们自己维护的可见性接口,而不是直接 show/hide
visible = (state == Qt.Checked)
self._setUserVisible(model, visible)
collision_nodes = model.findAllMatches("**/modelCollision_*")
for collision_node in collision_nodes:
collision_node.hide()
except Exception as e:
print(f"切换模型可见性失败: {str(e)}")
def refreshModelValues(self, nodePath):
"""实时刷新模型所有属性数值(相对/世界位置、旋转、缩放)"""
if not nodePath or self._propertyLayout is None:
@ -208,16 +295,31 @@ class PropertyPanelManager:
# 设置位置控件属性
for pos_widget in [self.pos_x, self.pos_y, self.pos_z]:
pos_widget.setRange(-1000, 1000)
pos_widget.setRange(-1000000.0, 1000000.0)
self.pos_x.setValue(relativePos.getX())
self.pos_y.setValue(relativePos.getY())
self.pos_z.setValue(relativePos.getZ())
# 连接位置变化事件
self.pos_x.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v))
self.pos_y.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v))
self.pos_z.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v))
# self.pos_x.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v))
# self.pos_y.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v))
# self.pos_z.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v))
def updateXPosition(value):
model.setX(value)
self.refreshModelValues(model)
self.pos_x.valueChanged.connect(updateXPosition)
def updateYPosition(value):
model.setY(value)
self.refreshModelValues(model)
self.pos_y.valueChanged.connect(updateYPosition)
def updateZPosition(value):
model.setZ(value)
self.refreshModelValues(model)
self.pos_z.valueChanged.connect(updateZPosition)
# 创建并设置 X, Y, Z 标签居中
x_label1 = QLabel("X")
@ -242,7 +344,7 @@ class PropertyPanelManager:
# 设置世界位置控件属性
for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]:
world_pos_widget.setRange(-1000, 1000)
world_pos_widget.setRange(-1000000.0, 1000000.0)
world_pos_widget.setReadOnly(True)
self.world_pos_x.setValue(worldPos.getX())
@ -261,7 +363,7 @@ class PropertyPanelManager:
# 设置旋转控件属性
for rot_widget in [self.rot_h, self.rot_p, self.rot_r]:
rot_widget.setRange(-180, 180)
rot_widget.setRange(-360, 360)
self.rot_h.setValue(model.getH())
self.rot_p.setValue(model.getP())
@ -332,6 +434,50 @@ class PropertyPanelManager:
# 材质属性组
self._updateModelMaterialPanel(model)
def refreshModelValues(self,nodePath):
if not nodePath or self._propertyLayout is None:
return
parent = nodePath.getParent()
render = self.world.render
relPos = nodePath.getPos(parent) if parent else nodePath.getPos()
if hasattr(self,'pos_x') and self.pos_x:
self.pos_x.blockSignals(True)
self.pos_x.setValue(relPos.getX())
self.pos_x.blockSignals(False)
if hasattr(self,'pos_y') and self.pos_y:
self.pos_y.blockSignals(True)
self.pos_y.setValue(relPos.getY())
self.pos_y.blockSignals(False)
if hasattr(self,'pos_z') and self.pos_z:
self.pos_z.blockSignals(True)
self.pos_z.setValue(relPos.getZ())
self.pos_z.blockSignals(False)
worldPos = nodePath.getPos(render)
for axis,attr in zip(('x','y','z'),('world_pos_x','world_pos_y','world_pos_z')):
spin = getattr(self,attr,None)
if spin:
spin.blockSignals(True)
spin.setValue(getattr(worldPos,axis))
spin.blockSignals(False)
hpr = nodePath.getHpr()
for idx,(attr,val) in enumerate(zip(('rot_h','rot_p','rot_r'),hpr)):
spin = getattr(self,attr,None)
if spin:
spin.blockSignals(True)
spin.setValue(val)
spin.blockSignals(False)
scale = nodePath.getScale()
for axis,attr in zip(('x','y','z'),('scale_x','scale_y','scale_z')):
spin = getattr(self,attr,None)
if spin:
spin.blockSignals(True)
spin.setValue(getattr(scale,axis))
spin.blockSignals(False)
def updateGUIPropertyPanel(self, gui_element):
"""更新GUI元素属性面板"""
@ -3892,7 +4038,6 @@ class PropertyPanelManager:
if preset_name in presets:
azimuth, altitude = presets[preset_name]
# 更新滑块和数值框
# 更新滑块和数值框
self.azimuthSpinBox.blockSignals(True)
self.altitudeSpinBox.blockSignals(True)
@ -4147,14 +4292,6 @@ class PropertyPanelManager:
# 忽略 Actor 加载错误,很多模型都不是角色动画
print(f"[信息] 该模型不包含骨骼动画: {actor_error}")
# 只有在没有骨骼动画时才检测非骨骼动画
if not has_skeletal_anim:
non_skeletal_anims = self._detectNonSkeletalAnimations(origin_model)
if non_skeletal_anims and self._validateNonSkeletalAnimations(origin_model, non_skeletal_anims):
self._buildNonSkeletalUI(origin_model, non_skeletal_anims, animation_layout)
has_animation = True
print(f"[信息] 检测到非骨骼动画: {list(non_skeletal_anims.keys())}")
# 如果都没有动画
if not has_animation:
no_anim_label = QLabel("此模型无动画")
@ -4826,459 +4963,7 @@ except Exception as e:
origin_model.setPythonTag("anim_speed",speed)
print(f"[动画] 速度设为: {speed} ({display_name})")
def _detectNonSkeletalAnimations(self, origin_model):
"""检测模型中的非骨骼动画"""
animations = {}
try:
print(f"[调试] 开始检测非骨骼动画: {origin_model}")
# 1. 精确检测 AnimBundle (非骨骼动画)
bundle_nodes = origin_model.findAllMatches("**/+AnimBundleNode")
print(f"[调试] 找到 AnimBundleNode 数量: {bundle_nodes.getNumPaths()}")
if not bundle_nodes.isEmpty():
for i, bundle_node in enumerate(bundle_nodes):
try:
bundle = bundle_node.node().getBundle()
print(f"[调试] AnimBundle #{i}: {bundle}")
if bundle and hasattr(bundle, 'getNumFrames'):
num_frames = bundle.getNumFrames()
print(f"[调试] Bundle 帧数: {num_frames}")
# 只有真正有多帧的才认为是动画
if num_frames > 1:
anim_names = []
# 尝试获取动画名称
try:
if hasattr(bundle, 'getName') and bundle.getName():
anim_names.append(bundle.getName())
else:
anim_names.append(f"Animation_{i}")
print(f"[调试] 检测到有效帧动画: {anim_names}, {num_frames}")
except Exception as name_error:
anim_names.append(f"Animation_{i}")
print(f"[调试] 使用默认动画名: Animation_{i}")
animations['transform'] = {
'bundle': bundle,
'names': anim_names,
'node': bundle_node,
'frames': num_frames
}
else:
print(f"[调试] Bundle 只有 {num_frames} 帧,不认为是动画")
except Exception as bundle_error:
print(f"[调试] 处理 bundle 失败: {bundle_error}")
# 2. 仅对 GLB 文件进行特殊检测
filepath = origin_model.getTag("model_path")
if filepath and filepath.lower().endswith('.glb') and not animations:
print(f"[调试] GLB 文件特殊检测: {filepath}")
# 检查是否有任何动画相关的节点名称
all_nodes = origin_model.findAllMatches("**")
anim_indicators = []
for node in all_nodes:
node_name = node.getName().lower()
# 查找典型的动画节点命名模式
if any(keyword in node_name for keyword in ['anim', 'key', 'frame', 'action', 'timeline']):
anim_indicators.append(node.getName())
print(f"[调试] 发现可能的动画节点: {node.getName()}")
# 只有在明确找到动画指示器时才创建动画条目
if anim_indicators:
animations['glb_keyframe'] = {
'bundle': None,
'names': ['GLB_Animation'],
'node': origin_model,
'type': 'glb_manual',
'indicators': anim_indicators
}
print(f"[调试] 创建 GLB 动画条目,基于指示器: {anim_indicators}")
except Exception as e:
print(f"检测非骨骼动画失败: {e}")
print(f"[调试] 最终检测结果: {animations}")
return animations if animations else None
def _validateNonSkeletalAnimations(self, origin_model, animations):
"""验证检测到的非骨骼动画是否真的可以播放"""
try:
print(f"[验证] 开始验证非骨骼动画: {list(animations.keys())}")
for anim_type, anim_data in animations.items():
if anim_type == 'transform':
# 验证变换动画
bundle = anim_data.get('bundle')
if bundle:
# 检查是否真的有可播放的动画数据
if hasattr(bundle, 'getNumFrames'):
frames = bundle.getNumFrames()
if frames <= 1:
print(f"[验证] 变换动画帧数不足: {frames}")
return False
# 检查是否有有效的动画通道
try:
if hasattr(bundle, 'getNumChannels'):
channels = bundle.getNumChannels()
if channels == 0:
print(f"[验证] 无有效动画通道")
return False
except:
pass
elif anim_type == 'glb_keyframe':
# 验证 GLB 动画指示器
indicators = anim_data.get('indicators', [])
if not indicators:
print(f"[验证] GLB 动画无有效指示器")
return False
print(f"[验证] 动画验证通过")
return True
except Exception as e:
print(f"[验证] 动画验证失败: {e}")
return False
def _buildNonSkeletalUI(self, origin_model, animations, layout):
"""构建非骨骼动画控制UI"""
from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox, QTabWidget
# 动画信息
info_text = f"非骨骼动画数量: {len(animations)}"
info_label = QLabel(info_text)
info_label.setStyleSheet("color:#888;font-size:10px;")
layout.addWidget(QLabel("信息:"), 0, 0)
layout.addWidget(info_label, 0, 1, 1, 3)
# 如果有多种类型的动画,使用标签页
if len(animations) > 1:
tab_widget = QTabWidget()
tab_widget.setMinimumWidth(200) # 设置最小宽度
for anim_type, anim_data in animations.items():
tab = QWidget()
tab_layout = QGridLayout(tab) # 改为QGridLayout保持一致
self._buildAnimationTypeUI(tab_layout, origin_model, anim_type, anim_data)
tab_widget.addTab(tab, self._getAnimTypeDisplayName(anim_type))
layout.addWidget(QLabel("动画类型:"), 1, 0)
layout.addWidget(tab_widget, 1, 1, 1, 3)
else:
# 只有一种类型,直接显示
anim_type, anim_data = next(iter(animations.items()))
self._buildAnimationTypeUI(layout, origin_model, anim_type, anim_data)
# 存储动画信息供控制使用
if not hasattr(self, '_non_skeletal_cache'):
self._non_skeletal_cache = {}
self._non_skeletal_cache[origin_model] = animations
def _buildAnimationTypeUI(self, layout, origin_model, anim_type, anim_data):
"""为特定动画类型构建UI"""
from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox
current_row = layout.rowCount()
if anim_type == 'transform':
# 变换动画控制
self.ns_transform_combo = QComboBox()
self.ns_transform_combo.addItems(anim_data['names'])
self.ns_transform_combo.setMinimumWidth(80)
layout.addWidget(QLabel("变换动画:"), current_row, 0)
layout.addWidget(self.ns_transform_combo, current_row, 1, 1, 3)
current_row += 1
elif anim_type == 'texture':
# 纹理动画控制
self.ns_texture_combo = QComboBox()
self.ns_texture_combo.addItems(anim_data['stages'])
self.ns_texture_combo.setMinimumWidth(80)
layout.addWidget(QLabel("纹理动画:"), current_row, 0)
layout.addWidget(self.ns_texture_combo, current_row, 1, 1, 3)
current_row += 1
elif anim_type == 'material':
# 材质动画控制
self.ns_material_combo = QComboBox()
self.ns_material_combo.addItems(anim_data['properties'])
self.ns_material_combo.setMinimumWidth(80)
layout.addWidget(QLabel("材质动画:"), current_row, 0)
layout.addWidget(self.ns_material_combo, current_row, 1, 1, 3)
current_row += 1
elif anim_type == 'lerp':
# Lerp动画控制
self.ns_lerp_combo = QComboBox()
self.ns_lerp_combo.addItems(anim_data['intervals'])
self.ns_lerp_combo.setMinimumWidth(80)
layout.addWidget(QLabel("Lerp动画"), current_row, 0)
layout.addWidget(self.ns_lerp_combo, current_row, 1, 1, 3)
current_row += 1
# 通用控制按钮
btn_box = QWidget()
btn_lay = QHBoxLayout(btn_box)
btn_lay.setContentsMargins(0, 0, 0, 0)
for txt, cmd in (("播放", "play"), ("暂停", "pause"), ("停止", "stop"), ("循环", "loop")):
btn = QPushButton(txt)
btn.setMinimumWidth(35) # 设置按钮最小宽度
btn.setMaximumWidth(50) # 限制按钮最大宽度
btn.clicked.connect(lambda _, c=cmd, t=anim_type: self._controlNonSkeletalAnimation(origin_model, t, c))
btn_lay.addWidget(btn)
layout.addWidget(QLabel("控制:"), current_row, 0)
layout.addWidget(btn_box, current_row, 1, 1, 3)
current_row += 1
# 播放速度
speed_spinbox = QDoubleSpinBox()
speed_spinbox.setRange(0.1, 5.0)
speed_spinbox.setSingleStep(0.1)
speed_spinbox.setValue(1.0)
speed_spinbox.setMinimumWidth(60)
speed_spinbox.setMaximumWidth(80)
speed_spinbox.valueChanged.connect(
lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v))
layout.addWidget(QLabel("播放速度:"), current_row, 0)
layout.addWidget(speed_spinbox, current_row, 1, 1, 3)
def _getAnimTypeDisplayName(self, anim_type):
"""获取动画类型的显示名称"""
names = {
'transform': '变换动画',
'glb_keyframe': 'GLB关键帧动画',
'texture': '纹理动画',
'material': '材质动画',
'lerp': 'Lerp动画'
}
return names.get(anim_type, anim_type)
def _controlNonSkeletalAnimation(self, origin_model, anim_type, command):
"""控制非骨骼动画播放"""
try:
if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache:
return
animations = self._non_skeletal_cache[origin_model]
if anim_type not in animations:
return
anim_data = animations[anim_type]
if anim_type == 'transform':
self._controlTransformAnimation(origin_model, anim_data, command)
elif anim_type == 'glb_keyframe':
self._controlGLBKeyframeAnimation(origin_model, anim_data, command)
elif anim_type == 'texture':
self._controlTextureAnimation(origin_model, anim_data, command)
elif anim_type == 'material':
self._controlMaterialAnimation(origin_model, anim_data, command)
elif anim_type == 'lerp':
self._controlLerpAnimation(origin_model, anim_data, command)
print(f"[非骨骼动画] {anim_type} {command}")
except Exception as e:
print(f"控制非骨骼动画失败: {e}")
def _controlTransformAnimation(self, origin_model, anim_data, command):
"""控制变换动画"""
try:
bundle = anim_data['bundle']
bundle_node = anim_data['node']
print(f"[调试] 控制变换动画: {command}, Bundle: {bundle}")
if command == 'play':
# 方法1: 通过 AnimBundle 直接播放
if hasattr(bundle, 'play'):
bundle.play()
print(f"[动画] 通过 bundle.play() 播放")
# 方法2: 通过 AnimControl 播放
elif hasattr(bundle_node.node(), 'getAnimControl'):
controls = bundle_node.node().getAnimControls()
if controls:
controls[0].play()
print(f"[动画] 通过 AnimControl 播放")
# 方法3: 通过启用动画节点
else:
bundle_node.node().setPlayRate(1.0)
print(f"[动画] 设置播放速率为 1.0")
elif command == 'pause':
if hasattr(bundle, 'pause'):
bundle.pause()
elif hasattr(bundle_node.node(), 'getAnimControl'):
controls = bundle_node.node().getAnimControls()
if controls:
controls[0].pause()
else:
bundle_node.node().setPlayRate(0.0)
elif command == 'stop':
if hasattr(bundle, 'stop'):
bundle.stop()
elif hasattr(bundle_node.node(), 'getAnimControl'):
controls = bundle_node.node().getAnimControls()
if controls:
controls[0].stop()
else:
bundle_node.node().setPlayRate(0.0)
# 重置到第一帧
if hasattr(bundle, 'setFrame'):
bundle.setFrame(0)
elif command == 'loop':
if hasattr(bundle, 'loop'):
bundle.loop()
elif hasattr(bundle_node.node(), 'getAnimControl'):
controls = bundle_node.node().getAnimControls()
if controls:
controls[0].loop(True)
controls[0].play()
else:
bundle_node.node().setPlayRate(1.0)
except Exception as e:
print(f"[错误] 控制变换动画失败: {e}")
import traceback
traceback.print_exc()
def _controlGLBKeyframeAnimation(self, origin_model, anim_data, command):
"""控制 GLB 关键帧动画"""
try:
print(f"[调试] 控制 GLB 动画: {command}")
# 尝试通过 AnimControlCollection 控制
from panda3d.core import AnimControlCollection
# 方法1: 查找 AnimControlCollection
anim_collection = AnimControlCollection()
origin_model.getAnimControls(anim_collection)
if anim_collection.getNumAnims() > 0:
for i in range(anim_collection.getNumAnims()):
anim_control = anim_collection.getAnim(i)
print(f"[调试] 找到动画控制: {anim_control.getName()}")
if command == 'play':
anim_control.setPlayRate(1.0)
print(f"[GLB动画] 播放: {anim_control.getName()}")
elif command == 'pause':
anim_control.setPlayRate(0.0)
print(f"[GLB动画] 暂停: {anim_control.getName()}")
elif command == 'stop':
anim_control.setPlayRate(0.0)
anim_control.setFrame(0)
print(f"[GLB动画] 停止: {anim_control.getName()}")
elif command == 'loop':
anim_control.setPlayRate(1.0)
anim_control.loop(True)
print(f"[GLB动画] 循环: {anim_control.getName()}")
return
# 方法2: 通过自动播放任务
if command == 'play':
origin_model.setPlayRate(1.0)
print(f"[GLB动画] 设置播放速率为 1.0")
elif command == 'pause':
origin_model.setPlayRate(0.0)
print(f"[GLB动画] 暂停播放")
elif command == 'stop':
origin_model.setPlayRate(0.0)
print(f"[GLB动画] 停止播放")
elif command == 'loop':
origin_model.setPlayRate(1.0)
print(f"[GLB动画] 循环播放")
except Exception as e:
print(f"[错误] 控制 GLB 动画失败: {e}")
def _controlTextureAnimation(self, origin_model, anim_data, command):
"""控制纹理动画"""
# 纹理动画通常通过 LerpInterval 实现
print(f"[纹理动画] {command} - 功能待实现")
def _controlMaterialAnimation(self, origin_model, anim_data, command):
"""控制材质动画"""
# 材质动画通常通过修改材质属性实现
print(f"[材质动画] {command} - 功能待实现")
def _controlLerpAnimation(self, origin_model, anim_data, command):
"""控制Lerp动画"""
# 通过 LerpInterval 控制
print(f"[Lerp动画] {command} - 功能待实现")
def _setNonSkeletalAnimationSpeed(self, origin_model, anim_type, speed):
"""设置非骨骼动画播放速度"""
try:
if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache:
return
animations = self._non_skeletal_cache[origin_model]
if anim_type not in animations:
return
anim_data = animations[anim_type]
if anim_type == 'transform':
bundle = anim_data['bundle']
if hasattr(bundle, 'setPlayRate'):
bundle.setPlayRate(speed)
print(f"[非骨骼动画] {anim_type} 速度设为: {speed}")
except Exception as e:
print(f"设置非骨骼动画速度失败: {e}")
def _detectPlayer(self,origin_model):
filepath = origin_model.getTag("model_path")
if filepath:
try:
actor = Actor(filepath)
if actor.getAnimNames():
actor.cleanup();actor.removeNode()
return ("actor",None)
except:
pass
bundle_np = origin_model.find("**/+AnimBundleNode")
if not bundle_np.isEmpty():
ctrl = bundle_np.node().getBundle().bind(origin_model.node(),PartGroup.PART_Whole)
return ("bundle",ctrl)
return None
def _buildBundleUI(self,origin_model,ctrl):
from PyQt5.QtWidgets import QLabel,QPushButton,QHBoxLayout,QWidget,QSlider
title = QLabel("骨骼动画控制")
title.setStyleSheet("color:#FF8C00;font-weight:bold;font-size:14px;margin-top:10px;")
self._propertyLayout.addRow(title)
btn_box = QWidget()
lay = QHBoxLayout(btn_box)
for txt , fn in (("播放",ctrl.play),
("暂停",ctrl.stop),
("重置",lambda:ctrl.pose(0))):
btn = QPushButton(txt)
btn.clicked.connect(fn)
lay.addWidget(btn)
self._propertyLayout.addRow("控制:",btn_box)
slider = QSlider()
slider.setOrientation(1)
slider.setRange(0,int(ctrl.getNumFrames()))
slider.valueChanged.connect(ctrl.pose)
self._propertyLayout.addRow("帧:",slider)
self._actor_cache[origin_model] = ("bundle",ctrl)
def _dispatchAnimCommand(self,origin_model,cmd):
cache = self._actor_cache.get(origin_model)
@ -5313,20 +4998,6 @@ except Exception as e:
elif isinstance(cmd,tuple) and cmd[0] == "speed":
actor.setPlayRate(cmd[1], anim_name)
elif kind == "bundle":
ctrl = player
if cmd == "play":
ctrl.play()
elif cmd == "pause":
ctrl.stop()
elif cmd == "stop":
ctrl.stop()
ctrl.pose(0)
elif cmd == "loop":
ctrl.loop(True)
elif isinstance(cmd,tuple) and cmd[0] =="speed":
ctrl.setPlayRate(cmd[1])
def removeActorForModel(self, model):
"""删除 model 对应的 Actor如果存在"""
actor = self._actor_cache.pop(model, None)

View File

@ -347,6 +347,24 @@ class CustomTreeWidget(QTreeWidget):
if not dragged_node or not target_node:
event.ignore()
return
# # 检查是否是有效的父子关系
# if self.isValidParentChild(dragged_item, target_item):
# # 保存当前的世界坐标
# world_pos = dragged_node.getPos(self.world.render)
#
# # 更新场景图中的父子关系
# dragged_node.wrtReparentTo(target_node)
#
# # 接受拖放事件,更新树形控件
# super().dropEvent(event)
#
# #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item)
# # 更新属性面板
# self.world.updatePropertyPanel(dragged_item)
# self.world.property_panel._syncEffectiveVisibility(dragged_node)
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
@ -394,7 +412,7 @@ class CustomTreeWidget(QTreeWidget):
# 事后验证:确保节点仍在"场景"根节点下
self._ensureUnderSceneRoot(dragged_item)
self.world.property_panel._syncEffectiveVisibility(dragged_node)
event.accept()
# try: