2193 lines
87 KiB
Python
2193 lines
87 KiB
Python
import os
|
||
from pathlib import Path
|
||
|
||
from imgui_bundle import imgui, imgui_ctx
|
||
|
||
|
||
class PropertyHelpers:
|
||
"""Property, collision, material, and picker helper methods."""
|
||
|
||
def __init__(self, app):
|
||
self.app = app
|
||
|
||
def __getattr__(self, name):
|
||
return getattr(self.app, name)
|
||
|
||
def __setattr__(self, name, value):
|
||
if name == "app" or name in self.__dict__ or hasattr(type(self), name):
|
||
object.__setattr__(self, name, value)
|
||
else:
|
||
setattr(self.app, name, value)
|
||
|
||
|
||
def _apply_gui_font(self, gui_element, font_path):
|
||
"""应用GUI元素的字体"""
|
||
try:
|
||
if hasattr(gui_element, 'setFont') and font_path:
|
||
gui_element.setFont(font_path)
|
||
gui_element.font_path = font_path
|
||
except Exception as e:
|
||
print(f"应用GUI字体失败: {e}")
|
||
|
||
|
||
def _apply_gui_font_size(self, gui_element, font_size):
|
||
"""应用GUI元素的字体大小"""
|
||
try:
|
||
if hasattr(gui_element, 'setFontSize'):
|
||
gui_element.setFontSize(font_size)
|
||
gui_element.font_size = font_size
|
||
except Exception as e:
|
||
print(f"应用GUI字体大小失败: {e}")
|
||
|
||
|
||
def _apply_gui_font_style(self, gui_element):
|
||
"""应用GUI元素的字体样式"""
|
||
try:
|
||
if hasattr(gui_element, 'setFontStyle'):
|
||
style = 0
|
||
if getattr(gui_element, 'font_bold', False):
|
||
style |= 1 # 粗体
|
||
if getattr(gui_element, 'font_italic', False):
|
||
style |= 2 # 斜体
|
||
gui_element.setFontStyle(style)
|
||
except Exception as e:
|
||
print(f"应用GUI字体样式失败: {e}")
|
||
|
||
# 特定类型的属性
|
||
if gui_type == "button":
|
||
if imgui.collapsing_header("按钮属性"):
|
||
# 按钮状态
|
||
is_pressed = getattr(gui_element, 'pressed', False)
|
||
changed, new_pressed = imgui.checkbox("按下状态", is_pressed)
|
||
if changed:
|
||
gui_element.pressed = new_pressed
|
||
|
||
# 按钮回调
|
||
callback_name = getattr(gui_element, 'callback_name', '')
|
||
changed, new_callback = imgui.input_text("回调函数", callback_name, 64)
|
||
if changed:
|
||
gui_element.callback_name = new_callback
|
||
|
||
elif gui_type == "entry":
|
||
if imgui.collapsing_header("输入框属性"):
|
||
# 输入框内容
|
||
entry_text = getattr(gui_element, 'entry_text', '')
|
||
changed, new_text = imgui.input_text("输入内容", entry_text, 256)
|
||
if changed:
|
||
gui_element.entry_text = new_text
|
||
if hasattr(gui_element, 'set'):
|
||
gui_element.set(new_text)
|
||
|
||
# 最大长度
|
||
max_length = getattr(gui_element, 'max_length', 256)
|
||
changed, new_max = imgui.input_int("最大长度", max_length)
|
||
if changed:
|
||
gui_element.max_length = max(max_length, 1)
|
||
|
||
# 密码模式
|
||
is_password = getattr(gui_element, 'is_password', False)
|
||
changed, new_password = imgui.checkbox("密码模式", is_password)
|
||
if changed:
|
||
gui_element.is_password = new_password
|
||
if hasattr(gui_element, 'obscure'):
|
||
gui_element.obscure(new_password)
|
||
|
||
elif gui_type in ["2d_image", "3d_image"]:
|
||
if imgui.collapsing_header("图像属性"):
|
||
# 图像路径
|
||
image_path = getattr(gui_element, 'image_path', '')
|
||
changed, new_path = imgui.input_text("图像路径", image_path, 256)
|
||
if changed and hasattr(self, 'gui_manager'):
|
||
gui_element.image_path = new_path
|
||
# TODO: 重新加载图像
|
||
|
||
# 图像缩放模式
|
||
scale_mode = getattr(gui_element, 'scale_mode', 'stretch')
|
||
if imgui.begin_combo("缩放模式", scale_mode):
|
||
if imgui.selectable("拉伸##stretch"):
|
||
gui_element.scale_mode = 'stretch'
|
||
if imgui.selectable("适应##fit"):
|
||
gui_element.scale_mode = 'fit'
|
||
if imgui.selectable("填充##fill"):
|
||
gui_element.scale_mode = 'fill'
|
||
imgui.end_combo()
|
||
|
||
|
||
def _has_collision(self, node):
|
||
"""检查节点是否有碰撞体"""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return False
|
||
|
||
# 检查是否有碰撞节点
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
return True
|
||
|
||
return False
|
||
except Exception as e:
|
||
print(f"检查碰撞状态失败: {e}")
|
||
return False
|
||
|
||
|
||
def _get_current_collision_shape(self, node):
|
||
"""获取当前碰撞形状"""
|
||
try:
|
||
if not self._has_collision(node):
|
||
return "球形 (Sphere)"
|
||
|
||
# 查找碰撞节点并判断形状
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
|
||
from panda3d.core import CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane
|
||
solid = child.node().getSolid(0)
|
||
if isinstance(solid, CollisionSphere):
|
||
return "球形 (Sphere)"
|
||
elif isinstance(solid, CollisionBox):
|
||
return "盒型 (Box)"
|
||
elif isinstance(solid, CollisionCapsule):
|
||
return "胶囊体 (Capsule)"
|
||
elif isinstance(solid, CollisionPlane):
|
||
return "平面 (Plane)"
|
||
|
||
# 兜底根据名称判断形状
|
||
if 'sphere' in name.lower():
|
||
return "球形 (Sphere)"
|
||
elif 'box' in name.lower():
|
||
return "盒型 (Box)"
|
||
elif 'capsule' in name.lower():
|
||
return "胶囊体 (Capsule)"
|
||
elif 'plane' in name.lower():
|
||
return "平面 (Plane)"
|
||
|
||
return "球形 (Sphere)" # 默认
|
||
except Exception as e:
|
||
print(f"获取碰撞形状失败: {e}")
|
||
return "球形 (Sphere)"
|
||
|
||
|
||
def _get_current_collision_shape_type(self, node):
|
||
"""获取当前碰撞形状类型(内部标识)"""
|
||
try:
|
||
shape_name = self._get_current_collision_shape(node)
|
||
if "Sphere" in shape_name:
|
||
return "sphere"
|
||
elif "Box" in shape_name:
|
||
return "box"
|
||
elif "Capsule" in shape_name:
|
||
return "capsule"
|
||
elif "Plane" in shape_name:
|
||
return "plane"
|
||
else:
|
||
return "sphere"
|
||
except Exception as e:
|
||
print(f"获取碰撞形状类型失败: {e}")
|
||
return "sphere"
|
||
|
||
|
||
def _get_collision_position_offset(self, node):
|
||
"""获取碰撞体位置偏移"""
|
||
try:
|
||
if not self._has_collision(node):
|
||
return (0.0, 0.0, 0.0)
|
||
|
||
# 查找碰撞节点并获取位置
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
pos = child.getPos()
|
||
return (pos.x, pos.y, pos.z)
|
||
|
||
return (0.0, 0.0, 0.0)
|
||
except Exception as e:
|
||
print(f"获取碰撞位置失败: {e}")
|
||
return (0.0, 0.0, 0.0)
|
||
|
||
|
||
def _is_collision_visible(self, node):
|
||
"""检查碰撞体是否可见"""
|
||
try:
|
||
if not self._has_collision(node):
|
||
return False
|
||
|
||
# 查找碰撞节点并检查可见性
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
return child.isHidden() == False
|
||
|
||
return False
|
||
except Exception as e:
|
||
print(f"检查碰撞可见性失败: {e}")
|
||
return False
|
||
|
||
|
||
def _add_collision_to_node(self, node):
|
||
"""为节点添加碰撞体"""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
print("无效的节点")
|
||
return
|
||
|
||
if self._has_collision(node):
|
||
print("节点已有碰撞体")
|
||
return
|
||
|
||
# 获取选择的形状
|
||
shape_name = getattr(self.app, '_selected_collision_shape', '球形 (Sphere)')
|
||
|
||
if hasattr(self, 'collision_manager'):
|
||
# 使用碰撞管理器添加碰撞体
|
||
shape_type = self._get_shape_type_from_name(shape_name)
|
||
collision_node = self.collision_manager.setupAdvancedCollision(
|
||
node,
|
||
shape_type=shape_type,
|
||
mask_type='MODEL_COLLISION'
|
||
)
|
||
|
||
if collision_node:
|
||
print(f"成功为节点 {node.getName()} 添加 {shape_name} 碰撞体")
|
||
else:
|
||
print(f"添加碰撞体失败")
|
||
else:
|
||
print("碰撞管理器未初始化")
|
||
|
||
except Exception as e:
|
||
print(f"添加碰撞体失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
|
||
def _remove_collision_from_node(self, node):
|
||
"""从节点移除碰撞体"""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
print("无效的节点")
|
||
return
|
||
|
||
if not self._has_collision(node):
|
||
print("节点没有碰撞体")
|
||
return
|
||
|
||
# 查找并移除碰撞节点
|
||
children_to_remove = []
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
children_to_remove.append(child)
|
||
|
||
# 移除找到的碰撞节点
|
||
for child in children_to_remove:
|
||
child.removeNode()
|
||
|
||
if children_to_remove:
|
||
print(f"成功移除节点 {node.getName()} 的碰撞体")
|
||
else:
|
||
print(f"未找到碰撞体")
|
||
|
||
except Exception as e:
|
||
print(f"移除碰撞体失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
|
||
def _toggle_collision_visibility(self, node):
|
||
"""切换碰撞体可见性"""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return
|
||
|
||
# 查找碰撞节点并切换可见性
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
if child.isHidden():
|
||
child.show()
|
||
else:
|
||
child.hide()
|
||
break
|
||
|
||
except Exception as e:
|
||
print(f"切换碰撞可见性失败: {e}")
|
||
|
||
|
||
def _update_collision_position(self, node, axis, value):
|
||
"""更新碰撞体位置"""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return
|
||
|
||
# 查找碰撞节点并更新位置
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
current_pos = child.getPos()
|
||
if axis == 'x':
|
||
child.setPos(value, current_pos.y, current_pos.z)
|
||
elif axis == 'y':
|
||
child.setPos(current_pos.x, value, current_pos.z)
|
||
elif axis == 'z':
|
||
child.setPos(current_pos.x, current_pos.y, value)
|
||
break
|
||
|
||
except Exception as e:
|
||
print(f"更新碰撞位置失败: {e}")
|
||
|
||
|
||
def _get_shape_type_from_name(self, shape_name):
|
||
"""从形状名称获取形状类型"""
|
||
if "Sphere" in shape_name:
|
||
return "sphere"
|
||
elif "Box" in shape_name:
|
||
return "box"
|
||
elif "Capsule" in shape_name:
|
||
return "capsule"
|
||
elif "Plane" in shape_name:
|
||
return "plane"
|
||
else:
|
||
return "sphere"
|
||
|
||
|
||
def _get_sphere_radius(self, node):
|
||
"""获取球形半径"""
|
||
try:
|
||
# 从碰撞节点获取半径信息
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
|
||
solid = child.node().getSolid(0)
|
||
from panda3d.core import CollisionSphere
|
||
if isinstance(solid, CollisionSphere):
|
||
return solid.getRadius()
|
||
return 1.0
|
||
except Exception as e:
|
||
print(f"获取球形半径失败: {e}")
|
||
return 1.0
|
||
|
||
|
||
def _update_sphere_radius(self, node, radius):
|
||
"""更新球形半径"""
|
||
try:
|
||
# 重新创建碰撞体来更新参数
|
||
if hasattr(self, 'collision_manager'):
|
||
# 先移除旧的碰撞体
|
||
self._remove_collision_from_node(node)
|
||
# 重新创建带有新参数的碰撞体
|
||
self.collision_manager.setupAdvancedCollision(
|
||
node,
|
||
shape_type='sphere',
|
||
mask_type='MODEL_COLLISION',
|
||
radius=radius
|
||
)
|
||
print(f"更新球形半径为: {radius}")
|
||
except Exception as e:
|
||
print(f"更新球形半径失败: {e}")
|
||
|
||
|
||
def _get_box_size(self, node):
|
||
"""获取盒型尺寸"""
|
||
try:
|
||
# 从碰撞节点获取尺寸信息
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
# 尝试从碰撞体获取尺寸
|
||
if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
|
||
solid = child.node().getSolid(0)
|
||
from panda3d.core import CollisionBox
|
||
if isinstance(solid, CollisionBox):
|
||
min_p = solid.getMin()
|
||
max_p = solid.getMax()
|
||
return (
|
||
max_p.x - min_p.x,
|
||
max_p.y - min_p.y,
|
||
max_p.z - min_p.z
|
||
)
|
||
return (1.0, 1.0, 1.0)
|
||
except Exception as e:
|
||
print(f"获取盒型尺寸失败: {e}")
|
||
return (1.0, 1.0, 1.0)
|
||
|
||
|
||
def _update_box_size(self, node, axis, value):
|
||
"""更新盒型尺寸"""
|
||
try:
|
||
# 获取当前尺寸
|
||
current_size = self._get_box_size(node)
|
||
new_size = list(current_size)
|
||
|
||
# 更新指定轴的尺寸
|
||
if axis == 'x':
|
||
new_size[0] = value
|
||
elif axis == 'y':
|
||
new_size[1] = value
|
||
elif axis == 'z':
|
||
new_size[2] = value
|
||
|
||
# 重新创建碰撞体
|
||
if hasattr(self, 'collision_manager'):
|
||
self._remove_collision_from_node(node)
|
||
self.collision_manager.setupAdvancedCollision(
|
||
node,
|
||
shape_type='box',
|
||
mask_type='MODEL_COLLISION',
|
||
width=new_size[0],
|
||
length=new_size[1],
|
||
height=new_size[2]
|
||
)
|
||
print(f"更新盒型尺寸: {new_size}")
|
||
except Exception as e:
|
||
print(f"更新盒型尺寸失败: {e}")
|
||
|
||
|
||
def _get_capsule_radius(self, node):
|
||
"""获取胶囊体半径"""
|
||
try:
|
||
# 从碰撞节点获取半径信息
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
|
||
solid = child.node().getSolid(0)
|
||
from panda3d.core import CollisionCapsule
|
||
if isinstance(solid, CollisionCapsule):
|
||
return solid.getRadius()
|
||
return 1.0
|
||
except Exception as e:
|
||
print(f"获取胶囊体半径失败: {e}")
|
||
return 1.0
|
||
|
||
|
||
def _update_capsule_radius(self, node, radius):
|
||
"""更新胶囊体半径"""
|
||
try:
|
||
# 获取当前高度
|
||
height = self._get_capsule_height(node)
|
||
|
||
# 重新创建碰撞体
|
||
if hasattr(self, 'collision_manager'):
|
||
self._remove_collision_from_node(node)
|
||
self.collision_manager.setupAdvancedCollision(
|
||
node,
|
||
shape_type='capsule',
|
||
mask_type='MODEL_COLLISION',
|
||
radius=radius,
|
||
height=height
|
||
)
|
||
print(f"更新胶囊体半径为: {radius}")
|
||
except Exception as e:
|
||
print(f"更新胶囊体半径失败: {e}")
|
||
|
||
|
||
def _get_capsule_height(self, node):
|
||
"""获取胶囊体高度"""
|
||
try:
|
||
# 从碰撞节点获取高度信息
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
|
||
solid = child.node().getSolid(0)
|
||
from panda3d.core import CollisionCapsule
|
||
if isinstance(solid, CollisionCapsule):
|
||
point_a = solid.getPointA()
|
||
point_b = solid.getPointB()
|
||
return (point_b - point_a).length() + 2 * solid.getRadius()
|
||
return 2.0
|
||
except Exception as e:
|
||
print(f"获取胶囊体高度失败: {e}")
|
||
return 2.0
|
||
|
||
|
||
def _update_capsule_height(self, node, height):
|
||
"""更新胶囊体高度"""
|
||
try:
|
||
# 获取当前半径
|
||
radius = self._get_capsule_radius(node)
|
||
|
||
# 重新创建碰撞体
|
||
if hasattr(self, 'collision_manager'):
|
||
self._remove_collision_from_node(node)
|
||
self.collision_manager.setupAdvancedCollision(
|
||
node,
|
||
shape_type='capsule',
|
||
mask_type='MODEL_COLLISION',
|
||
radius=radius,
|
||
height=height
|
||
)
|
||
print(f"更新胶囊体高度为: {height}")
|
||
except Exception as e:
|
||
print(f"更新胶囊体高度失败: {e}")
|
||
|
||
|
||
def _get_plane_normal(self, node):
|
||
"""获取平面法向量"""
|
||
try:
|
||
# 从碰撞节点获取法向量信息
|
||
for child in node.getChildren():
|
||
if hasattr(child, 'getName') and child.getName():
|
||
name = child.getName()
|
||
if 'collision' in name.lower() or 'Collision' in name:
|
||
if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
|
||
solid = child.node().getSolid(0)
|
||
from panda3d.core import CollisionPlane
|
||
if isinstance(solid, CollisionPlane):
|
||
plane = solid.getPlane()
|
||
normal = plane.getNormal()
|
||
return (normal.x, normal.y, normal.z)
|
||
return (0.0, 0.0, 1.0)
|
||
except Exception as e:
|
||
print(f"获取平面法向量失败: {e}")
|
||
return (0.0, 0.0, 1.0)
|
||
|
||
|
||
def _update_plane_normal(self, node, axis, value):
|
||
"""更新平面法向量"""
|
||
try:
|
||
# 获取当前法向量
|
||
current_normal = self._get_plane_normal(node)
|
||
new_normal = list(current_normal)
|
||
|
||
# 更新指定轴的值
|
||
if axis == 'x':
|
||
new_normal[0] = value
|
||
elif axis == 'y':
|
||
new_normal[1] = value
|
||
elif axis == 'z':
|
||
new_normal[2] = value
|
||
|
||
# 标准化法向量
|
||
from panda3d.core import Vec3
|
||
normal_vec = Vec3(*new_normal)
|
||
normal_vec.normalize()
|
||
|
||
# 重新创建碰撞体
|
||
if hasattr(self, 'collision_manager'):
|
||
self._remove_collision_from_node(node)
|
||
self.collision_manager.setupAdvancedCollision(
|
||
node,
|
||
shape_type='plane',
|
||
mask_type='MODEL_COLLISION',
|
||
normal=normal_vec
|
||
)
|
||
print(f"更新平面法向量为: ({normal_vec.x:.2f}, {normal_vec.y:.2f}, {normal_vec.z:.2f})")
|
||
except Exception as e:
|
||
print(f"更新平面法向量失败: {e}")
|
||
|
||
|
||
def _manual_collision_detection(self):
|
||
"""手动执行碰撞检测"""
|
||
try:
|
||
if hasattr(self, 'collision_manager'):
|
||
results = self.collision_manager.detectModelCollisions(log_results=True)
|
||
if results:
|
||
print(f"手动碰撞检测完成,发现 {len(results)} 个碰撞")
|
||
else:
|
||
print("手动碰撞检测完成,未发现碰撞")
|
||
except Exception as e:
|
||
print(f"手动碰撞检测失败: {e}")
|
||
|
||
def _update_node_name(self, node, new_name):
|
||
"""更新节点名称"""
|
||
if new_name and new_name != node.getName():
|
||
node.setName(new_name)
|
||
# 更新场景树显示
|
||
if hasattr(self, 'scene_tree'):
|
||
self.scene_tree.refresh()
|
||
|
||
|
||
def _get_material_base_color(self, material):
|
||
"""获取材质基础颜色"""
|
||
try:
|
||
if hasattr(material, 'base_color') and material.base_color is not None:
|
||
return (material.base_color.x, material.base_color.y, material.base_color.z, material.base_color.w)
|
||
elif hasattr(material, 'get_base_color'):
|
||
color = material.get_base_color()
|
||
return (color.x, color.y, color.z, color.w)
|
||
elif hasattr(material, 'getDiffuse'):
|
||
color = material.getDiffuse()
|
||
return (color.x, color.y, color.z, color.w if hasattr(color, 'w') else 1.0)
|
||
else:
|
||
return (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||
except:
|
||
return (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||
|
||
def _apply_material_to_geom_states(self, node, material):
|
||
"""Bake the editable material into every GeomState so RP can see runtime edits."""
|
||
try:
|
||
from panda3d.core import MaterialAttrib
|
||
|
||
if not node or node.isEmpty() or material is None:
|
||
return
|
||
|
||
# For imported multi-material models we edit the existing material
|
||
# instances in place. Rebroadcasting one material to every GeomNode
|
||
# would collapse the whole model to a single material.
|
||
try:
|
||
if not node.hasMaterial():
|
||
self._invalidate_material_render_cache()
|
||
return
|
||
except Exception:
|
||
pass
|
||
|
||
for geom_path in node.find_all_matches("**/+GeomNode"):
|
||
try:
|
||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(material)))
|
||
except Exception:
|
||
pass
|
||
|
||
geom_node = geom_path.node()
|
||
for i in range(geom_node.getNumGeoms()):
|
||
try:
|
||
geom_state = geom_node.getGeomState(i)
|
||
geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(material)))
|
||
except Exception:
|
||
pass
|
||
self._invalidate_material_render_cache()
|
||
except Exception as e:
|
||
print(f"同步Geom材质状态失败: {e}")
|
||
|
||
def _get_node_materials(self, node):
|
||
"""Return the editable materials currently used by a node."""
|
||
if not node or node.isEmpty():
|
||
return []
|
||
|
||
try:
|
||
if node.hasMaterial():
|
||
return [node.getMaterial()]
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
materials = list(node.find_all_materials())
|
||
except Exception:
|
||
materials = []
|
||
|
||
unique_materials = []
|
||
seen_keys = set()
|
||
for material in materials:
|
||
key = getattr(material, "this", None) or id(material)
|
||
if key in seen_keys:
|
||
continue
|
||
seen_keys.add(key)
|
||
unique_materials.append(material)
|
||
return unique_materials
|
||
|
||
def _invalidate_material_render_cache(self):
|
||
"""Force Panda/RenderPipeline to pick up runtime material edits immediately."""
|
||
try:
|
||
from panda3d.core import RenderState
|
||
RenderState.clear_cache()
|
||
except Exception:
|
||
pass
|
||
|
||
def _ensure_material_for_node(self, node):
|
||
"""Ensure a node has at least one editable material and return the primary material."""
|
||
try:
|
||
if node and not node.isEmpty() and node.hasMaterial():
|
||
material = node.getMaterial()
|
||
self._apply_material_to_geom_states(node, material)
|
||
return material
|
||
except Exception:
|
||
pass
|
||
|
||
materials = self._get_node_materials(node)
|
||
if materials:
|
||
return materials[0]
|
||
|
||
try:
|
||
from panda3d.core import Material, Vec4
|
||
|
||
material = Material(f"default-material-{node.getName() or 'node'}")
|
||
material.set_base_color(Vec4(0.8, 0.8, 0.8, 1.0))
|
||
material.set_roughness(0.5)
|
||
material.set_metallic(0.0)
|
||
material.set_refractive_index(1.5)
|
||
material.set_emission(Vec4(0.0, 0.0, 1.0, 0.0))
|
||
node.setMaterial(material, 1)
|
||
self._apply_material_to_geom_states(node, material)
|
||
return material
|
||
except Exception as e:
|
||
print(f"创建默认材质失败: {e}")
|
||
return None
|
||
|
||
def _get_material_surface_type(self, material):
|
||
"""Return the RenderPipeline shading model value used by this material."""
|
||
try:
|
||
emission = material.emission if hasattr(material, "emission") else None
|
||
if emission is None:
|
||
return 0
|
||
shading_model = int(round(float(emission.x)))
|
||
if shading_model in (0, 1, 3):
|
||
return shading_model
|
||
except Exception:
|
||
pass
|
||
return 0
|
||
|
||
def _set_material_surface_type(self, node, material, surface_type, refresh_pipeline=True):
|
||
"""Update material shading model and sync node transparency state."""
|
||
try:
|
||
from panda3d.core import Vec4
|
||
|
||
surface_type = int(surface_type)
|
||
previous_surface_type = self._get_material_surface_type(material)
|
||
emission = material.emission if hasattr(material, "emission") and material.emission is not None else Vec4(0, 0, 1, 0)
|
||
if surface_type == 3:
|
||
if previous_surface_type == 3:
|
||
opacity = self._get_material_opacity(material)
|
||
else:
|
||
base_alpha = float(self._get_material_base_color(material)[3])
|
||
opacity = base_alpha if 0.0 < base_alpha <= 1.0 else 1.0
|
||
else:
|
||
opacity = 1.0
|
||
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w)))
|
||
self._apply_material_to_geom_states(node, material)
|
||
self._apply_material_surface_state(node, material)
|
||
if refresh_pipeline:
|
||
self._refresh_pipeline_material_mode(node, material)
|
||
except Exception as e:
|
||
print(f"设置材质表面类型失败: {e}")
|
||
|
||
def _get_material_opacity(self, material):
|
||
"""Return opacity for transparent materials."""
|
||
try:
|
||
if self._get_material_surface_type(material) != 3:
|
||
return 1.0
|
||
emission = material.emission if hasattr(material, "emission") else None
|
||
if emission is not None:
|
||
return max(0.0, min(1.0, float(emission.z)))
|
||
except Exception:
|
||
pass
|
||
return 1.0
|
||
|
||
def _material_uses_transparent_pass(self, material):
|
||
"""Return whether the material should be rendered through RP forward transparency."""
|
||
try:
|
||
return self._get_material_surface_type(material) == 3
|
||
except Exception:
|
||
return False
|
||
|
||
def _refresh_pipeline_material_mode(self, node, material):
|
||
"""Let RenderPipeline re-evaluate transparent material routing for this subtree."""
|
||
try:
|
||
render_pipeline = getattr(self, "render_pipeline", None)
|
||
if not render_pipeline or not node or node.isEmpty():
|
||
return
|
||
if self._material_uses_transparent_pass(material) and hasattr(render_pipeline, "prepare_scene"):
|
||
self._bake_effective_geom_materials(node)
|
||
self._isolate_transparent_geoms(node)
|
||
render_pipeline.prepare_scene(node)
|
||
except Exception as e:
|
||
print(f"刷新RenderPipeline材质模式失败: {e}")
|
||
|
||
def _bake_effective_geom_materials(self, node):
|
||
"""Bake inherited/material override state back into each GeomState for RP scene analysis."""
|
||
try:
|
||
from panda3d.core import MaterialAttrib
|
||
|
||
if not node or node.isEmpty():
|
||
return False
|
||
|
||
changed = False
|
||
for geom_np in node.find_all_matches("**/+GeomNode"):
|
||
geom_node = geom_np.node()
|
||
net_state = None
|
||
try:
|
||
net_state = geom_np.getNetState()
|
||
except Exception:
|
||
pass
|
||
|
||
for geom_index in range(geom_node.getNumGeoms()):
|
||
try:
|
||
geom_state = geom_node.getGeomState(geom_index)
|
||
except Exception:
|
||
continue
|
||
|
||
material = None
|
||
try:
|
||
if geom_state.hasAttrib(MaterialAttrib):
|
||
material_attrib = geom_state.getAttrib(MaterialAttrib)
|
||
material = material_attrib.getMaterial() if material_attrib else None
|
||
except Exception:
|
||
material = None
|
||
|
||
if material is None and net_state is not None:
|
||
try:
|
||
if net_state.hasAttrib(MaterialAttrib):
|
||
material_attrib = net_state.getAttrib(MaterialAttrib)
|
||
material = material_attrib.getMaterial() if material_attrib else None
|
||
except Exception:
|
||
material = None
|
||
|
||
if material is None:
|
||
try:
|
||
if geom_np.hasMaterial():
|
||
material = geom_np.getMaterial()
|
||
except Exception:
|
||
material = None
|
||
|
||
if material is None:
|
||
continue
|
||
|
||
try:
|
||
geom_node.setGeomState(geom_index, geom_state.setAttrib(MaterialAttrib.make(material)))
|
||
changed = True
|
||
except Exception:
|
||
continue
|
||
|
||
if changed:
|
||
self._invalidate_material_render_cache()
|
||
return changed
|
||
except Exception as e:
|
||
print(f"烘焙Geom材质状态失败: {e}")
|
||
return False
|
||
|
||
def _get_renderable_node_material(self, node):
|
||
"""Resolve the material that should represent a renderable GeomNode."""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return None
|
||
except Exception:
|
||
return None
|
||
|
||
try:
|
||
if node.hasMaterial():
|
||
material = node.getMaterial()
|
||
if material is not None:
|
||
return material
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
geom_node = node.node()
|
||
for geom_index in range(geom_node.getNumGeoms()):
|
||
geom_state = geom_node.getGeomState(geom_index)
|
||
if not geom_state.hasAttrib(MaterialAttrib):
|
||
continue
|
||
material_attrib = geom_state.getAttrib(MaterialAttrib)
|
||
material = material_attrib.getMaterial() if material_attrib else None
|
||
if material is not None:
|
||
return material
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
materials = self._get_node_materials(node)
|
||
if materials:
|
||
return materials[0]
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
def _isolate_transparent_geoms(self, node):
|
||
"""Split multi-geom GeomNodes so RP transparent materials can be routed correctly."""
|
||
try:
|
||
from panda3d.core import GeomNode, MaterialAttrib
|
||
|
||
if not node or node.isEmpty():
|
||
return False
|
||
|
||
geom_paths = [match for match in node.find_all_matches("**/+GeomNode")]
|
||
changed = False
|
||
|
||
for geom_np in geom_paths:
|
||
try:
|
||
geom_node = geom_np.node()
|
||
geom_count = geom_node.getNumGeoms()
|
||
except Exception:
|
||
continue
|
||
|
||
if geom_count <= 1:
|
||
continue
|
||
|
||
has_transparent_geom = False
|
||
for geom_index in range(geom_count):
|
||
try:
|
||
geom_state = geom_node.getGeomState(geom_index)
|
||
if not geom_state.hasAttrib(MaterialAttrib):
|
||
continue
|
||
material_attrib = geom_state.getAttrib(MaterialAttrib)
|
||
geom_material = material_attrib.getMaterial() if material_attrib else None
|
||
if geom_material and self._get_material_surface_type(geom_material) == 3:
|
||
has_transparent_geom = True
|
||
break
|
||
except Exception:
|
||
continue
|
||
|
||
if not has_transparent_geom:
|
||
continue
|
||
|
||
parent_np = geom_np.getParent()
|
||
if not parent_np or parent_np.isEmpty():
|
||
continue
|
||
|
||
try:
|
||
local_transform = geom_np.getTransform(parent_np)
|
||
except Exception:
|
||
local_transform = None
|
||
try:
|
||
local_mat = geom_np.getMat(parent_np)
|
||
except Exception:
|
||
local_mat = None
|
||
try:
|
||
path_state = geom_np.getState()
|
||
except Exception:
|
||
path_state = None
|
||
|
||
tag_keys = []
|
||
python_tag_keys = []
|
||
try:
|
||
tag_keys = list(geom_np.getTagKeys())
|
||
except Exception:
|
||
pass
|
||
try:
|
||
python_tag_keys = list(geom_np.getPythonTagKeys())
|
||
except Exception:
|
||
pass
|
||
|
||
for geom_index in range(geom_count):
|
||
try:
|
||
split_geom = geom_node.getGeom(geom_index).makeCopy()
|
||
split_state = geom_node.getGeomState(geom_index)
|
||
except Exception:
|
||
continue
|
||
|
||
effective_material = None
|
||
|
||
try:
|
||
if (not split_state.hasAttrib(MaterialAttrib)) or split_state.getAttrib(MaterialAttrib).getMaterial() is None:
|
||
effective_state = geom_np.getNetState()
|
||
if effective_state.hasAttrib(MaterialAttrib):
|
||
effective_material = effective_state.getAttrib(MaterialAttrib).getMaterial()
|
||
if effective_material is not None:
|
||
split_state = split_state.setAttrib(MaterialAttrib.make(effective_material))
|
||
else:
|
||
effective_material = split_state.getAttrib(MaterialAttrib).getMaterial()
|
||
except Exception:
|
||
pass
|
||
|
||
split_name = geom_node.getName() if geom_count == 1 else f"{geom_node.getName()}__geom_{geom_index}"
|
||
split_node = GeomNode(split_name)
|
||
split_node.addGeom(split_geom, split_state)
|
||
split_np = parent_np.attachNewNode(split_node)
|
||
|
||
if local_transform is not None:
|
||
try:
|
||
split_np.setTransform(parent_np, local_transform)
|
||
except Exception:
|
||
pass
|
||
elif local_mat is not None:
|
||
try:
|
||
split_np.setMat(parent_np, local_mat)
|
||
except Exception:
|
||
pass
|
||
|
||
if path_state is not None:
|
||
try:
|
||
split_np.setState(path_state)
|
||
except Exception:
|
||
pass
|
||
|
||
if effective_material is not None:
|
||
try:
|
||
split_np.setMaterial(effective_material, 1)
|
||
except Exception:
|
||
pass
|
||
|
||
if geom_np.isHidden():
|
||
split_np.hide()
|
||
|
||
for tag_key in tag_keys:
|
||
try:
|
||
split_np.setTag(tag_key, geom_np.getTag(tag_key))
|
||
except Exception:
|
||
pass
|
||
|
||
for tag_key in python_tag_keys:
|
||
try:
|
||
split_np.setPythonTag(tag_key, geom_np.getPythonTag(tag_key))
|
||
except Exception:
|
||
pass
|
||
|
||
geom_np.removeNode()
|
||
changed = True
|
||
|
||
if changed:
|
||
self._invalidate_material_render_cache()
|
||
return changed
|
||
except Exception as e:
|
||
print(f"拆分透明几何节点失败: {e}")
|
||
return False
|
||
|
||
def _iter_material_state_nodes(self, node):
|
||
"""Yield the concrete renderable nodes that should receive material state updates."""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return []
|
||
renderable_nodes = [match for match in node.find_all_matches("**/+GeomNode")]
|
||
if renderable_nodes:
|
||
return renderable_nodes
|
||
except Exception:
|
||
pass
|
||
return [node]
|
||
|
||
def _sync_material_render_effect(self, node, material=None, force=False, source_node=None):
|
||
"""Keep RenderPipeline effect options in sync with the current material surface mode."""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return
|
||
|
||
render_pipeline = getattr(self, "render_pipeline", None)
|
||
if not render_pipeline:
|
||
return
|
||
|
||
material = material or self._ensure_material_for_node(node)
|
||
if not material:
|
||
return
|
||
|
||
source_node = source_node or node
|
||
use_forward = self._material_uses_transparent_pass(material)
|
||
use_metallic_effect = source_node.hasTag("material_effect_metallic_enabled")
|
||
enable_parallax = source_node.hasTag("material_effect_parallax_enabled")
|
||
|
||
effect_path = "effects/pbr_with_metallic.yaml" if use_metallic_effect else "effects/default.yaml"
|
||
options = {
|
||
"normal_mapping": True,
|
||
"render_gbuffer": not use_forward,
|
||
"render_forward": use_forward,
|
||
"alpha_testing": False,
|
||
"parallax_mapping": enable_parallax,
|
||
"render_shadow": True,
|
||
"render_envmap": True,
|
||
}
|
||
sort = 60 if use_metallic_effect else 55
|
||
effect_signature = "|".join((
|
||
effect_path,
|
||
"1" if options["render_forward"] else "0",
|
||
"1" if options["render_gbuffer"] else "0",
|
||
"1" if options["parallax_mapping"] else "0",
|
||
"1" if use_metallic_effect else "0",
|
||
))
|
||
current_signature = node.getTag("material_render_effect_signature") if node.hasTag("material_render_effect_signature") else ""
|
||
if not force and current_signature == effect_signature:
|
||
return
|
||
|
||
apply_effect = getattr(render_pipeline, "_internal_set_effect", None) or getattr(render_pipeline, "set_effect", None)
|
||
if not apply_effect:
|
||
return
|
||
apply_effect(node, effect_path, options, sort)
|
||
node.setTag("material_render_effect_signature", effect_signature)
|
||
except Exception as e:
|
||
print(f"同步材质渲染 effect 失败: {e}")
|
||
|
||
def _set_material_opacity(self, node, material, opacity_value):
|
||
"""Update transparent opacity in the RenderPipeline-compatible slot."""
|
||
try:
|
||
from panda3d.core import Vec4
|
||
|
||
opacity_value = max(0.0, min(1.0, float(opacity_value)))
|
||
emission = material.emission if hasattr(material, "emission") and material.emission is not None else Vec4(3, 0, 1, 0)
|
||
surface_type = self._get_material_surface_type(material)
|
||
if surface_type != 3:
|
||
surface_type = 3
|
||
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity_value, float(emission.w)))
|
||
|
||
base_color = list(self._get_material_base_color(material))
|
||
base_color[3] = opacity_value
|
||
self._set_material_base_color(material, tuple(base_color))
|
||
self._apply_material_to_geom_states(node, material)
|
||
self._apply_material_surface_state(node, material)
|
||
except Exception as e:
|
||
print(f"设置材质透明度失败: {e}")
|
||
|
||
def _apply_material_surface_state(self, node, material):
|
||
"""Sync Panda node transparency mode with the material surface type."""
|
||
try:
|
||
from panda3d.core import ColorBlendAttrib, TransparencyAttrib
|
||
|
||
for target_node in self._iter_material_state_nodes(node):
|
||
target_material = self._get_renderable_node_material(target_node) or material
|
||
if target_material is not None:
|
||
try:
|
||
target_node.setMaterial(target_material, 1)
|
||
except Exception:
|
||
pass
|
||
|
||
# Let RenderPipeline handle transparent materials via its
|
||
# forward pass. Leaving Panda-side alpha blending enabled here
|
||
# causes the object to be blended twice and makes live editing
|
||
# hard to reason about.
|
||
target_node.setTransparency(TransparencyAttrib.M_none)
|
||
target_node.setAlphaScale(1.0)
|
||
target_node.setDepthWrite(True)
|
||
try:
|
||
target_node.clearBin()
|
||
except Exception:
|
||
pass
|
||
try:
|
||
target_node.clearAttrib(ColorBlendAttrib.getClassType())
|
||
except Exception:
|
||
pass
|
||
self._sync_material_render_effect(target_node, target_material, source_node=node)
|
||
except Exception as e:
|
||
print(f"同步材质透明状态失败: {e}")
|
||
|
||
def _set_material_base_color(self, material, color):
|
||
"""Set material base color using the APIs available in the current Panda build."""
|
||
try:
|
||
from panda3d.core import Vec4
|
||
|
||
color_vec = Vec4(*color)
|
||
if hasattr(material, "set_base_color"):
|
||
material.set_base_color(color_vec)
|
||
elif hasattr(material, "setBaseColor"):
|
||
material.setBaseColor(color_vec)
|
||
elif hasattr(material, "setDiffuse"):
|
||
material.setDiffuse(color_vec)
|
||
except Exception as e:
|
||
print(f"设置材质基础颜色失败: {e}")
|
||
|
||
|
||
def _update_material_base_color(self, material, component, value):
|
||
"""更新材质基础颜色"""
|
||
try:
|
||
base_color = self._get_material_base_color(material)
|
||
new_color = list(base_color)
|
||
|
||
if component == 'r':
|
||
new_color[0] = value
|
||
elif component == 'g':
|
||
new_color[1] = value
|
||
elif component == 'b':
|
||
new_color[2] = value
|
||
elif component == 'a':
|
||
new_color[3] = value
|
||
|
||
new_color_tuple = tuple(new_color)
|
||
|
||
self._set_material_base_color(material, new_color_tuple)
|
||
except Exception as e:
|
||
print(f"更新材质基础颜色失败: {e}")
|
||
|
||
|
||
def _update_material_roughness(self, material, value):
|
||
"""更新材质粗糙度"""
|
||
try:
|
||
if hasattr(material, 'set_roughness'):
|
||
material.set_roughness(value)
|
||
except Exception as e:
|
||
print(f"更新材质粗糙度失败: {e}")
|
||
|
||
|
||
def _update_material_metallic(self, material, value):
|
||
"""更新材质金属性"""
|
||
try:
|
||
if hasattr(material, 'set_metallic'):
|
||
material.set_metallic(value)
|
||
except Exception as e:
|
||
print(f"更新材质金属性失败: {e}")
|
||
|
||
|
||
def _update_material_ior(self, material, value):
|
||
"""更新材质折射率"""
|
||
try:
|
||
if hasattr(material, 'set_refractive_index'):
|
||
material.set_refractive_index(value)
|
||
except Exception as e:
|
||
print(f"更新材质折射率失败: {e}")
|
||
|
||
|
||
def _apply_material_preset(self, material, preset_name):
|
||
"""应用材质预设"""
|
||
try:
|
||
from panda3d.core import Vec4, Material
|
||
|
||
presets = {
|
||
"默认": {
|
||
"base_color": Vec4(0.8, 0.8, 0.8, 1.0),
|
||
"roughness": 0.5,
|
||
"metallic": 0.0,
|
||
"ior": 1.5
|
||
},
|
||
"金属": {
|
||
"base_color": Vec4(0.7, 0.7, 0.8, 1.0),
|
||
"roughness": 0.2,
|
||
"metallic": 1.0,
|
||
"ior": 2.5
|
||
},
|
||
"塑料": {
|
||
"base_color": Vec4(0.9, 0.9, 0.9, 1.0),
|
||
"roughness": 0.8,
|
||
"metallic": 0.0,
|
||
"ior": 1.45
|
||
},
|
||
"玻璃": {
|
||
"base_color": Vec4(0.9, 0.9, 1.0, 0.2),
|
||
"roughness": 0.0,
|
||
"metallic": 0.0,
|
||
"ior": 1.5
|
||
},
|
||
"木材": {
|
||
"base_color": Vec4(0.6, 0.4, 0.2, 1.0),
|
||
"roughness": 0.9,
|
||
"metallic": 0.0,
|
||
"ior": 1.55
|
||
},
|
||
"混凝土": {
|
||
"base_color": Vec4(0.5, 0.5, 0.5, 1.0),
|
||
"roughness": 1.0,
|
||
"metallic": 0.0,
|
||
"ior": 1.5
|
||
}
|
||
}
|
||
|
||
if preset_name in presets:
|
||
preset = presets[preset_name]
|
||
|
||
# 应用基础颜色
|
||
self._set_material_base_color(material, (
|
||
preset["base_color"].x,
|
||
preset["base_color"].y,
|
||
preset["base_color"].z,
|
||
preset["base_color"].w,
|
||
))
|
||
|
||
# 应用PBR属性
|
||
if hasattr(material, 'set_roughness'):
|
||
material.set_roughness(preset["roughness"])
|
||
if hasattr(material, 'set_metallic'):
|
||
material.set_metallic(preset["metallic"])
|
||
if hasattr(material, 'set_refractive_index'):
|
||
material.set_refractive_index(preset["ior"])
|
||
if hasattr(material, "set_emission"):
|
||
surface_type = 3.0 if preset_name == "玻璃" else 0.0
|
||
opacity = preset["base_color"].w if preset_name == "玻璃" else 1.0
|
||
material.set_emission(Vec4(surface_type, 0.0, opacity, 0.0))
|
||
|
||
print(f"已应用材质预设: {preset_name}")
|
||
except Exception as e:
|
||
print(f"应用材质预设失败: {e}")
|
||
|
||
|
||
def _apply_material_to_node(self, node):
|
||
"""为节点应用材质"""
|
||
try:
|
||
material = self._ensure_material_for_node(node)
|
||
if material:
|
||
self._apply_material_surface_state(node, material)
|
||
print("材质已应用到节点")
|
||
except Exception as e:
|
||
print(f"应用材质失败: {e}")
|
||
|
||
|
||
def _reset_material(self, node):
|
||
"""重置节点材质"""
|
||
try:
|
||
materials = list(node.find_all_materials())
|
||
|
||
for material in materials:
|
||
# 重置为默认材质属性
|
||
from panda3d.core import Vec4
|
||
default_color = Vec4(0.8, 0.8, 0.8, 1.0)
|
||
|
||
self._set_material_base_color(material, (0.8, 0.8, 0.8, 1.0))
|
||
|
||
if hasattr(material, 'set_roughness'):
|
||
material.set_roughness(0.5)
|
||
if hasattr(material, 'set_metallic'):
|
||
material.set_metallic(0.0)
|
||
if hasattr(material, 'set_refractive_index'):
|
||
material.set_refractive_index(1.5)
|
||
if hasattr(material, 'set_emission'):
|
||
material.set_emission(Vec4(0.0, 0.0, 1.0, 0.0))
|
||
self._apply_material_surface_state(node, material)
|
||
|
||
print(f"已重置材质")
|
||
except Exception as e:
|
||
print(f"重置材质失败: {e}")
|
||
|
||
|
||
def _select_texture_for_material(self, node, material, texture_type):
|
||
"""为材质选择纹理"""
|
||
try:
|
||
import tkinter as tk
|
||
from tkinter import filedialog
|
||
|
||
# 推断初始目录
|
||
initial_dir = getattr(self, "_texture_dialog_path", "")
|
||
if not initial_dir or not os.path.exists(initial_dir):
|
||
candidates = [
|
||
os.path.join(os.getcwd(), "Resources", "textures"),
|
||
os.path.join(os.getcwd(), "Resources"),
|
||
os.getcwd(),
|
||
]
|
||
initial_dir = next((p for p in candidates if os.path.exists(p)), os.getcwd())
|
||
|
||
# 弹出系统文件选择窗口
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
try:
|
||
root.attributes("-topmost", True)
|
||
except Exception:
|
||
pass
|
||
|
||
selected_path = filedialog.askopenfilename(
|
||
title=f"选择{texture_type}贴图",
|
||
initialdir=initial_dir,
|
||
filetypes=[
|
||
("Image Files", "*.png;*.jpg;*.jpeg;*.bmp;*.tif;*.tiff;*.tga;*.dds;*.ktx;*.hdr;*.exr"),
|
||
("All Files", "*.*"),
|
||
],
|
||
)
|
||
root.destroy()
|
||
|
||
if selected_path and os.path.exists(selected_path):
|
||
self._texture_dialog_path = os.path.dirname(selected_path)
|
||
self._apply_texture_to_material(node, material, texture_type, selected_path)
|
||
else:
|
||
print(f"已取消选择{texture_type}贴图")
|
||
|
||
except Exception as e:
|
||
print(f"选择纹理失败: {e}")
|
||
|
||
|
||
def _get_material_texture_slots(self):
|
||
"""材质贴图类型到固定槽位映射(对应 p3d_TextureN)"""
|
||
return {
|
||
"diffuse": 0, # p3d_Texture0
|
||
"normal": 1, # p3d_Texture1
|
||
"ior": 2, # p3d_Texture2
|
||
"roughness": 3, # p3d_Texture3
|
||
"parallax": 4, # p3d_Texture4
|
||
"metallic": 5, # p3d_Texture5
|
||
"emission": 6, # p3d_Texture6
|
||
"ao": 7, # p3d_Texture7
|
||
"alpha": 8, # p3d_Texture8
|
||
"detail": 9, # p3d_Texture9
|
||
"gloss": 10 # p3d_Texture10
|
||
}
|
||
|
||
|
||
def _get_material_stage_mode_map(self):
|
||
"""不同贴图类型对应的 TextureStage 模式"""
|
||
from panda3d.core import TextureStage
|
||
return {
|
||
"diffuse": TextureStage.MModulate,
|
||
"normal": TextureStage.MNormal,
|
||
"roughness": TextureStage.MGloss,
|
||
"metallic": TextureStage.MSelector,
|
||
"emission": TextureStage.MGlow,
|
||
"ao": TextureStage.MSelector,
|
||
"alpha": TextureStage.MBlend,
|
||
"parallax": TextureStage.MHeight,
|
||
"detail": TextureStage.MModulate,
|
||
"gloss": TextureStage.MGloss,
|
||
"ior": TextureStage.MSelector,
|
||
}
|
||
|
||
|
||
def _find_texture_stage_by_name(self, node, stage_name):
|
||
"""按名称查找节点上的纹理阶段"""
|
||
texture_stages = node.findAllTextureStages()
|
||
for i in range(texture_stages.getNumTextureStages()):
|
||
stage = texture_stages.getTextureStage(i)
|
||
if stage and stage.getName() == stage_name:
|
||
return stage
|
||
return None
|
||
|
||
|
||
def _get_neutral_texture(self, texture_type):
|
||
"""获取指定贴图类型的中性占位纹理(1x1)"""
|
||
try:
|
||
from panda3d.core import Texture
|
||
|
||
if not hasattr(self, "_neutral_texture_cache"):
|
||
self._neutral_texture_cache = {}
|
||
if texture_type in self._neutral_texture_cache:
|
||
return self._neutral_texture_cache[texture_type]
|
||
|
||
# 各类型中性值,确保在未显式设置该通道时不影响结果
|
||
neutral_rgba = {
|
||
"diffuse": (255, 255, 255, 255), # 白色,不改变底色
|
||
"normal": (128, 128, 255, 255), # 平面法线
|
||
"ior": (0, 0, 0, 255), # blend_ior 中的“无附加影响”值
|
||
"roughness": (255, 255, 255, 255), # 1.0
|
||
"parallax": (0, 0, 0, 255), # 0 高度
|
||
"metallic": (255, 255, 255, 255), # 1.0,保持乘法中性
|
||
"emission": (0, 0, 0, 255), # 无自发光
|
||
"ao": (255, 255, 255, 255), # 1.0,不衰减
|
||
"alpha": (255, 255, 255, 255), # 完全不透明
|
||
"detail": (255, 255, 255, 255), # 不改变细节叠加
|
||
"gloss": (0, 0, 0, 255), # 0 光泽(对 roughness 影响最小)
|
||
}
|
||
|
||
rgba = neutral_rgba.get(texture_type, (255, 255, 255, 255))
|
||
tex = Texture(f"__neutral_{texture_type}")
|
||
tex.setup2dTexture(1, 1, Texture.T_unsigned_byte, Texture.F_rgba8)
|
||
tex.setRamImage(bytes(rgba))
|
||
tex.setMagfilter(Texture.FTNearest)
|
||
tex.setMinfilter(Texture.FTNearest)
|
||
|
||
self._neutral_texture_cache[texture_type] = tex
|
||
return tex
|
||
except Exception as e:
|
||
print(f"创建中性纹理失败({texture_type}): {e}")
|
||
return None
|
||
|
||
|
||
def _ensure_texture_slot_alignment(self, node, target_slot, texture_slots, stage_mode_map):
|
||
"""补齐低位槽的占位纹理,确保 p3d_TextureN 与固定槽位一致"""
|
||
try:
|
||
from panda3d.core import TextureStage
|
||
except Exception:
|
||
return
|
||
|
||
slot_to_type = {slot: tex_type for tex_type, slot in texture_slots.items()}
|
||
for required_slot in range(target_slot):
|
||
required_type = slot_to_type.get(required_slot)
|
||
if not required_type:
|
||
continue
|
||
|
||
stage_name = f"{required_type}_map"
|
||
if self._find_texture_stage_by_name(node, stage_name):
|
||
continue
|
||
|
||
neutral_texture = self._get_neutral_texture(required_type)
|
||
if not neutral_texture:
|
||
continue
|
||
|
||
stage = TextureStage(stage_name)
|
||
stage.setSort(required_slot)
|
||
stage.setMode(stage_mode_map.get(required_type, TextureStage.MSelector))
|
||
node.setTexture(stage, neutral_texture, 1)
|
||
|
||
|
||
def _ensure_metallic_texture_effect(self, node):
|
||
"""启用支持 p3d_Texture5 的 effect,确保金属性贴图生效"""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return
|
||
|
||
node.setTag("material_effect_metallic_enabled", "1")
|
||
self._sync_material_render_effect(node)
|
||
except Exception as e:
|
||
print(f"启用金属性贴图 effect 失败: {e}")
|
||
|
||
|
||
def _ensure_default_texture_effect(self, node, enable_parallax=False):
|
||
"""启用默认贴图 effect(主要用于法线/粗糙度等通道)"""
|
||
try:
|
||
if not node or node.isEmpty():
|
||
return
|
||
|
||
# 已启用金属性增强 effect 时,不覆盖它
|
||
if node.hasTag("material_effect_metallic_enabled"):
|
||
return
|
||
|
||
parallax_enabled = enable_parallax or node.hasTag("material_effect_parallax_enabled")
|
||
if parallax_enabled:
|
||
node.setTag("material_effect_parallax_enabled", "1")
|
||
|
||
node.setTag("material_effect_default_texture_enabled", "1")
|
||
self._sync_material_render_effect(node)
|
||
except Exception as e:
|
||
print(f"启用默认贴图 effect 失败: {e}")
|
||
|
||
|
||
def _set_material_scalar_property(self, material, prop_name, value):
|
||
"""安全设置材质标量属性(兼容 snake_case / CamelCase)"""
|
||
try:
|
||
if not material:
|
||
return False
|
||
|
||
setter_map = {
|
||
"roughness": ("set_roughness", "setRoughness"),
|
||
"metallic": ("set_metallic", "setMetallic"),
|
||
"ior": ("set_refractive_index", "setRefractiveIndex"),
|
||
}
|
||
setters = setter_map.get(prop_name, ())
|
||
for setter_name in setters:
|
||
if hasattr(material, setter_name):
|
||
getattr(material, setter_name)(value)
|
||
return True
|
||
|
||
if hasattr(material, prop_name):
|
||
setattr(material, prop_name, value)
|
||
return True
|
||
except Exception as e:
|
||
print(f"设置材质属性失败({prop_name}={value}): {e}")
|
||
return False
|
||
|
||
|
||
def _get_material_emission(self, material):
|
||
"""安全获取材质发光向量"""
|
||
try:
|
||
if not material:
|
||
return None
|
||
if hasattr(material, "emission") and material.emission is not None:
|
||
return material.emission
|
||
if hasattr(material, "get_emission"):
|
||
return material.get_emission()
|
||
if hasattr(material, "getEmission"):
|
||
return material.getEmission()
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
|
||
def _set_material_emission(self, material, emission_vec):
|
||
"""安全设置材质发光向量"""
|
||
try:
|
||
if not material:
|
||
return False
|
||
if hasattr(material, "set_emission"):
|
||
material.set_emission(emission_vec)
|
||
return True
|
||
if hasattr(material, "setEmission"):
|
||
material.setEmission(emission_vec)
|
||
return True
|
||
if hasattr(material, "emission"):
|
||
material.emission = emission_vec
|
||
return True
|
||
except Exception as e:
|
||
print(f"设置材质发光参数失败: {e}")
|
||
return False
|
||
|
||
|
||
def _configure_material_for_texture_map(self, material, texture_type):
|
||
"""贴图驱动参数修正:避免必须手动把数值调到 1.0 才生效"""
|
||
try:
|
||
from panda3d.core import Vec4
|
||
|
||
if not material:
|
||
return
|
||
|
||
if texture_type == "roughness":
|
||
self._set_material_scalar_property(material, "roughness", 1.0)
|
||
elif texture_type == "metallic":
|
||
# 默认 metallic=0 会把金属贴图乘没,设置为 1 让贴图直接驱动
|
||
self._set_material_scalar_property(material, "metallic", 1.0)
|
||
elif texture_type == "normal":
|
||
# RenderPipeline 中 normalfactor 映射到 emission.y
|
||
emission = self._get_material_emission(material)
|
||
if emission is None:
|
||
emission = Vec4(0.0, 0.0, 0.0, 1.0)
|
||
|
||
new_emission = Vec4(
|
||
float(getattr(emission, "x", 0.0)),
|
||
1.0,
|
||
float(getattr(emission, "z", 0.0)),
|
||
float(getattr(emission, "w", 1.0)),
|
||
)
|
||
self._set_material_emission(material, new_emission)
|
||
except Exception as e:
|
||
print(f"修正贴图驱动参数失败({texture_type}): {e}")
|
||
|
||
|
||
def _apply_texture_to_material(self, node, material, texture_type, texture_path):
|
||
"""应用纹理到材质"""
|
||
try:
|
||
import os
|
||
from panda3d.core import TextureStage, Filename
|
||
|
||
# 加载纹理(优先使用应用的 loader,避免创建临时 Loader 导致状态不一致)
|
||
loader = getattr(self, "loader", None)
|
||
if not loader:
|
||
print("无法加载纹理: loader 不可用")
|
||
return
|
||
|
||
# 关键修复:把系统路径统一转换成 Panda3D 可识别格式(避免 D:/ 与 /d/ 冲突)
|
||
normalized_path = texture_path
|
||
if texture_path:
|
||
try:
|
||
from scene import util
|
||
normalized_path = util.normalize_model_path(texture_path)
|
||
except Exception:
|
||
# 在 scene 模块初始化阶段可能出现循环导入,回退到 Panda3D 标准路径
|
||
normalized_path = Filename.from_os_specific(texture_path).get_fullpath()
|
||
panda_filename = Filename.from_os_specific(texture_path) if texture_path else None
|
||
|
||
texture = None
|
||
candidate_paths = []
|
||
if normalized_path:
|
||
candidate_paths.append(normalized_path)
|
||
if panda_filename:
|
||
candidate_paths.append(panda_filename)
|
||
candidate_paths.append(panda_filename.get_fullpath())
|
||
if texture_path:
|
||
candidate_paths.append(texture_path)
|
||
|
||
for path_candidate in candidate_paths:
|
||
if not path_candidate:
|
||
continue
|
||
try:
|
||
texture = loader.loadTexture(path_candidate)
|
||
if texture:
|
||
break
|
||
except Exception:
|
||
continue
|
||
|
||
if not texture:
|
||
print(f"无法加载纹理: {texture_path} (normalized={normalized_path})")
|
||
return
|
||
|
||
# 设置纹理属性
|
||
texture.setMagfilter(texture.FTLinear)
|
||
texture.setMinfilter(texture.FTLinearMipmapLinear)
|
||
|
||
texture_type = (texture_type or "").strip().lower()
|
||
texture_slots = self._get_material_texture_slots()
|
||
if texture_type not in texture_slots:
|
||
print(f"未知纹理类型: {texture_type}")
|
||
return
|
||
|
||
slot = texture_slots[texture_type]
|
||
|
||
# 纹理阶段名保持稳定,便于重复设置时精确替换
|
||
stage_name = f"{texture_type}_map"
|
||
|
||
# 修正材质参数,确保贴图不需要手工把滑块调到 1.0 才可见
|
||
self._configure_material_for_texture_map(material, texture_type)
|
||
|
||
# 清理同类型旧 stage,避免叠加造成覆盖/污染
|
||
old_stage = self._find_texture_stage_by_name(node, stage_name)
|
||
if old_stage:
|
||
node.clearTexture(old_stage)
|
||
|
||
# 关键修复:补齐低位槽,避免 p3d_TextureN 因“缺槽”而错位
|
||
stage_mode_map = self._get_material_stage_mode_map()
|
||
self._ensure_texture_slot_alignment(node, slot, texture_slots, stage_mode_map)
|
||
|
||
texture_stage = TextureStage(stage_name)
|
||
texture_stage.setSort(slot)
|
||
texture_stage.setMode(stage_mode_map.get(texture_type, TextureStage.MSelector))
|
||
|
||
# 应用 stage(用于可视状态与序列化)
|
||
node.setTexture(texture_stage, texture, 1)
|
||
|
||
# 同时写入 shader 输入,进一步保证固定槽位可直接读取
|
||
node.setShaderInput(f"p3d_Texture{slot}", texture)
|
||
|
||
# 金属性贴图需要额外 effect 才会被默认管线采样
|
||
if texture_type == "metallic":
|
||
self._ensure_metallic_texture_effect(node)
|
||
else:
|
||
# 法线贴图依赖 normal_mapping 宏,显式开启默认 effect
|
||
self._ensure_default_texture_effect(node, enable_parallax=(texture_type == "parallax"))
|
||
|
||
# 记录路径,便于 UI 展示和后续恢复
|
||
if texture_path:
|
||
node.setTag(f"material_texture_{texture_type}", os.path.normpath(normalized_path or texture_path))
|
||
|
||
print(f"已应用{texture_type}纹理到槽位 p3d_Texture{slot}: {texture_path}")
|
||
|
||
except Exception as e:
|
||
print(f"应用纹理失败: {e}")
|
||
|
||
|
||
def _clear_all_textures(self, node):
|
||
"""清除节点所有纹理"""
|
||
try:
|
||
# 清除所有纹理阶段
|
||
node.clearTexture()
|
||
|
||
# 清理额外 shader input 与记录标签
|
||
texture_slots = self._get_material_texture_slots()
|
||
for texture_type, slot in texture_slots.items():
|
||
try:
|
||
node.clearShaderInput(f"p3d_Texture{slot}")
|
||
except Exception:
|
||
pass
|
||
tag_name = f"material_texture_{texture_type}"
|
||
if node.hasTag(tag_name):
|
||
node.clearTag(tag_name)
|
||
if node.hasTag("material_effect_metallic_enabled"):
|
||
node.clearTag("material_effect_metallic_enabled")
|
||
if node.hasTag("material_effect_default_texture_enabled"):
|
||
node.clearTag("material_effect_default_texture_enabled")
|
||
if node.hasTag("material_effect_parallax_enabled"):
|
||
node.clearTag("material_effect_parallax_enabled")
|
||
|
||
print("已清除所有纹理")
|
||
except Exception as e:
|
||
print(f"清除纹理失败: {e}")
|
||
|
||
|
||
def _display_current_textures(self, node, material):
|
||
"""显示当前纹理信息"""
|
||
try:
|
||
# 获取所有纹理阶段
|
||
texture_stages = node.findAllTextureStages()
|
||
|
||
has_any = False
|
||
|
||
if texture_stages:
|
||
imgui.text("当前纹理:")
|
||
for i in range(texture_stages.getNumTextureStages()):
|
||
stage = texture_stages.getTextureStage(i)
|
||
texture = node.getTexture(stage)
|
||
if texture:
|
||
stage_name = stage.getName() or "未命名"
|
||
# 隐藏未被用户显式设置的占位纹理,避免面板误导
|
||
if stage_name.endswith("_map"):
|
||
tex_type = stage_name[:-4]
|
||
if tex_type and not node.hasTag(f"material_texture_{tex_type}"):
|
||
continue
|
||
|
||
texture_name = texture.getName() or "未命名"
|
||
imgui.text(f" {stage_name}: {texture_name}")
|
||
has_any = True
|
||
|
||
# 显示属性面板记录的贴图路径(包含非漫反射通道)
|
||
tracked_types = ["diffuse", "normal", "roughness", "metallic", "emission", "ao", "alpha", "parallax", "detail", "gloss", "ior"]
|
||
for texture_type in tracked_types:
|
||
tag_name = f"material_texture_{texture_type}"
|
||
if node.hasTag(tag_name):
|
||
imgui.text(f" {texture_type}: {node.getTag(tag_name)}")
|
||
has_any = True
|
||
|
||
if not has_any:
|
||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "当前无纹理")
|
||
except Exception as e:
|
||
print(f"显示纹理信息失败: {e}")
|
||
|
||
|
||
def _update_shading_model(self, material, model_index):
|
||
"""更新着色模型"""
|
||
try:
|
||
from panda3d.core import Vec4
|
||
|
||
# 根据不同的着色模型设置相应的参数
|
||
if model_index == 1: # 自发光着色模型
|
||
print("设置自发光着色模型...")
|
||
if hasattr(material, 'set_emission'):
|
||
current_emission = material.emission or Vec4(0, 0, 0, 0)
|
||
new_emission = Vec4(1.0, current_emission.y, current_emission.z, current_emission.w)
|
||
material.set_emission(new_emission)
|
||
print("自发光着色模型设置完成")
|
||
|
||
elif model_index == 3: # 透明着色模型
|
||
print("设置透明着色模型...")
|
||
if hasattr(material, 'set_emission'):
|
||
current_emission = material.emission or Vec4(0, 0, 1, 0)
|
||
new_emission = Vec4(3.0, current_emission.y, current_emission.z, current_emission.w)
|
||
material.set_emission(new_emission)
|
||
|
||
print("透明着色模型设置完成")
|
||
|
||
else: # 默认着色模型
|
||
print("设置默认着色模型...")
|
||
if hasattr(material, 'set_emission'):
|
||
current_emission = material.emission or Vec4(0, 0, 0, 0)
|
||
new_emission = Vec4(0.0, current_emission.y, current_emission.z, current_emission.w)
|
||
material.set_emission(new_emission)
|
||
print("默认着色模型设置完成")
|
||
|
||
print(f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})")
|
||
|
||
except Exception as e:
|
||
print(f"更新着色模型失败: {e}")
|
||
|
||
|
||
def _update_transparency(self, material, opacity_value):
|
||
"""更新透明度"""
|
||
try:
|
||
from panda3d.core import Vec4
|
||
if hasattr(material, 'set_emission'):
|
||
current_emission = material.emission or Vec4(3, 0, 1, 0)
|
||
material.set_emission(Vec4(current_emission.x, current_emission.y, opacity_value, current_emission.w))
|
||
print(f"透明度已更新: {opacity_value}")
|
||
except Exception as e:
|
||
print(f"更新透明度失败: {e}")
|
||
|
||
|
||
def _draw_texture_file_dialog(self):
|
||
"""系统文件对话框模式下无需绘制 ImGui 贴图对话框"""
|
||
return
|
||
|
||
def start_transform_monitoring(self, node):
|
||
"""开始变换监控"""
|
||
if node and not node.isEmpty():
|
||
self._monitored_node = node
|
||
self._transform_monitoring = True
|
||
self._transform_update_timer = 0
|
||
|
||
# 记录初始变换值
|
||
self._update_last_transform_values()
|
||
|
||
|
||
def stop_transform_monitoring(self):
|
||
"""停止变换监控"""
|
||
self._transform_monitoring = False
|
||
self._monitored_node = None
|
||
self._last_transform_values = {}
|
||
|
||
|
||
def _update_last_transform_values(self):
|
||
"""更新最后记录的变换值"""
|
||
if self._monitored_node and not self._monitored_node.isEmpty():
|
||
try:
|
||
pos = self._monitored_node.getPos()
|
||
hpr = self._monitored_node.getHpr()
|
||
scale = self._monitored_node.getScale()
|
||
|
||
self._last_transform_values = {
|
||
'pos': (pos.x, pos.y, pos.z),
|
||
'hpr': (hpr.x, hpr.y, hpr.z),
|
||
'scale': (scale.x, scale.y, scale.z)
|
||
}
|
||
except Exception as e:
|
||
print(f"更新变换值失败: {e}")
|
||
|
||
|
||
def _check_transform_changes(self):
|
||
"""检查变换变化"""
|
||
if not self._transform_monitoring or not self._monitored_node:
|
||
return
|
||
|
||
try:
|
||
pos = self._monitored_node.getPos()
|
||
hpr = self._monitored_node.getHpr()
|
||
scale = self._monitored_node.getScale()
|
||
|
||
current_values = {
|
||
'pos': (pos.x, pos.y, pos.z),
|
||
'hpr': (hpr.x, hpr.y, hpr.z),
|
||
'scale': (scale.x, scale.y, scale.z)
|
||
}
|
||
|
||
# 检查是否有变化
|
||
if current_values != self._last_transform_values:
|
||
# 更新记录的值
|
||
self._last_transform_values = current_values
|
||
# 触发属性面板更新(通过设置更新标志)
|
||
self.property_panel_update_timer = 0
|
||
|
||
except Exception as e:
|
||
print(f"检查变换变化失败: {e}")
|
||
|
||
|
||
def update_transform_monitoring(self, dt):
|
||
"""更新变换监控(在主循环中调用)"""
|
||
if not self._transform_monitoring:
|
||
return
|
||
|
||
self._transform_update_timer += dt
|
||
if self._transform_update_timer >= self._transform_update_interval:
|
||
self._transform_update_timer = 0
|
||
self._check_transform_changes()
|
||
|
||
|
||
def show_color_picker(self, target_object, property_name, initial_color, callback=None):
|
||
"""显示颜色选择器"""
|
||
self._color_picker_active = True
|
||
self._color_picker_target = (target_object, property_name)
|
||
self._color_picker_current_color = initial_color
|
||
self._color_picker_callback = callback
|
||
|
||
|
||
def _draw_color_picker(self):
|
||
"""绘制颜色选择器对话框"""
|
||
if not self._color_picker_active:
|
||
return
|
||
|
||
# 设置对话框标志
|
||
flags = (imgui.WindowFlags_.no_resize |
|
||
imgui.WindowFlags_.no_collapse |
|
||
imgui.WindowFlags_.modal)
|
||
|
||
dialog_width = 300
|
||
dialog_height = 400
|
||
self.style_manager.prepare_centered_dialog(dialog_width, dialog_height)
|
||
|
||
with imgui_ctx.begin("颜色选择器", True, flags) as window:
|
||
if not window.opened:
|
||
self._color_picker_active = False
|
||
self._color_picker_target = None
|
||
return
|
||
|
||
imgui.text("选择颜色")
|
||
imgui.separator()
|
||
|
||
# 颜色编辑器
|
||
changed, new_color = imgui.color_edit4(
|
||
"颜色##color_picker",
|
||
self._color_picker_current_color
|
||
)
|
||
|
||
if changed:
|
||
self._color_picker_current_color = new_color
|
||
|
||
# 预设颜色
|
||
imgui.text("预设颜色")
|
||
preset_colors = [
|
||
(1.0, 0.0, 0.0, 1.0), # 红色
|
||
(0.0, 1.0, 0.0, 1.0), # 绿色
|
||
(0.0, 0.0, 1.0, 1.0), # 蓝色
|
||
(1.0, 1.0, 0.0, 1.0), # 黄色
|
||
(1.0, 0.0, 1.0, 1.0), # 洋红
|
||
(0.0, 1.0, 1.0, 1.0), # 青色
|
||
(1.0, 1.0, 1.0, 1.0), # 白色
|
||
(0.0, 0.0, 0.0, 1.0), # 黑色
|
||
(0.5, 0.5, 0.5, 1.0), # 灰色
|
||
(0.188, 0.404, 0.753, 1.0), # 主题蓝色
|
||
(0.176, 1.0, 0.769, 1.0), # 主题绿色
|
||
(0.953, 0.616, 0.471, 1.0), # 主题橙色
|
||
]
|
||
|
||
# 绘制预设颜色按钮
|
||
colors_per_row = 6
|
||
for i, color in enumerate(preset_colors):
|
||
if i % colors_per_row != 0:
|
||
imgui.same_line()
|
||
|
||
imgui.color_button(f"preset_{i}", color, 0, (30, 30))
|
||
if imgui.is_item_clicked():
|
||
self._color_picker_current_color = color
|
||
|
||
imgui.separator()
|
||
|
||
# 按钮区域
|
||
if imgui.button("确定"):
|
||
self._apply_color_selection()
|
||
self._color_picker_active = False
|
||
self._color_picker_target = None
|
||
|
||
imgui.same_line()
|
||
if imgui.button("取消"):
|
||
self._color_picker_active = False
|
||
self._color_picker_target = None
|
||
|
||
|
||
def _apply_color_selection(self):
|
||
"""应用颜色选择"""
|
||
if self._color_picker_callback:
|
||
try:
|
||
self._color_picker_callback(self._color_picker_current_color)
|
||
except Exception as e:
|
||
print(f"颜色回调执行失败: {e}")
|
||
return
|
||
|
||
if not self._color_picker_target:
|
||
return
|
||
|
||
target_object, property_name = self._color_picker_target
|
||
|
||
try:
|
||
# 应用颜色到目标对象
|
||
if hasattr(target_object, 'setColor'):
|
||
target_object.setColor(self._color_picker_current_color)
|
||
elif property_name and hasattr(target_object, property_name):
|
||
setattr(target_object, property_name, self._color_picker_current_color)
|
||
|
||
except Exception as e:
|
||
print(f"应用颜色失败: {e}")
|
||
|
||
|
||
def _draw_color_button(self, label, color, size=(50, 20)):
|
||
"""绘制颜色按钮并支持点击打开颜色选择器"""
|
||
imgui.color_button(label, color, 0, size)
|
||
if imgui.is_item_clicked():
|
||
# 打开颜色选择器
|
||
self.show_color_picker(None, None, color)
|
||
|
||
|
||
def _refresh_available_fonts(self):
|
||
"""刷新可用字体列表"""
|
||
try:
|
||
import platform
|
||
from pathlib import Path
|
||
|
||
system = platform.system().lower()
|
||
font_paths = []
|
||
|
||
if system == "linux":
|
||
font_dirs = [
|
||
"/usr/share/fonts/truetype/",
|
||
"/usr/share/fonts/opentype/",
|
||
"/usr/local/share/fonts/",
|
||
"~/.fonts/"
|
||
]
|
||
elif system == "windows":
|
||
font_dirs = [
|
||
"C:/Windows/Fonts/",
|
||
]
|
||
elif system == "darwin":
|
||
font_dirs = [
|
||
"/System/Library/Fonts/",
|
||
"/Library/Fonts/",
|
||
"~/Library/Fonts/"
|
||
]
|
||
else:
|
||
font_dirs = []
|
||
|
||
# 扫描字体目录
|
||
common_fonts = []
|
||
for font_dir in font_dirs:
|
||
font_path = Path(font_dir).expanduser()
|
||
if font_path.exists():
|
||
for font_file in font_path.rglob("*.ttf"):
|
||
common_fonts.append(str(font_file))
|
||
for font_file in font_path.rglob("*.otf"):
|
||
common_fonts.append(str(font_file))
|
||
for font_file in font_path.rglob("*.ttc"):
|
||
common_fonts.append(str(font_file))
|
||
|
||
# 过滤常见字体
|
||
font_keywords = [
|
||
"arial", "helvetica", "times", "courier", "verdana", "georgia",
|
||
"comic", "impact", "trebuchet", "palatino", "garamond",
|
||
"noto", "dejavu", "liberation", "ubuntu", "roboto", "open",
|
||
"droid", "source", "wenquanyi", "wqy", "pingfang", "stheiti",
|
||
"microsoft", "msyh", "simsun", "simhei", "kaiti", "fangsong"
|
||
]
|
||
|
||
self._available_fonts = []
|
||
for font_path in common_fonts:
|
||
font_name = Path(font_path).name.lower()
|
||
if any(keyword in font_name for keyword in font_keywords):
|
||
self._available_fonts.append(font_path)
|
||
|
||
# 添加一些默认字体路径
|
||
default_fonts = [
|
||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||
"/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
|
||
"C:/Windows/Fonts/arial.ttf",
|
||
"C:/Windows/Fonts/msyh.ttc",
|
||
"/System/Library/Fonts/PingFang.ttc"
|
||
]
|
||
|
||
for font_path in default_fonts:
|
||
if Path(font_path).exists() and font_path not in self._available_fonts:
|
||
self._available_fonts.append(font_path)
|
||
|
||
print(f"✓ 找到 {len(self._available_fonts)} 个可用字体")
|
||
|
||
except Exception as e:
|
||
print(f"⚠ 字体扫描失败: {e}")
|
||
self._available_fonts = []
|
||
|
||
|
||
def show_font_selector(self, target_object, property_name, current_font, callback=None):
|
||
"""显示字体选择器"""
|
||
self._font_selector_active = True
|
||
self._font_selector_target = (target_object, property_name)
|
||
self._font_selector_current_font = current_font or ""
|
||
self._font_selector_search_text = ""
|
||
self._font_selector_callback = callback
|
||
|
||
|
||
def _draw_font_selector(self):
|
||
"""绘制字体选择器对话框"""
|
||
if not self._font_selector_active:
|
||
return
|
||
|
||
# 设置对话框标志
|
||
flags = (imgui.WindowFlags_.no_resize |
|
||
imgui.WindowFlags_.no_collapse |
|
||
imgui.WindowFlags_.modal)
|
||
|
||
dialog_width = 400
|
||
dialog_height = 500
|
||
self.style_manager.prepare_centered_dialog(dialog_width, dialog_height)
|
||
|
||
with imgui_ctx.begin("字体选择器", True, flags) as window:
|
||
if not window.opened:
|
||
self._font_selector_active = False
|
||
self._font_selector_target = None
|
||
return
|
||
|
||
imgui.text("选择字体")
|
||
imgui.separator()
|
||
|
||
# 当前字体显示
|
||
imgui.text(f"当前字体: {self._font_selector_current_font or '默认'}")
|
||
|
||
# 字体搜索框
|
||
changed, search_text = imgui.input_text("搜索", self._font_selector_search_text, 256)
|
||
if changed:
|
||
self._font_selector_search_text = search_text
|
||
search_text = self._font_selector_search_text
|
||
imgui.separator()
|
||
|
||
# 字体列表
|
||
if imgui.begin_child("font_list", (380, 300)):
|
||
for font_path in self._available_fonts:
|
||
font_name = Path(font_path).name
|
||
|
||
# 搜索过滤
|
||
if search_text and search_text.lower() not in font_name.lower():
|
||
continue
|
||
|
||
# 字体项
|
||
if imgui.selectable(font_name, font_path == self._font_selector_current_font):
|
||
self._font_selector_current_font = font_path
|
||
|
||
# 显示字体路径作为工具提示
|
||
if imgui.is_item_hovered():
|
||
imgui.set_tooltip(font_path)
|
||
|
||
imgui.end_child()
|
||
|
||
imgui.separator()
|
||
|
||
# 按钮区域
|
||
if imgui.button("确定"):
|
||
self._apply_font_selection()
|
||
self._font_selector_active = False
|
||
self._font_selector_target = None
|
||
|
||
imgui.same_line()
|
||
if imgui.button("取消"):
|
||
self._font_selector_active = False
|
||
self._font_selector_target = None
|
||
|
||
imgui.same_line()
|
||
if imgui.button("刷新字体"):
|
||
self._refresh_available_fonts()
|
||
|
||
|
||
def _apply_font_selection(self):
|
||
"""应用字体选择"""
|
||
if not self._font_selector_target:
|
||
return
|
||
|
||
target_object, property_name = self._font_selector_target
|
||
|
||
try:
|
||
# 应用字体到目标对象
|
||
if hasattr(target_object, property_name):
|
||
setattr(target_object, property_name, self._font_selector_current_font)
|
||
|
||
# 调用回调函数
|
||
if self._font_selector_callback:
|
||
self._font_selector_callback(self._font_selector_current_font)
|
||
|
||
except Exception as e:
|
||
print(f"应用字体失败: {e}")
|
||
|
||
|
||
def _draw_font_selector_button(self, label, current_font):
|
||
"""绘制字体选择器按钮"""
|
||
font_name = Path(current_font).name if current_font else "默认字体"
|
||
display_text = f"{font_name[:20]}..." if len(font_name) > 20 else font_name
|
||
|
||
if imgui.button(f"{label}: {display_text}##font_selector"):
|
||
self.show_font_selector(None, None, current_font)
|