EG/ui/panels/property_helpers.py

2848 lines
115 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_collision_node(self, node):
"""Return the first collision child node for a scene node."""
try:
if not node or node.isEmpty():
return None
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
except Exception:
pass
return None
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._build_collision_node(node, shape_type)
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 _build_collision_node(self, node, shape_type, position_offset=None, visible=None, **shape_kwargs):
"""Create a collision node for the given shape and restore editor state."""
try:
if not hasattr(self, 'collision_manager') or not self.collision_manager:
return None
collision_node = self.collision_manager.setupAdvancedCollision(
node,
shape_type=shape_type,
mask_type='MODEL_COLLISION',
**shape_kwargs,
)
if not collision_node:
return None
if position_offset is not None:
try:
collision_node.setPos(*position_offset)
except Exception:
pass
if visible is not None:
if visible:
collision_node.show()
else:
collision_node.hide()
return collision_node
except Exception as e:
print(f"创建碰撞节点失败: {e}")
return None
def _rebuild_collision_node(self, node, shape_type, position_offset=None, visible=None, **shape_kwargs):
"""Recreate collision geometry while preserving offset/visibility by default."""
try:
had_collision = self._has_collision(node)
if position_offset is None and had_collision:
position_offset = self._get_collision_position_offset(node)
if visible is None and had_collision:
visible = self._is_collision_visible(node)
self._remove_collision_from_node(node)
return self._build_collision_node(
node,
shape_type,
position_offset=position_offset,
visible=visible,
**shape_kwargs,
)
except Exception as e:
print(f"重建碰撞节点失败: {e}")
return None
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._rebuild_collision_node(node, 'sphere', 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._rebuild_collision_node(
node,
'box',
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._rebuild_collision_node(node, 'capsule', 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._rebuild_collision_node(node, 'capsule', 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._rebuild_collision_node(node, 'plane', 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 _capture_collision_snapshot(self, node):
"""Capture collision editor state for undo/redo."""
snapshot = {
"has_collision": False,
"shape_name": getattr(self.app, "_selected_collision_shape", "球形 (Sphere)"),
}
try:
if not node or node.isEmpty() or not self._has_collision(node):
return snapshot
shape_name = self._get_current_collision_shape(node)
shape_type = self._get_current_collision_shape_type(node)
snapshot.update({
"has_collision": True,
"shape_name": shape_name,
"shape_type": shape_type,
"position_offset": tuple(float(v) for v in self._get_collision_position_offset(node)),
"visible": bool(self._is_collision_visible(node)),
})
if shape_type == "sphere":
snapshot["radius"] = float(self._get_sphere_radius(node))
elif shape_type == "box":
snapshot["size"] = tuple(float(v) for v in self._get_box_size(node))
elif shape_type == "capsule":
snapshot["radius"] = float(self._get_capsule_radius(node))
snapshot["height"] = float(self._get_capsule_height(node))
elif shape_type == "plane":
snapshot["normal"] = tuple(float(v) for v in self._get_plane_normal(node))
return snapshot
except Exception as e:
print(f"捕获碰撞快照失败: {e}")
return snapshot
def _collision_snapshots_equal(self, before_snapshot, after_snapshot):
"""Compare collision snapshots with float tolerance."""
before_snapshot = before_snapshot or {}
after_snapshot = after_snapshot or {}
if bool(before_snapshot.get("has_collision")) != bool(after_snapshot.get("has_collision")):
return False
if before_snapshot.get("shape_name") != after_snapshot.get("shape_name"):
return False
if before_snapshot.get("shape_type") != after_snapshot.get("shape_type"):
return False
if bool(before_snapshot.get("visible", False)) != bool(after_snapshot.get("visible", False)):
return False
def _rounded(values):
if values is None:
return None
if isinstance(values, (tuple, list)):
return tuple(round(float(value), 6) for value in values)
return round(float(values), 6)
for key in ("position_offset", "size", "normal", "radius", "height"):
if _rounded(before_snapshot.get(key)) != _rounded(after_snapshot.get(key)):
return False
return True
def _apply_collision_snapshot(self, node, snapshot):
"""Apply collision state back to a node."""
try:
if not node or node.isEmpty():
return
snapshot = snapshot or {}
shape_name = snapshot.get("shape_name")
if shape_name:
self.app._selected_collision_shape = shape_name
self._remove_collision_from_node(node)
if not snapshot.get("has_collision"):
return
shape_type = snapshot.get("shape_type") or self._get_shape_type_from_name(shape_name or "球形 (Sphere)")
build_kwargs = {}
if shape_type == "sphere":
build_kwargs["radius"] = float(snapshot.get("radius", 1.0))
elif shape_type == "box":
size = snapshot.get("size") or (1.0, 1.0, 1.0)
build_kwargs.update({
"width": float(size[0]),
"length": float(size[1]),
"height": float(size[2]),
})
elif shape_type == "capsule":
build_kwargs.update({
"radius": float(snapshot.get("radius", 1.0)),
"height": float(snapshot.get("height", 2.0)),
})
elif shape_type == "plane":
from panda3d.core import Vec3
normal = snapshot.get("normal") or (0.0, 0.0, 1.0)
build_kwargs["normal"] = Vec3(*normal)
collision_node = self._build_collision_node(
node,
shape_type,
position_offset=snapshot.get("position_offset"),
visible=bool(snapshot.get("visible", False)),
**build_kwargs,
)
if collision_node and snapshot.get("visible", False):
collision_node.show()
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_light_color(self, node):
"""Return editable light color."""
try:
stored_color = node.getPythonTag("editor_light_color") if hasattr(node, "getPythonTag") else None
if stored_color and len(stored_color) >= 3:
return (
float(stored_color[0]),
float(stored_color[1]),
float(stored_color[2]),
float(stored_color[3]) if len(stored_color) > 3 else 1.0,
)
if hasattr(node, "hasColor") and node.hasColor():
color = node.getColor()
if color and len(color) >= 3:
return (
float(color[0]),
float(color[1]),
float(color[2]),
float(color[3]) if len(color) > 3 else 1.0,
)
rp_light = node.getPythonTag("rp_light_object")
if rp_light is not None:
try:
light_color = rp_light.get_color() if hasattr(rp_light, "get_color") else getattr(rp_light, "color", None)
if light_color is not None:
return (
float(light_color.x),
float(light_color.y),
float(light_color.z),
1.0,
)
except Exception:
pass
except Exception:
return (1.0, 1.0, 1.0, 1.0)
def _apply_light_color(self, node, color):
"""Apply light color to both editor node and RP light object."""
try:
rgba = (
float(color[0]),
float(color[1]),
float(color[2]),
float(color[3]) if len(color) > 3 else 1.0,
)
node.setPythonTag("editor_light_color", rgba)
node.setColor(*rgba)
except Exception:
pass
try:
rp_light = node.getPythonTag("rp_light_object")
if rp_light is not None:
rgb = rgba[:3]
if hasattr(rp_light, "set_color"):
rp_light.set_color(*rgb)
elif hasattr(rp_light, "setColor"):
rp_light.setColor(*rgb)
elif hasattr(rp_light, "color"):
rp_light.color = rgb
if hasattr(rp_light, "set_needs_update"):
rp_light.set_needs_update(True)
except Exception as e:
print(f"应用光源颜色失败: {e}")
def _capture_light_snapshot(self, node):
return {"color": tuple(float(v) for v in self._get_light_color(node))}
def _light_snapshots_equal(self, before_snapshot, after_snapshot):
before_color = tuple(round(float(v), 6) for v in (before_snapshot or {}).get("color", (1.0, 1.0, 1.0, 1.0)))
after_color = tuple(round(float(v), 6) for v in (after_snapshot or {}).get("color", (1.0, 1.0, 1.0, 1.0)))
return before_color == after_color
def _apply_light_snapshot(self, node, snapshot):
color = (snapshot or {}).get("color")
if color is not None:
self._apply_light_color(node, color)
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 _capture_node_material_snapshot(self, node):
"""Capture editable material state for a node."""
materials = self._get_node_materials(node)
if not materials:
fallback_material = self._ensure_material_for_node(node)
materials = [fallback_material] if fallback_material else []
material_entries = []
for material in materials:
emission = None
try:
if hasattr(material, "emission") and material.emission is not None:
emission = (
float(material.emission.x),
float(material.emission.y),
float(material.emission.z),
float(material.emission.w),
)
except Exception:
emission = None
entry = {
"material": material,
"base_color": tuple(float(v) for v in self._get_material_base_color(material)),
"roughness": float(material.roughness) if hasattr(material, "roughness") and material.roughness is not None else None,
"metallic": float(material.metallic) if hasattr(material, "metallic") and material.metallic is not None else None,
"ior": float(material.refractive_index) if hasattr(material, "refractive_index") and material.refractive_index is not None else None,
"emission": emission,
}
material_entries.append(entry)
texture_tags = {}
for texture_type in self._get_material_texture_slots().keys():
tag_name = f"material_texture_{texture_type}"
if node and hasattr(node, "hasTag") and node.hasTag(tag_name):
texture_tags[texture_type] = node.getTag(tag_name)
# Fallback: some runtime nodes inherit textures from parent/root state and
# therefore have no explicit material_texture_* tags on the selected node.
# Capture the actual texture bindings as stable paths so save/load can
# persist them back into the source scene tree.
if node and not node.isEmpty():
texture_slots = self._get_material_texture_slots()
slot_to_type = {slot: tex_type for tex_type, slot in texture_slots.items()}
def record_texture_binding(texture_type, texture):
if not texture_type or texture_type in texture_tags or not texture:
return
resolved_path = ""
try:
if texture.hasFullpath():
fullpath = texture.getFullpath()
try:
resolved_path = fullpath.toOsSpecific()
except Exception:
resolved_path = str(fullpath)
except Exception:
resolved_path = ""
resolved_path = os.path.normpath(str(resolved_path).strip()) if resolved_path else ""
if not resolved_path or not os.path.exists(resolved_path):
try:
texture_name = os.path.normpath(str(texture.getName() or "").strip())
except Exception:
texture_name = ""
if texture_name and os.path.exists(texture_name):
resolved_path = texture_name
if resolved_path:
texture_tags[texture_type] = resolved_path
def infer_texture_type(stage):
if not stage:
return None
try:
stage_name = (stage.getName() or "").strip().lower()
except Exception:
stage_name = ""
if stage_name.endswith("_map"):
inferred_type = stage_name[:-4]
if inferred_type in texture_slots:
return inferred_type
try:
return slot_to_type.get(int(stage.getSort()))
except Exception:
return None
try:
texture_stages = node.findAllTextureStages()
except Exception:
texture_stages = None
if texture_stages:
for stage_index in range(texture_stages.getNumTextureStages()):
try:
stage = texture_stages.getTextureStage(stage_index)
except Exception:
continue
texture_type = infer_texture_type(stage)
try:
texture = node.getTexture(stage)
except Exception:
texture = None
record_texture_binding(texture_type, texture)
# Some imported/runtime nodes keep effective textures only in the
# inherited RenderState. Read TextureAttrib from net state as a
# second fallback so SSBO save can persist those bindings too.
if len(texture_tags) < len(texture_slots):
try:
from panda3d.core import TextureAttrib
net_state = node.getNetState()
if net_state.hasAttrib(TextureAttrib.getClassType()):
texture_attrib = net_state.getAttrib(TextureAttrib.getClassType())
if texture_attrib:
try:
num_on = texture_attrib.getNumOnStages()
get_stage = texture_attrib.getOnStage
get_texture = texture_attrib.getOnTexture
except Exception:
num_on = 0
get_stage = None
get_texture = None
for stage_index in range(int(num_on or 0)):
if get_stage is None or get_texture is None:
break
try:
stage = get_stage(stage_index)
texture = get_texture(stage)
except Exception:
continue
texture_type = infer_texture_type(stage)
record_texture_binding(texture_type, texture)
except Exception:
pass
effect_tags = {}
for tag_name in (
"material_effect_metallic_enabled",
"material_effect_default_texture_enabled",
"material_effect_parallax_enabled",
):
effect_tags[tag_name] = bool(node and hasattr(node, "hasTag") and node.hasTag(tag_name))
if texture_tags:
if texture_tags.get("metallic"):
effect_tags["material_effect_metallic_enabled"] = True
if texture_tags.get("parallax"):
effect_tags["material_effect_parallax_enabled"] = True
if any(texture_path for texture_path in texture_tags.values()):
effect_tags["material_effect_default_texture_enabled"] = True
return {
"materials": material_entries,
"node_state": {
"textures": texture_tags,
"effect_tags": effect_tags,
},
}
def _normalize_material_snapshot(self, snapshot):
if isinstance(snapshot, list):
return {"materials": snapshot, "node_state": {"textures": {}, "effect_tags": {}}}
if isinstance(snapshot, dict):
return {
"materials": snapshot.get("materials", []) or [],
"node_state": snapshot.get("node_state", {}) or {"textures": {}, "effect_tags": {}},
}
return {"materials": [], "node_state": {"textures": {}, "effect_tags": {}}}
def _material_snapshots_equal(self, before_snapshot, after_snapshot):
"""Compare two material snapshots with a small float tolerance."""
before_snapshot = self._normalize_material_snapshot(before_snapshot)
after_snapshot = self._normalize_material_snapshot(after_snapshot)
before_materials = before_snapshot.get("materials", [])
after_materials = after_snapshot.get("materials", [])
if len(before_materials) != len(after_materials):
return False
def _rounded_tuple(values):
if values is None:
return None
return tuple(round(float(value), 6) for value in values)
for before_entry, after_entry in zip(before_materials, after_materials):
before_material = before_entry.get("material")
after_material = after_entry.get("material")
before_key = getattr(before_material, "this", None) or id(before_material)
after_key = getattr(after_material, "this", None) or id(after_material)
if before_key != after_key:
return False
if _rounded_tuple(before_entry.get("base_color")) != _rounded_tuple(after_entry.get("base_color")):
return False
if _rounded_tuple(before_entry.get("emission")) != _rounded_tuple(after_entry.get("emission")):
return False
for field_name in ("roughness", "metallic", "ior"):
before_value = before_entry.get(field_name)
after_value = after_entry.get(field_name)
if before_value is None or after_value is None:
if before_value != after_value:
return False
continue
if round(float(before_value), 6) != round(float(after_value), 6):
return False
before_node_state = before_snapshot.get("node_state", {}) or {}
after_node_state = after_snapshot.get("node_state", {}) or {}
if (before_node_state.get("textures", {}) or {}) != (after_node_state.get("textures", {}) or {}):
return False
if (before_node_state.get("effect_tags", {}) or {}) != (after_node_state.get("effect_tags", {}) or {}):
return False
return True
def _apply_node_material_snapshot(self, node, snapshot):
"""Apply a captured material snapshot back onto a node."""
try:
from panda3d.core import Vec4
if not node or node.isEmpty():
return
snapshot = self._normalize_material_snapshot(snapshot)
materials_snapshot = snapshot.get("materials", [])
node_state = snapshot.get("node_state", {}) or {}
for entry in materials_snapshot:
material = entry.get("material")
if material is None:
continue
base_color = entry.get("base_color")
if base_color is not None:
self._set_material_base_color(material, tuple(base_color))
roughness = entry.get("roughness")
if roughness is not None and hasattr(material, "set_roughness"):
material.set_roughness(float(roughness))
metallic = entry.get("metallic")
if metallic is not None and hasattr(material, "set_metallic"):
material.set_metallic(float(metallic))
ior = entry.get("ior")
if ior is not None and hasattr(material, "set_refractive_index"):
material.set_refractive_index(float(ior))
emission = entry.get("emission")
if emission is not None and hasattr(material, "set_emission"):
material.set_emission(Vec4(*emission))
self._apply_material_to_geom_states(node, material)
self._apply_material_surface_state(node, material)
self._clear_all_textures(node)
texture_tags = node_state.get("textures", {}) or {}
primary_material = None
if materials_snapshot:
primary_material = materials_snapshot[0].get("material")
if primary_material is None:
primary_material = self._ensure_material_for_node(node)
texture_slots = self._get_material_texture_slots()
for texture_type, texture_path in sorted(texture_tags.items(), key=lambda item: texture_slots.get(item[0], 999)):
if texture_path:
self._apply_texture_to_material(node, primary_material, texture_type, texture_path)
for tag_name, enabled in (node_state.get("effect_tags", {}) or {}).items():
if enabled:
node.setTag(tag_name, "1")
elif node.hasTag(tag_name):
node.clearTag(tag_name)
for entry in materials_snapshot:
material = entry.get("material")
if material is not None:
self._refresh_pipeline_material_mode(node, material)
ssbo_editor = getattr(self.app, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
try:
ssbo_editor.sync_scene_nodes_to_pick([node])
except Exception:
pass
except Exception as e:
print(f"应用材质快照失败: {e}")
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
target_geom_paths = self._get_geom_paths_for_material(node, material)
if not target_geom_paths:
try:
if node.hasMaterial():
node.setMaterial(material, 1)
except Exception:
pass
try:
target_geom_paths = [geom_path for geom_path in node.find_all_matches("**/+GeomNode")]
except Exception:
target_geom_paths = []
if not target_geom_paths:
self._invalidate_material_render_cache()
return
for geom_path in target_geom_paths:
try:
geom_path.setMaterial(material, 1)
except Exception:
pass
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 = self._get_material_identity_key(material)
if key in seen_keys:
continue
seen_keys.add(key)
unique_materials.append(material)
return unique_materials
def _get_material_identity_key(self, material):
try:
return getattr(material, "this", None) or id(material)
except Exception:
return id(material)
def _geom_uses_material(self, geom_path, material):
try:
from panda3d.core import MaterialAttrib
target_key = self._get_material_identity_key(material)
if geom_path.hasMaterial():
geom_material = geom_path.getMaterial()
if self._get_material_identity_key(geom_material) == target_key:
return True
geom_node = geom_path.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)
geom_material = material_attrib.getMaterial() if material_attrib else None
if geom_material is not None and self._get_material_identity_key(geom_material) == target_key:
return True
try:
net_state = geom_path.getNetState()
if net_state.hasAttrib(MaterialAttrib):
material_attrib = net_state.getAttrib(MaterialAttrib)
geom_material = material_attrib.getMaterial() if material_attrib else None
if geom_material is not None and self._get_material_identity_key(geom_material) == target_key:
return True
except Exception:
pass
except Exception:
pass
return False
def _get_geom_paths_for_material(self, node, material):
try:
if not node or node.isEmpty() or material is None:
return []
return [
geom_path
for geom_path in node.find_all_matches("**/+GeomNode")
if self._geom_uses_material(geom_path, material)
]
except Exception:
return []
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:
# Entering transparent mode should never unexpectedly
# inherit stale low alpha from imported material data.
opacity = 1.0
material.set_emission(Vec4(float(surface_type), float(emission.y), float(emission.z), float(emission.w)))
self._set_material_opacity(node, material, opacity)
else:
opacity = 1.0
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w)))
base_color = list(self._get_material_base_color(material))
base_color[3] = 1.0
self._set_material_base_color(material, tuple(base_color))
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"):
current_opacity = self._get_material_opacity(material)
self._bake_effective_geom_materials(node)
split_changed = self._isolate_transparent_geoms(node)
if split_changed:
self._bake_effective_geom_materials(node)
self._apply_material_surface_state(node, material)
render_pipeline.prepare_scene(node)
self._set_material_opacity(node, material, current_opacity)
if split_changed:
self._apply_material_surface_state(node, material)
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")
if use_forward:
effect_path = "effects/simple_transparent.yaml"
options = {}
sort = 100
else:
effect_path = "effects/pbr_with_metallic.yaml" if use_metallic_effect else "effects/default.yaml"
options = {
"normal_mapping": True,
"render_gbuffer": True,
"render_forward": False,
"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.get("render_forward", False) else "0",
"1" if options.get("render_gbuffer", False) else "0",
"1" if options.get("parallax_mapping", False) 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 _get_material_transparent_base_color(self, material):
try:
base_color = list(self._get_material_base_color(material))
if len(base_color) < 4:
base_color = list(base_color[:3]) + [1.0]
return tuple(float(v) for v in base_color[:4])
except Exception:
return (1.0, 1.0, 1.0, 1.0)
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
is_transparent = self._get_material_surface_type(material) == 3
opacity = self._get_material_opacity(material) if is_transparent else 1.0
base_color = self._get_material_transparent_base_color(material)
target_nodes = self._get_geom_paths_for_material(node, material)
if not target_nodes:
target_nodes = self._iter_material_state_nodes(node)
for target_node in target_nodes:
target_material = material
if target_material is not None:
try:
target_node.setMaterial(target_material, 1)
except Exception:
pass
if is_transparent:
target_node.setTransparency(TransparencyAttrib.M_alpha)
target_node.setAlphaScale(opacity)
try:
target_node.setColorScale(1.0, 1.0, 1.0, opacity)
except Exception:
pass
try:
target_node.setBin("transparent", 0)
except Exception:
pass
try:
target_node.setShaderInput("material_base_color", base_color)
except Exception:
pass
try:
target_node.setShaderInput("material_opacity", float(opacity))
except Exception:
pass
target_node.setDepthWrite(False)
else:
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.clearColorScale()
except Exception:
pass
try:
target_node.clearShaderInput("material_base_color")
except Exception:
pass
try:
target_node.clearShaderInput("material_opacity")
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:
# 先清理贴图与effect标签避免后续再次设置贴图时被旧状态污染
try:
self._clear_all_textures(node)
except Exception:
pass
try:
if node.hasTag("material_render_effect_signature"):
node.clearTag("material_render_effect_signature")
except Exception:
pass
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)
return bool(self._apply_texture_to_material(node, material, texture_type, selected_path))
else:
print(f"已取消选择{texture_type}贴图")
return False
except Exception as e:
print(f"选择纹理失败: {e}")
return False
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:
stable_texture_path = os.path.normpath(os.path.abspath(texture_path))
node.setTag(f"material_texture_{texture_type}", stable_texture_path)
print(f"已应用{texture_type}纹理到槽位 p3d_Texture{slot}: {texture_path}")
return True
except Exception as e:
print(f"应用纹理失败: {e}")
return False
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("已清除所有纹理")
return True
except Exception as e:
print(f"清除纹理失败: {e}")
return False
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 is not None and (not node.isEmpty()) and node.hasParent():
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 is not None and (not self._monitored_node.isEmpty()) and self._monitored_node.hasParent():
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 self._monitored_node is None:
return
if self._monitored_node.isEmpty() or (not self._monitored_node.hasParent()):
self.stop_transform_monitoring()
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)