forked from Rowland/EG
addRender #18
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
@ -1,3 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
2
.idea/EG.iml
generated
2
.idea/EG.iml
generated
@ -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">
|
||||
|
||||
15
.idea/inspectionProfiles/Project_Default.xml
generated
15
.idea/inspectionProfiles/Project_Default.xml
generated
@ -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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@ -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
2
.idea/misc.xml
generated
@ -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
8
.idea/modules.xml
generated
@ -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
6
.idea/vcs.xml
generated
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/TranslateArrowHandle.fbx
Executable file
BIN
core/TranslateArrowHandle.fbx
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6
main.py
6
main.py
@ -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):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -631,6 +631,7 @@ class SceneManager:
|
||||
|
||||
# 将碰撞节点附加到模型上
|
||||
cNodePath = model.attachNewNode(cNode)
|
||||
#cNodePath.hide()
|
||||
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
|
||||
|
||||
# ==================== 场景树管理 ====================
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user