跨平台路径标准化,坐标系问题修改,缩放后子节点移动问题修复

This commit is contained in:
Hector 2025-08-14 10:06:26 +08:00
parent ee0fe2741d
commit 3261163ec8
7 changed files with 495 additions and 292 deletions

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,7 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta
CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib,
TransparencyAttrib)
from direct.task.TaskManagerGlobal import taskMgr
import math
class SelectionSystem:
@ -49,14 +50,14 @@ class SelectionSystem:
# 高亮相关
self.gizmoHighlightAxis = None
self.gizmo_colors = {
"x": (1, 0, 0, 1), # 红色
"y": (0, 1, 0, 1), # 绿色
"z": (0, 0, 1, 1) # 蓝色
"x": (1*10, 0, 0, 1), # 红色
"y": (0, 1*10, 0, 1), # 绿色
"z": (0, 0, 1*10, 1) # 蓝色
}
self.gizmo_highlight_colors = {
"x": (1.5, 1.5, 0, 1), # 黄色高亮
"y": (1.5, 1.5, 0, 1), # 黄色高亮
"z": (1.5, 1.5, 0, 1) # 黄色高亮
"x": (1.5*20, 1.5*20, 0, 1), # 黄色高亮
"y": (1.5*20, 1.5*20, 0, 1), # 黄色高亮
"z": (1.5*20, 1.5*20, 0, 1) # 黄色高亮
}
print("✓ 选择和变换系统初始化完成")
@ -595,7 +596,7 @@ class SelectionSystem:
print(f"❌ 最后备用方案也失败: {e}")
def updateGizmoTask(self, task):
"""坐标轴更新任务"""
"""坐标轴更新任务 - 包含固定大小功能"""
try:
if not self.gizmo or not self.gizmoTarget:
return task.done
@ -632,12 +633,58 @@ class SelectionSystem:
# 顶级模型:使用世界坐标系朝向
self.gizmo.setHpr(0, 0, 0)
return task.cont
# 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小
self._updateGizmoScreenSize()
return task.cont
except Exception as e:
print(f"坐标轴更新任务出错: {str(e)}")
return task.done
def _updateGizmoScreenSize(self):
"""动态调整坐标轴大小,保持固定的屏幕大小"""
try:
if not self.gizmo or not self.gizmoTarget:
return
# 计算相机到坐标轴的距离
gizmo_world_pos = self.gizmo.getPos(self.world.render)
cam_pos = self.world.cam.getPos()
distance_to_gizmo = (cam_pos - gizmo_world_pos).length()
# 获取相机视野角度和窗口尺寸
fov = self.world.cam.node().getLens().getFov()[0] # 水平视野角度
fov_radians = math.radians(fov)
winWidth, winHeight = self.world.getWindowSize()
# 计算一个像素在坐标轴距离处对应的世界坐标大小
pixel_to_world_ratio = distance_to_gizmo * math.tan(fov_radians / 2) / (winWidth / 2)
# 设定坐标轴在屏幕上的期望像素长度(固定值)
desired_screen_length = 120 # 像素
# 计算世界坐标系中的轴长度
world_axis_length = desired_screen_length * pixel_to_world_ratio
# 计算缩放比例(相对于基础轴长度)
scale_factor = world_axis_length / self.axis_length
# 应用缩放到坐标轴
self.gizmo.setScale(scale_factor)
# 限制缩放范围,避免过大或过小
min_scale = 0.1
max_scale = 10.0
final_scale = max(min_scale, min(max_scale, scale_factor))
if final_scale != scale_factor:
self.gizmo.setScale(final_scale)
except Exception as e:
# 静默处理错误,避免频繁输出
pass
def clearGizmo(self):
"""清除坐标轴"""
if self.gizmo:
@ -1247,8 +1294,38 @@ class SelectionSystem:
# 使用透视投影公式world_size = screen_size * distance * tan(fov/2) / (screen_width/2)
pixel_to_world_ratio = distance_to_object * math.tan(fov_radians / 2) / (winWidth / 2)
# 使用动态比例因子
scale_factor = pixel_to_world_ratio * 0.5 # 0.5是调整因子,可以根据需要调整
# 【改进修复】:智能缩放补偿,区分继承缩放和本体缩放
# 计算父节点链的累积缩放(不包括目标节点本身)
parent_cumulative_scale = 1.0
current_node = self.gizmoTarget.getParent()
while current_node and current_node != self.world.render:
node_scale = current_node.getScale()
# 使用缩放的几何平均值作为累积因子
scale_magnitude = (abs(node_scale.x) * abs(node_scale.y) * abs(node_scale.z)) ** (1.0/3.0)
parent_cumulative_scale *= scale_magnitude
current_node = current_node.getParent()
# 获取目标节点自身的缩放
target_scale = self.gizmoTarget.getScale()
target_scale_magnitude = (abs(target_scale.x) * abs(target_scale.y) * abs(target_scale.z)) ** (1.0/3.0)
# 智能补偿策略:
# 1. 只对父节点链的小缩放进行完全补偿(这通常是单位转换导致的)
# 2. 对目标节点自身的缩放进行部分补偿(避免大模型缩小后移动过快)
parent_compensation = 1.0 / parent_cumulative_scale if parent_cumulative_scale > 0 else 1.0
# 对目标节点自身的缩放使用平方根补偿,减少过度补偿
target_compensation = 1.0 / math.sqrt(target_scale_magnitude) if target_scale_magnitude > 0 else 1.0
# 限制目标补偿的最大值,避免移动过快
target_compensation = min(target_compensation, 10.0) # 最大10倍补偿
# 综合补偿因子
total_compensation = parent_compensation * target_compensation
scale_factor = pixel_to_world_ratio * 0.5 * total_compensation
print(f"智能缩放补偿: 父链缩放={parent_cumulative_scale:.4f}, 目标缩放={target_scale_magnitude:.4f}")
print(f"父链补偿={parent_compensation:.2f}, 目标补偿={target_compensation:.2f}, 总补偿={total_compensation:.2f}")
# 【关键修复】:在正确的坐标系中计算移动向量
# 计算移动距离(标量)
@ -1282,6 +1359,8 @@ class SelectionSystem:
newPos = self.gizmoTargetStartPos + movement
self.gizmoTarget.setPos(newPos)
self.world.property_panel.refreshModelValues(self.gizmoTarget)
# 每次拖拽都输出调试信息(但限制频率)
if not hasattr(self, '_last_drag_debug_time'):
self._last_drag_debug_time = 0

22
main.py
View File

@ -391,7 +391,27 @@ class MyWorld(CoreWorld):
# 模型导入和处理方法 - 代理到scene_manager
def importModel(self, filepath):
"""导入模型到场景"""
return self.scene_manager.importModel(filepath)
# 检查是否是FBX文件如果是则询问用户是否要转换为GLB
if filepath.lower().endswith('.fbx'):
try:
from PyQt5.QtWidgets import QMessageBox
reply = QMessageBox.question(
None,
'格式转换选择',
'FBX文件检测到\n\n是否要尝试转换为GLB格式以获得更好的动画支持\n\n点击""尝试转换为GLB格式需要安装转换工具\n点击""直接使用原始FBX格式导入',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
auto_convert = (reply == QMessageBox.Yes)
except ImportError:
# 如果没有PyQt5默认不转换
print("检测到FBX文件由于GUI不可用将直接使用原始格式导入")
auto_convert = False
else:
# 非FBX文件保持原有逻辑
auto_convert = True
return self.scene_manager.importModel(filepath, auto_convert_to_glb=auto_convert)
def importModelAsync(self, filepath):
"""异步导入模型"""

View File

@ -138,64 +138,64 @@ class SceneManager:
print(f"导入模型失败: {str(e)}")
return None
def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
"""导入带动画的FBX模型使用assimp
Args:
filepath: FBX文件路径
scale: 缩放比例默认0.01从厘米转换到米
auto_play: 是否自动播放第一个动画
Returns:
Actor对象如果加载失败返回None
"""
try:
print(f"\n=== 导入动画FBX模型: {filepath} ===")
filepath = util.normalize_model_path(filepath)
# 使用动画管理器加载FBX
actor = self.animation_manager.load_fbx_with_animations(filepath, scale)
if actor:
# 设置模型名称
model_name = os.path.basename(filepath)
actor.setName(model_name)
# 调整模型位置到地面
self._adjustModelToGround(actor)
# 设置碰撞检测
self.setupCollision(actor)
# 添加文件标签
actor.setTag("file", model_name)
actor.setTag("is_animated_model", "1")
# 添加到模型列表
self.models.append(actor)
# 自动播放第一个动画
if auto_play:
available_anims = self.animation_manager.get_available_animations(actor)
if available_anims:
first_anim = available_anims[0]
self.animation_manager.play_animation(actor, first_anim, loop=True)
print(f"🎬 自动播放动画: {first_anim}")
# 更新场景树
self.updateSceneTree()
print(f"=== 动画FBX模型导入成功: {model_name} ===\n")
return actor
else:
print("❌ 动画FBX模型导入失败")
return None
except Exception as e:
print(f"导入动画FBX模型失败: {str(e)}")
import traceback
traceback.print_exc()
return None
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
# """导入带动画的FBX模型使用assimp
#
# Args:
# filepath: FBX文件路径
# scale: 缩放比例默认0.01,从厘米转换到米)
# auto_play: 是否自动播放第一个动画
#
# Returns:
# Actor对象如果加载失败返回None
# """
# try:
# print(f"\n=== 导入动画FBX模型: {filepath} ===")
# filepath = util.normalize_model_path(filepath)
#
# # 使用动画管理器加载FBX
# actor = self.animation_manager.load_fbx_with_animations(filepath, scale)
#
# if actor:
# # 设置模型名称
# model_name = os.path.basename(filepath)
# actor.setName(model_name)
#
# # 调整模型位置到地面
# self._adjustModelToGround(actor)
#
# # 设置碰撞检测
# self.setupCollision(actor)
#
# # 添加文件标签
# actor.setTag("file", model_name)
# actor.setTag("is_animated_model", "1")
#
# # 添加到模型列表
# self.models.append(actor)
#
# # 自动播放第一个动画
# if auto_play:
# available_anims = self.animation_manager.get_available_animations(actor)
# if available_anims:
# first_anim = available_anims[0]
# self.animation_manager.play_animation(actor, first_anim, loop=True)
# print(f"🎬 自动播放动画: {first_anim}")
#
# # 更新场景树
# self.updateSceneTree()
#
# print(f"=== 动画FBX模型导入成功: {model_name} ===\n")
# return actor
# else:
# print("❌ 动画FBX模型导入失败")
# return None
#
# except Exception as e:
# print(f"导入动画FBX模型失败: {str(e)}")
# import traceback
# traceback.print_exc()
# return None
def _applyMaterialsToModel(self, model):
"""递归应用材质到模型的所有GeomNode"""
@ -1069,7 +1069,7 @@ class SceneManager:
if self._convertWithAssimp(filepath, glb_path, progress):
return glb_path
print(f"[GLB转换] 所有转换方法都失败")
#print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换")
return None
except Exception as e:

View File

@ -5,69 +5,71 @@ from panda3d.core import GeomNode, ModelRoot
class InterfaceManager:
"""界面管理器 - 处理树形控件和UI交互"""
def __init__(self, world):
"""初始化界面管理器"""
self.world = world
self.treeWidget = None
self._expanded_paths = set()
def setTreeWidget(self, treeWidget):
"""设置树形控件引用并更新场景树"""
self.treeWidget = treeWidget
# 添加右键菜单
self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu)
# 更新场景树
self.world.scene_manager.updateSceneTree()
def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件"""
if not item:
return
# 获取节点对象
nodePath = item.data(0, Qt.UserRole)
if nodePath:
# 更新选择状态
self.world.selected_np = nodePath
self.world.selection.updateSelection(nodePath)
# 更新属性面板
self.world.property_panel.updatePropertyPanel(item)
print(f"树形控件点击: {item.text(0)}")
else:
# 如果没有节点对象,清除选择
self.world.selection.updateSelection(None)
#self.world.property_panel.clearPropertyPanel()
def showTreeContextMenu(self, position):
"""显示树形控件的右键菜单"""
item = self.treeWidget.itemAt(position)
if not item:
return
# 获取节点对象
nodePath = item.data(0, Qt.UserRole)
if not nodePath:
return
# 创建菜单
menu = QMenu()
# 检查是否是GUI元素
if hasattr(nodePath, 'getTag') and nodePath.getTag("gui_type"):
# GUI元素菜单
editAction = menu.addAction("编辑")
editAction.triggered.connect(lambda: self.world.gui_manager.editGUIElementDialog(nodePath))
deleteAction = menu.addAction("删除GUI元素")
deleteAction.triggered.connect(lambda: self.world.gui_manager.deleteGUIElement(nodePath))
duplicateAction = menu.addAction("复制")
duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath))
else:
# 为模型节点或其子节点添加删除选项
parentItem = item.parent()
@ -75,10 +77,10 @@ class InterfaceManager:
if self.isModelOrChild(item):
deleteAction = menu.addAction("删除")
deleteAction.triggered.connect(lambda: self.deleteNode(nodePath, item))
# 显示菜单
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
def isModelOrChild(self, item):
"""检查是否是模型节点或其子节点"""
while item and item.parent():
@ -86,90 +88,104 @@ class InterfaceManager:
return True
item = item.parent()
return False
def deleteNode(self, nodePath, item):
"""删除节点"""
try:
# 从场景中移除
self.world.property_panel.removeActorForModel(nodePath)
nodePath.removeNode()
# 如果是模型根节点,从模型列表中移除
if item.parent().text(0) == "模型":
if nodePath in self.world.models:
self.world.models.remove(nodePath)
# 从树形控件中移除
parentItem = item.parent()
if parentItem:
parentItem.removeChild(item)
print(f"成功删除节点: {nodePath.getName()}")
# 清空属性面板和选择框
self.world.property_panel.clearPropertyPanel()
self.world.selection.updateSelection(None)
except Exception as e:
print(f"删除节点失败: {str(e)}")
def updateSceneTree(self):
"""更新场景树显示 - 实际实现"""
if not self.treeWidget:
return
self._expanded_paths.clear()
self._collect_expanded()
print("\n=== 更新场景树 ===")
self.treeWidget.clear()
# 创建场景根节点
sceneRoot = QTreeWidgetItem(self.treeWidget, ['场景'])
# 添加相机节点
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
cameraItem.setData(0, Qt.UserRole, self.world.cam)
print("添加相机节点")
# 添加模型节点组
modelsItem = QTreeWidgetItem(sceneRoot, ['模型'])
print(f"模型列表中的模型数量: {len(self.world.models)}")
# 添加GUI元素节点组
guiItem = QTreeWidgetItem(sceneRoot, ['GUI元素'])
print(f"GUI元素数量: {len(self.world.gui_elements)}")
lightItem = QTreeWidgetItem(sceneRoot,['灯光'])
# 递归添加节点及其子节点
def addNodeToTree(node, parentItem):
print(f"\n处理节点: {node.getName()}")
# 创建节点项
BLACK_LIST={'','**','temp','collision'}
def should_skip(node):
name = node.getName()
return name in BLACK_LIST or name.startswith('__')
def addNodeToTree(node,parentItem,force=False):
if not force and should_skip(node):
return
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
nodeItem.setData(0, Qt.UserRole, node)
print(f"添加节点: {node.getName()}")
# 递归处理所有子节点
nodeItem.setData(0,Qt.UserRole,node)
for child in node.getChildren():
# 检查是否是有效的模型节点
if (isinstance(child.node(), GeomNode) or
child.hasTag("file") or
child.getName() == "RootNode" or
isinstance(child.node(), ModelRoot)):
print(f"处理子节点: {child.getName()}")
addNodeToTree(child, nodeItem)
else:
print(f"跳过节点: {child.getName()}")
addNodeToTree(child,nodeItem,force=False)
# 递归添加节点及其子节点
# def addNodeToTree(node, parentItem):
# print(f"\n处理节点: {node.getName()}")
# # 创建节点项
# nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
# nodeItem.setData(0, Qt.UserRole, node)
# print(f"添加节点: {node.getName()}")
#
# # 递归处理所有子节点
# for child in node.getChildren():
# # 检查是否是有效的模型节点
# if (isinstance(child.node(), GeomNode) or
# child.hasTag("file") or
# child.getName() == "RootNode" or
# isinstance(child.node(), ModelRoot)):
# print(f"处理子节点: {child.getName()}")
# addNodeToTree(child, nodeItem)
# else:
# print(f"跳过节点: {child.getName()}")
# 添加所有模型及其子节点
for model in self.world.models:
print(f"\n处理根模型: {model.getName()}")
addNodeToTree(model, modelsItem)
addNodeToTree(model, modelsItem,force=True)
# 添加所有GUI元素
for gui_element in self.world.gui_elements:
gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown"
gui_text = gui_element.getTag("gui_text") if hasattr(gui_element, 'getTag') else "GUI元素"
display_name = f"{gui_type}: {gui_text}"
guiElementItem = QTreeWidgetItem(guiItem, [display_name])
guiElementItem.setData(0, Qt.UserRole, gui_element)
@ -179,17 +195,18 @@ class InterfaceManager:
addNodeToTree(light_element,lightItem)
for light_element in self.world.Pointlight:
addNodeToTree(light_element,lightItem)
# 添加地板节点
if hasattr(self.world, 'ground'):
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
groundItem.setData(0, Qt.UserRole, self.world.ground)
print("添加地板节点")
# 展开所有节点
self.treeWidget.expandAll()
#self.treeWidget.expandAll()
self._restore_expanded()
print("=== 场景树更新完成 ===\n")
def findTreeItem(self, node, parentItem):
"""在树形控件中查找指定的节点项"""
for i in range(parentItem.childCount()):
@ -202,4 +219,30 @@ class InterfaceManager:
found = self.findTreeItem(node, item)
if found:
return found
return None
return None
def _collect_expanded(self,item=None,prefix=""):
if item is None:
root = self.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
self._collect_expanded(root.child(i),prefix)
return
path = f"{prefix}/{item.text(0)}"
if item.isExpanded():
self._expanded_paths.add(path)
for i in range(item.childCount()):
self._collect_expanded(item.child(i),path)
def _restore_expanded(self,item=None,prefix=""):
if item is None:
root = self.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
self._restore_expanded(root.child(i),prefix)
return
path = f"{prefix}/{item.text(0)}"
if path in self._expanded_paths:
item.setExpanded(True)
for i in range(item.childCount()):
self._restore_expanded(item.child(i),path)

View File

@ -9,6 +9,7 @@ from PyQt5.QtCore import Qt
from deploy_libs.unicodedata import normalize
from direct.actor.Actor import Actor
from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup
from scene import util
class PropertyPanelManager:
@ -92,6 +93,63 @@ class PropertyPanelManager:
if propertyWidget:
propertyWidget.update()
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, '_xSpin') and self._xSpin:
self._xSpin.blockSignals(True)
self._xSpin.setValue(relPos.x)
self._xSpin.blockSignals(False)
if hasattr(self, '_ySpin') and self._ySpin:
self._ySpin.blockSignals(True)
self._ySpin.setValue(relPos.y)
self._ySpin.blockSignals(False)
if hasattr(self, '_zSpin') and self._zSpin:
self._zSpin.blockSignals(True)
self._zSpin.setValue(relPos.z)
self._zSpin.blockSignals(False)
# ---------------- 世界位置 ----------------
worldPos = nodePath.getPos(render)
for axis, attr in zip(('x', 'y', 'z'), ('_worldXSpin', '_worldYSpin', '_worldZSpin')):
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(('_hSpin', '_pSpin', '_rSpin'), 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'), ('_xScaleSpin', '_yScaleSpin', '_zScaleSpin')):
spin = getattr(self, attr, None)
if spin:
spin.blockSignals(True)
spin.setValue(getattr(scale, axis))
spin.blockSignals(False)
def _refreshWorldPos(self,model):
if not hasattr(self,'worldXSpin'):
return
world = model.getPos(self.world.render)
self._worldXSpin.setValue(world.x)
self._worldYSpin.setValue(world.y)
self._worldZSpin.setValue(world.z)
def _updateModelPropertyPanel(self, model):
"""更新模型属性面板"""
# 获取父节点
@ -100,84 +158,103 @@ class PropertyPanelManager:
# 位置属性(相对于父节点)
relativePos = model.getPos(parent) if parent else model.getPos()
xPos = QDoubleSpinBox()
xPos.setRange(-1000, 1000)
xPos.setValue(relativePos.getX())
xPos.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v))
self._propertyLayout.addRow("相对位置 X:", xPos)
self._xSpin = QDoubleSpinBox()
self._xSpin.setRange(-1000, 1000)
self._xSpin.setValue(relativePos.getX())
#self._xSpin.valueChanged.connect(lambda v: (model.setX(parent, v) if parent else model.setX(v),self._refreshWorldPos(model))[0])
self._xSpin.valueChanged.connect(
lambda v: (model.setX(parent, v) if parent else model.setX(v),
self.refreshModelValues(model))[0])
self._propertyLayout.addRow("相对位置 X:", self._xSpin)
yPos = QDoubleSpinBox()
yPos.setRange(-1000, 1000)
yPos.setValue(relativePos.getY())
yPos.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v))
self._propertyLayout.addRow("相对位置 Y:", yPos)
self._ySpin = QDoubleSpinBox()
self._ySpin.setRange(-1000, 1000)
self._ySpin.setValue(relativePos.getY())
#self._ySpin.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v))
self._ySpin.valueChanged.connect(
lambda v: (model.setY(parent, v) if parent else model.setY(v),
self.refreshModelValues(model))[0])
self._propertyLayout.addRow("相对位置 Y:", self._ySpin)
zPos = QDoubleSpinBox()
zPos.setRange(-1000, 1000)
zPos.setValue(relativePos.getZ())
zPos.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v))
self._propertyLayout.addRow("相对位置 Z:", zPos)
self._zSpin = QDoubleSpinBox()
self._zSpin.setRange(-1000, 1000)
self._zSpin.setValue(relativePos.getZ())
#self._zSpin.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v))
self._zSpin.valueChanged.connect(
lambda v:(model.setZ(parent,v) if parent else model.setZ(v),
self.refreshModelValues(model))[0])
self._propertyLayout.addRow("相对位置 Z:", self._zSpin)
# 世界位置(只读)
worldPos = model.getPos(self.world.render)
worldXPos = QDoubleSpinBox()
worldXPos.setRange(-1000, 1000)
worldXPos.setValue(worldPos.getX())
worldXPos.setReadOnly(True)
self._propertyLayout.addRow("世界位置 X:", worldXPos)
worldYPos = QDoubleSpinBox()
worldYPos.setRange(-1000, 1000)
worldYPos.setValue(worldPos.getY())
worldYPos.setReadOnly(True)
self._propertyLayout.addRow("世界位置 Y:", worldYPos)
self._worldXSpin = QDoubleSpinBox()
self._worldXSpin.setRange(-1000, 1000)
self._worldXSpin.setDecimals(2)
self._worldXSpin.setValue(worldPos.x)
self._worldXSpin.setReadOnly(True)
self._propertyLayout.addRow("世界位置 X:", self._worldXSpin)
worldZPos = QDoubleSpinBox()
worldZPos.setRange(-1000, 1000)
worldZPos.setValue(worldPos.getZ())
worldZPos.setReadOnly(True)
self._propertyLayout.addRow("世界位置 Z:", worldZPos)
self._worldYSpin = QDoubleSpinBox()
self._worldYSpin.setRange(-1000, 1000)
self._worldYSpin.setDecimals(2)
self._worldYSpin.setValue(worldPos.y)
self._worldYSpin.setReadOnly(True)
self._propertyLayout.addRow("世界位置 Y:", self._worldYSpin)
self._worldZSpin = QDoubleSpinBox()
self._worldZSpin.setRange(-1000, 1000)
self._worldZSpin.setDecimals(2)
self._worldZSpin.setValue(worldPos.z)
self._worldZSpin.setReadOnly(True)
self._propertyLayout.addRow("世界位置 Z:", self._worldZSpin)
# 旋转属性
hRot = QDoubleSpinBox()
hRot.setRange(-180, 180)
hRot.setValue(model.getH())
hRot.valueChanged.connect(lambda v: model.setH(v))
self._propertyLayout.addRow("旋转 H:", hRot)
self._hSpin = QDoubleSpinBox()
self._hSpin.setRange(-360, 360)
self._hSpin.setDecimals(2)
self._hSpin.setValue(model.getH())
self._hSpin.valueChanged.connect(lambda v: model.setH(v))
self._propertyLayout.addRow("旋转 H:", self._hSpin)
pRot = QDoubleSpinBox()
pRot.setRange(-180, 180)
pRot.setValue(model.getP())
pRot.valueChanged.connect(lambda v: model.setP(v))
self._propertyLayout.addRow("旋转 P:", pRot)
self._pSpin = QDoubleSpinBox()
self._pSpin.setRange(-360, 360)
self._pSpin.setDecimals(2)
self._pSpin.setValue(model.getP())
self._pSpin.valueChanged.connect(lambda v: model.setP(v))
self._propertyLayout.addRow("旋转 P:", self._pSpin)
rRot = QDoubleSpinBox()
rRot.setRange(-180, 180)
rRot.setValue(model.getR())
rRot.valueChanged.connect(lambda v: model.setR(v))
self._propertyLayout.addRow("旋转 R:", rRot)
self._rSpin = QDoubleSpinBox()
self._rSpin.setRange(-360, 360)
self._rSpin.setDecimals(2)
self._rSpin.setValue(model.getR())
self._rSpin.valueChanged.connect(lambda v: model.setR(v))
self._propertyLayout.addRow("旋转 R:", self._rSpin)
# 缩放属性
xScale = QDoubleSpinBox()
xScale.setRange(0.01, 100)
xScale.setSingleStep(0.1)
xScale.setValue(model.getScale().getX())
xScale.valueChanged.connect(lambda v: model.setScale(v, model.getScale().getY(), model.getScale().getZ()))
self._propertyLayout.addRow("缩放 X:", xScale)
self._xScaleSpin = QDoubleSpinBox()
self._xScaleSpin.setRange(0.001, 1000)
self._xScaleSpin.setSingleStep(0.1)
self._xScaleSpin.setDecimals(3)
self._xScaleSpin.setValue(model.getScale().getX())
self._xScaleSpin.valueChanged.connect(lambda v: model.setScale(v, model.getScale().getY(), model.getScale().getZ()))
self._propertyLayout.addRow("缩放 X:", self._xScaleSpin)
yScale = QDoubleSpinBox()
yScale.setRange(0.01, 100)
yScale.setSingleStep(0.1)
yScale.setValue(model.getScale().getY())
yScale.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), v, model.getScale().getZ()))
self._propertyLayout.addRow("缩放 Y:", yScale)
self.yScaleSpin = QDoubleSpinBox()
self.yScaleSpin.setRange(0.001, 1000)
self.yScaleSpin.setSingleStep(0.1)
self.yScaleSpin.setDecimals(3)
self.yScaleSpin.setValue(model.getScale().getY())
self.yScaleSpin.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), v, model.getScale().getZ()))
self._propertyLayout.addRow("缩放 Y:", self.yScaleSpin)
zScale = QDoubleSpinBox()
zScale.setRange(0.01, 100)
zScale.setSingleStep(0.1)
zScale.setValue(model.getScale().getZ())
zScale.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v))
self._propertyLayout.addRow("缩放 Z:", zScale)
self.zScaleSpin = QDoubleSpinBox()
self.zScaleSpin.setRange(0.001, 1000)
self.zScaleSpin.setSingleStep(0.1)
self.zScaleSpin.setDecimals(3)
self.zScaleSpin.setValue(model.getScale().getZ())
self.zScaleSpin.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v))
self._propertyLayout.addRow("缩放 Z:", self.zScaleSpin)
self._addAnimationPanel(model)
self._addSunAzimuthPanel()
@ -434,36 +511,6 @@ class PropertyPanelManager:
zScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'z', v))
self._propertyLayout.addRow("缩放 Z:", zScaleSpinBox)
# 获取父节点
#parent = model.getParent()
# 位置属性(相对于父节点)
#relativePos = model.getPos(parent) if parent else model.getPos()
# xPos = QDoubleSpinBox()
# xPos.setRange(-1000, 1000)
# xPos.setValue(relativePos.getX())
# xPos.valueChanged.connect(lambda v: model.setX(parent, v) if parent else model.setX(v))
# self._propertyLayout.addRow("相对位置 X:", xPos)
#print(f"{model} x :{model.getPos()}")
# yPos = QDoubleSpinBox()
# yPos.setRange(-1000, 1000)
# yPos.setValue(relativePos.getY())
# yPos.valueChanged.connect(lambda v: model.setY(parent, v) if parent else model.setY(v))
# self._propertyLayout.addRow("相对位置 Y:", yPos)
#
# zPos = QDoubleSpinBox()
# zPos.setRange(-1000, 1000)
# zPos.setValue(relativePos.getZ())
# zPos.valueChanged.connect(lambda v: model.setZ(parent, v) if parent else model.setZ(v))
# self._propertyLayout.addRow("相对位置 Z:", zPos)
# 世界位置(只读)
worldPos = model.getPos(self.world.render)
worldXPos = QDoubleSpinBox()
@ -484,49 +531,6 @@ class PropertyPanelManager:
worldZPos.setReadOnly(True)
self._propertyLayout.addRow("世界位置 Z:", worldZPos)
# 旋转属性
# hRot = QDoubleSpinBox()
# hRot.setRange(-180, 180)
# hRot.setValue(model.getH())
# hRot.valueChanged.connect(lambda v: model.setH(v))
# self._propertyLayout.addRow("旋转 H:", hRot)
#
# pRot = QDoubleSpinBox()
# pRot.setRange(-180, 180)
# pRot.setValue(model.getP())
# pRot.valueChanged.connect(lambda v: model.setP(v))
# self._propertyLayout.addRow("旋转 P:", pRot)
#
# rRot = QDoubleSpinBox()
# rRot.setRange(-180, 180)
# rRot.setValue(model.getR())
# rRot.valueChanged.connect(lambda v: model.setR(v))
# self._propertyLayout.addRow("旋转 R:", rRot)
# 缩放属性
# xScale = QDoubleSpinBox()
# xScale.setRange(0.01, 100)
# xScale.setSingleStep(0.1)
# xScale.setValue(model.getScale().getX())
# xScale.valueChanged.connect(lambda v: model.setScale(v, model.getScale().getY(), model.getScale().getZ()))
# self._propertyLayout.addRow("缩放 X:", xScale)
#
# yScale = QDoubleSpinBox()
# yScale.setRange(0.01, 100)
# yScale.setSingleStep(0.1)
# yScale.setValue(model.getScale().getY())
# yScale.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), v, model.getScale().getZ()))
# self._propertyLayout.addRow("缩放 Y:", yScale)
#
# zScale = QDoubleSpinBox()
# zScale.setRange(0.01, 100)
# zScale.setSingleStep(0.1)
# zScale.setValue(model.getScale().getZ())
# zScale.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v))
# self._propertyLayout.addRow("缩放 Z:", zScale)
def _updateLightPosition(self,light_object,node_path,axis,value):
current_pos = light_object.pos
@ -813,6 +817,7 @@ class PropertyPanelManager:
#漫反射贴图
diffuse_button = QPushButton("选择漫反射贴图")
diffuse_button.clicked.connect(lambda checked,title=unique_name:self._selectDiffuseTexture(title))
#diffuse_button.clicked.connect(lambda: self._selectDiffuseTexture())
self._propertyLayout.addRow("漫反射贴图:",diffuse_button)
#法线贴图
@ -1147,8 +1152,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyDiffuseTexture(material_title,filename)
print(f"已选择漫反射贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyDiffuseTexture(material_title,normalized_path)
print(f"已选择漫反射贴图:{filename} -> 标准化路径:{normalized_path}")
def _selectNormalTexture(self,material):
"""选择法线贴图"""
@ -1159,8 +1166,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyNormalTexture(material,filename)
print(f"已选择法线贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyNormalTexture(material,normalized_path)
print(f"已选择法线贴图:{filename} -> 标准化路径:{normalized_path}")
def _selectRoughnessTexture(self,material):
"""选择粗糙度贴图"""
@ -1171,8 +1180,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyRoughnessTexture_FINAL(material,filename)
print(f"已选择粗糙度贴图: {filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyRoughnessTexture_FINAL(material,normalized_path)
print(f"已选择粗糙度贴图: {filename} -> 标准化路径:{normalized_path}")
def _selectMetallicTexture(self,material):
"""选择金属性贴图"""
@ -1183,8 +1194,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyMetallicTexture_NEW(material,filename)
print(f"已选择金属性贴图: {filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyMetallicTexture_NEW(material,normalized_path)
print(f"已选择金属性贴图: {filename} -> 标准化路径:{normalized_path}")
#IOR贴图
def _selectIORTexture(self,material):
@ -1196,8 +1209,10 @@ class PropertyPanelManager:
if file_dialong.exec_():
filename = file_dialong.selectedFiles()[0]
if filename:
self._applyIORTexture(material,filename)
print(f"已选择IOR贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyIORTexture(material,normalized_path)
print(f"已选择IOR贴图:{filename} -> 标准化路径:{normalized_path}")
def _selectParallaxTexture(self,material):
"""选择视差贴图"""
@ -1208,8 +1223,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyParallaxTexture(material,filename)
print(f"已选择视差贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyParallaxTexture(material,normalized_path)
print(f"已选择视差贴图:{filename} -> 标准化路径:{normalized_path}")
def _selectEmissionTexture(self,material):
"""选择自发光贴图"""
@ -1220,8 +1237,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyEmissionTexture(material,filename)
print(f"已选择自发光贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyEmissionTexture(material,normalized_path)
print(f"已选择自发光贴图:{filename} -> 标准化路径:{normalized_path}")
def _selectAOTexture(self,material):
"""选择环境光遮蔽贴图"""
@ -1232,8 +1251,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyAOTexture(material,filename)
print(f"已选择AO贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyAOTexture(material,normalized_path)
print(f"已选择AO贴图:{filename} -> 标准化路径:{normalized_path}")
def _selectAlphaTexture(self,material):
"""选择透明度贴图"""
@ -1244,8 +1265,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyAlphaTexture(material,filename)
print(f"已选择透明度贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyAlphaTexture(material,normalized_path)
print(f"已选择透明度贴图:{filename} -> 标准化路径:{normalized_path}")
def _selectDetailTexture(self,material):
"""选择细节贴图"""
@ -1256,8 +1279,10 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyDetailTexture(material,filename)
print(f"已选择细节贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyDetailTexture(material,normalized_path)
print(f"已选择细节贴图:{filename} -> 标准化路径:{normalized_path}")
def _selectGlossTexture(self,material):
"""选择光泽贴图"""
@ -1268,9 +1293,43 @@ class PropertyPanelManager:
if file_dialog.exec_():
filename = file_dialog.selectedFiles()[0]
if filename:
self._applyGlossTexture(material,filename)
print(f"已选择光泽贴图:{filename}")
# 使用跨平台路径标准化
normalized_path = util.normalize_model_path(filename)
self._applyGlossTexture(material,normalized_path)
print(f"已选择光泽贴图:{filename} -> 标准化路径:{normalized_path}")
# def _applyDiffuseTexture(self, texture_path):
# from panda3d.core import TextureStage
# try:
# from RenderPipelineFile.rpcore.loader import RPLoader
# texture = RPLoader.load_texture(texture_path)
# if not texture:
# print("纹理加载失败")
# return
#
# node = self.world.selected_np
# if not node:
# print("未选中节点")
# return
#
# # 1. 直接给节点挂贴图
# diffuse_stage = TextureStage("diffuse")
# diffuse_stage.setSort(0)
# node.setTexture(diffuse_stage, texture)
#
# # 2. 再给它刷一个 RenderPipeline 效果
# effect_file = "effects/default.yaml"
# self.world.render_pipeline.set_effect(
# node,
# effect_file,
# {"diffuse_texture": texture},
# 100
# )
# print("贴图已直接贴到节点:", node.getName())
# except Exception as e:
# print("贴图失败:", e)
def _applyDiffuseTexture(self,material_title,texture_path):
"""应用漫反射贴图"""
try:
@ -3959,7 +4018,9 @@ class PropertyPanelManager:
self.speed_spinbox = QDoubleSpinBox()
self.speed_spinbox.setRange(0.1,5.0)
self.speed_spinbox.setSingleStep(0.1)
self.speed_spinbox.setValue(1.0)
saved = origin_model.getPythonTag("anim_speed")
self.speed_spinbox.setValue(saved if saved is not None else 1.0)
#self.speed_spinbox.setValue(1.0)
self.speed_spinbox.valueChanged.connect(lambda v:self._setAnimationSpeed(origin_model,v))
self._propertyLayout.addRow("播放速度:",self.speed_spinbox)
@ -4541,7 +4602,6 @@ except Exception as e:
actor = self._getActor(origin_model)
if not actor:
return
# 获取原始动画名称
current_index = self.animation_combo.currentIndex()
@ -4552,6 +4612,7 @@ except Exception as e:
if anim_name:
actor.setPlayRate(speed, anim_name)
origin_model.setPythonTag("anim_speed",speed)
print(f"[动画] 速度设为: {speed} ({display_name})")
def _detectNonSkeletalAnimations(self, origin_model):

View File

@ -407,11 +407,11 @@ class CustomTreeWidget(QTreeWidget):
currentItem = self.currentItem()
if currentItem and currentItem.parent():
# 检查是否是模型节点或其子节点
if self.world.isModelOrChild(currentItem):
if self.world.interface_manager.isModelOrChild(currentItem):
nodePath = currentItem.data(0, Qt.UserRole)
if nodePath:
print("正在删除节点...")
self.world.deleteNode(nodePath, currentItem)
self.world.interface_manager.deleteNode(nodePath, currentItem)
print("删除完成")
else:
super().keyPressEvent(event)