Compare commits

...

2 Commits

Author SHA1 Message Date
ayuan9957
11ad2d5742 清理qt和旧版gizmo 2026-03-15 17:23:37 +08:00
ayuan9957
49d630f967 Fix transparency shader responses 2026-03-15 16:19:32 +08:00
27 changed files with 1759 additions and 1123 deletions

View File

@ -0,0 +1,11 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "EG"
[setup]
script = ""
[[actions]]
name = "go"
icon = "tool"
command = "python ./main.py"

View File

@ -10,17 +10,21 @@ vertex_shader: |
texcoord = p3d_MultiTexCoord0;
}
fragment_shader: |
#version 330
uniform sampler2D p3d_Texture0;
uniform float material_opacity = 1.0;
in vec2 texcoord;
out vec4 o_color;
void main() {
vec4 c = texture(p3d_Texture0, texcoord);
o_color = vec4(c.rgb, c.a * material_opacity);
}
render_states:
TransparencyAttrib: M_alpha
DepthWriteAttrib: 0
fragment_shader: |
#version 330
uniform sampler2D p3d_Texture0;
uniform vec4 material_base_color = vec4(1.0, 1.0, 1.0, 1.0);
uniform float material_opacity = 1.0;
in vec2 texcoord;
out vec4 o_color;
void main() {
vec4 c = texture(p3d_Texture0, texcoord);
o_color = vec4(
material_base_color.rgb * c.rgb,
material_base_color.a * c.a * material_opacity
);
}
render_states:
TransparencyAttrib: M_alpha
DepthWriteAttrib: 0

View File

@ -45,6 +45,7 @@
layout(location = 0) in VertexOutput vOutput;
uniform Panda3DMaterial p3d_Material;
uniform vec4 p3d_ColorScale;
#pragma include "includes/normal_mapping.inc.glsl"
#pragma include "includes/forward_shading.inc.glsl"
@ -141,9 +142,9 @@ void main() {
m.shading_model = mInput.shading_model;
#if DONT_FETCH_DEFAULT_TEXTURES
m.basecolor = mInput.color;
m.basecolor = mInput.color * p3d_ColorScale.xyz;
#else
m.basecolor = mInput.color * sampled_diffuse.xyz;
m.basecolor = mInput.color * sampled_diffuse.xyz * p3d_ColorScale.xyz;
#endif
m.normal = material_nrm;
m.metallic = mInput.metallic;
@ -160,7 +161,7 @@ void main() {
vec3 view_dir = normalize(m_out.position - MainSceneData.camera_pos);
vec3 color = vec3(0);
float alpha = m_out.shading_model_param0;
float alpha = m_out.shading_model_param0 * p3d_ColorScale.w;
AmbientResult ambient = get_full_forward_ambient(m_out, view_dir);
color += ambient.diffuse;

View File

@ -469,6 +469,129 @@ class TransformGizmo(DirectObject):
# New user action invalidates redo chain.
self._redo_history.clear()
def _coerce_vec3(self, value, fallback) -> p3d.Vec3:
if value is None:
return p3d.Vec3(fallback)
try:
return p3d.Vec3(value)
except Exception:
pass
if isinstance(value, (tuple, list)) and len(value) >= 3:
return p3d.Vec3(value[0], value[1], value[2])
return p3d.Vec3(fallback)
def _make_transform_mat(self, pos, hpr, scale) -> p3d.LMatrix4f:
try:
state = p3d.TransformState.make_pos_hpr_scale(pos, hpr, scale)
return p3d.LMatrix4f(state.get_mat())
except Exception:
pass
try:
state = p3d.TransformState.makePosHprScale(pos, hpr, scale)
return p3d.LMatrix4f(state.getMat())
except Exception:
temp = NodePath("tg_temp_transform")
temp.setPos(pos)
temp.setHpr(hpr)
temp.setScale(scale)
return p3d.LMatrix4f(temp.getMat())
def _invert_matrix(self, mat) -> Optional[p3d.LMatrix4f]:
inv = p3d.LMatrix4f(mat)
try:
inv.invertInPlace()
return inv
except Exception:
pass
try:
inv.invert_in_place()
return inv
except Exception:
return None
def _build_ssbo_group_snapshot_command(self, action: Dict[str, Any]):
node: NodePath = action.get("node")
if node is None or node.isEmpty() or (not node.hasTag("is_ssbo_proxy")):
return None
ssbo_editor = getattr(self.world, "ssbo_editor", None)
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
if not controller:
return None
selection_key = node.getTag("ssbo_selection_key") if node.hasTag("ssbo_selection_key") else None
selected_ids = list(controller.name_to_ids.get(selection_key, [])) if selection_key else []
if not selected_ids:
selected_ids = list(getattr(ssbo_editor, "selected_ids", []) or [])
targets = []
for gid in selected_ids:
obj_np = controller.id_to_object_np.get(gid)
if obj_np and not obj_np.is_empty():
targets.append(obj_np)
if not targets:
return None
current_pos = node.getPos(self.world.render)
current_hpr = node.getHpr(self.world.render)
current_scale = node.getScale(self.world.render)
old_pos = self._coerce_vec3(action.get("old_pos"), current_pos)
new_pos = self._coerce_vec3(action.get("new_pos"), current_pos)
old_hpr = self._coerce_vec3(action.get("old_hpr"), current_hpr)
new_hpr = self._coerce_vec3(action.get("new_hpr"), current_hpr)
old_scale = self._coerce_vec3(action.get("old_scale"), current_scale)
new_scale = self._coerce_vec3(action.get("new_scale"), current_scale)
old_proxy_mat = self._make_transform_mat(old_pos, old_hpr, old_scale)
new_proxy_mat = self._make_transform_mat(new_pos, new_hpr, new_scale)
new_proxy_inv = self._invert_matrix(new_proxy_mat)
if new_proxy_inv is None:
return None
before_state = []
after_state = []
for target in targets:
try:
current_world_mat = p3d.LMatrix4f(target.get_mat(self.world.render))
except Exception:
try:
current_world_mat = p3d.LMatrix4f(target.getMat(self.world.render))
except Exception:
continue
old_world_mat = p3d.LMatrix4f(current_world_mat * new_proxy_inv * old_proxy_mat)
before_state.append({"node": target, "mat": old_world_mat})
after_state.append({"node": target, "mat": current_world_mat})
if not before_state:
return None
def apply_state(state):
synced_nodes = []
for item in state:
target = item.get("node")
mat = item.get("mat")
if target is None or target.isEmpty() or mat is None:
continue
try:
target.set_mat(self.world.render, mat)
except Exception:
try:
target.setMat(self.world.render, mat)
except Exception:
continue
synced_nodes.append(target)
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
try:
ssbo_editor.sync_scene_nodes_to_pick(synced_nodes)
except Exception:
pass
from core.Command_System import SnapshotStateCommand
return SnapshotStateCommand(apply_state, before_state, after_state)
def _record_action_with_command_manager(self, action: Dict[str, Any]) -> bool:
"""Prefer routing transform actions into the global command manager."""
command_manager = getattr(self.world, "command_manager", None)
@ -476,6 +599,11 @@ class TransformGizmo(DirectObject):
return False
try:
group_command = self._build_ssbo_group_snapshot_command(action)
if group_command is not None:
command_manager.execute_command(group_command)
return True
from core.Command_System import MoveNodeCommand, RotateNodeCommand, ScaleNodeCommand
kind = action.get("kind")
@ -490,6 +618,7 @@ class TransformGizmo(DirectObject):
action.get("old_pos"),
action.get("new_pos"),
reference_node=self.world.render,
world=self.world,
)
elif kind == "rotate":
command = RotateNodeCommand(
@ -497,12 +626,14 @@ class TransformGizmo(DirectObject):
action.get("old_hpr"),
action.get("new_hpr"),
reference_node=self.world.render,
world=self.world,
)
elif kind == "scale":
command = ScaleNodeCommand(
node,
action.get("old_scale"),
action.get("new_scale"),
world=self.world,
)
if command is None:
@ -513,6 +644,7 @@ class TransformGizmo(DirectObject):
except Exception:
return False
def _sync_light_position_if_needed(self, node: Optional[NodePath]) -> None:
"""When target node wraps an RP light, keep RP light position in sync."""
try:

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
"""Editor context adapter for legacy bridge access."""
"""Editor context adapter for the ImGui editor runtime."""
class EditorContext:
@ -7,14 +7,8 @@ class EditorContext:
def __init__(self, world):
self.world = world
def get_interface_manager(self):
return getattr(self.world, "interface_manager", None)
def get_tree_widget(self):
interface_manager = self.get_interface_manager()
if not interface_manager:
return None
return getattr(interface_manager, "treeWidget", None)
return None
def get_gui_manager(self):
return getattr(self.world, "gui_manager", None)

View File

@ -22,12 +22,8 @@ class EventHandler:
self._editor_context = get_editor_context(self.world)
return self._editor_context
def _get_interface_manager(self):
"""获取兼容层中的场景树管理器(若存在)。"""
return self._get_editor_context().get_interface_manager()
def _get_tree_widget(self):
"""安全获取场景树控件。"""
"""Qt tree has been removed in the ImGui editor."""
return self._get_editor_context().get_tree_widget()
def _get_gui_manager(self):
@ -35,29 +31,7 @@ class EventHandler:
return self._get_editor_context().get_gui_manager()
def _sync_tree_selection(self, selected_model):
"""将当前选中模型同步到场景树。"""
interface_manager = self._get_interface_manager()
tree_widget = self._get_tree_widget()
if not interface_manager or not tree_widget:
return False
root = tree_widget.invisibleRootItem()
found_item = None
for i in range(root.childCount()):
scene_item = root.child(i)
if scene_item.text(0) != "场景":
continue
found_item = interface_manager.findTreeItem(selected_model, scene_item)
if found_item:
try:
tree_widget.itemClicked.disconnect()
except TypeError:
pass
tree_widget.setCurrentItem(found_item)
tree_widget.itemClicked.connect(interface_manager.onTreeItemClicked)
return True
break
"""Scene tree selection is rendered directly from editor state in ImGui."""
return False
def showClickRay(self, nearPoint, farPoint, hitPos=None):
@ -236,73 +210,10 @@ class EventHandler:
pickerNP.removeNode()
return
# 优先检查是否点击了坐标轴
#print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
if self.world.selection.hasLegacyGizmoInput():
#print("准备检查坐标轴点击...")
try:
highlighted_axis = self.world.selection.gizmoHighlightAxis
if highlighted_axis:
print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽")
# 直接使用高亮轴开始拖拽
self.world.selection.startGizmoDrag(highlighted_axis, x, y)
pickerNP.removeNode()
return
# 如果没有高亮轴,再尝试检测点击
gizmoAxis = self.world.selection.checkGizmoClick(x, y)
if gizmoAxis:
print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
# 开始坐标轴拖拽
self.world.selection.startGizmoDrag(gizmoAxis, x, y)
pickerNP.removeNode()
return
else:
print("× 没有点击到坐标轴")
# gizmoAxis = self.world.selection.checkGizmoClick(x, y)
# if gizmoAxis:
# #print(f"✓ 检测到坐标轴点击: {gizmoAxis}")
# # 开始坐标轴拖拽
# self.world.selection.startGizmoDrag(gizmoAxis, x, y)
# pickerNP.removeNode()
# return
# else:
# print("× 没有点击到坐标轴")
except Exception as e:
print(f"❌ 坐标轴点击检测出现异常: {str(e)}")
import traceback
traceback.print_exc()
print("继续处理模型选择...")
#print("继续处理碰撞结果...")
if hitPos and hitNode:
#print(f"✓ 检测到碰撞,开始处理点击事件")
#print(f"GUI编辑模式: {self.world.guiEditMode}")
#print(f"当前工具: {self.world.currentTool}")
# 处理GUI编辑模式
if self.world.guiEditMode:
gui_manager = self._get_gui_manager()
if not gui_manager:
print("GUI编辑模式已开启但 gui_manager 不可用")
pickerNP.removeNode()
return
#print("处理GUI编辑模式点击")
# 检查是否点击了GUI元素
clickedGUI = gui_manager.findClickedGUI(hitNode)
if clickedGUI:
# 选中GUI元素
self.world.selection.updateSelection(clickedGUI)
gui_manager.selectGUIInTree(clickedGUI)
print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}")
elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool:
# 在点击位置创建新GUI元素
gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool)
pickerNP.removeNode()
return
# 根据当前工具处理点击事件
if self.world.currentTool in ("选择", "移动", "旋转", "缩放"):
print("✓ 使用选择工具处理点击")
@ -317,22 +228,7 @@ class EventHandler:
print(f"当前工具不是'选择',无法处理: {self.world.currentTool}")
else:
print("没有检测到碰撞")
# 如果不在GUI编辑模式清除选择
if not self.world.guiEditMode:
self.world.selection.updateSelection(None)
# 在GUI编辑模式下即使没有碰撞也可以在空白区域创建GUI
if (self.world.guiEditMode and
hasattr(self.world, 'currentGUITool') and
self.world.currentGUITool):
# 使用默认的地面高度创建GUI
default_height = 0.0
world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换
world_pos.setZ(default_height)
gui_manager = self._get_gui_manager()
if gui_manager:
gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool)
self.world.selection.updateSelection(None)
# 确保总是清理碰撞检测节点
try:
@ -552,10 +448,7 @@ class EventHandler:
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""
# 处理坐标轴拖拽结束
if self.world.selection.isLegacyGizmoDragActive():
self.world.selection.stopGizmoDrag()
return
return
def wheelForward(self, data=None):
"""处理滚轮向前滚动(前进)"""
@ -597,30 +490,6 @@ class EventHandler:
"""处理鼠标移动事件"""
if not evt:
return
# 处理坐标轴拖拽
if self.world.selection.isLegacyGizmoDragActive():
x = evt.get('x', 0)
y = evt.get('y', 0)
# 获取准确的窗口尺寸
winWidth, winHeight = self.world.getWindowSize()
# 将屏幕坐标转换为世界坐标
mx = 2.0 * x / float(winWidth) - 1.0
my = 1.0 - 2.0 * y / float(winHeight)
# 更新坐标轴拖拽
self.world.selection.updateGizmoDrag(x, y)
return
# 更新坐标轴高亮(鼠标悬停效果)
if self.world.selection.hasLegacyGizmoInput() and not self.world.selection.isLegacyGizmoDragActive():
x = evt.get('x', 0)
y = evt.get('y', 0)
# 减少高亮调试输出,只在需要时输出
# 已静默处理,避免控制台刷屏
self.world.selection.updateGizmoHighlight(x, y)
# 调用CoreWorld的父类方法处理基础的相机旋转
super(type(self.world), self.world).mouseMoveEvent(evt)

View File

@ -24,6 +24,8 @@ class ImGuiStyleManager:
self.world = world
self.io = imgui_backend.io
self.style = None # 延迟初始化在apply_style中设置
self._icon_cache = {}
self._missing_icons = set()
# 颜色定义 - 与Qt UI保持一致
self.colors = {
@ -403,24 +405,40 @@ class ImGuiStyleManager:
def load_icon(self, icon_name):
"""加载图标纹理为ImGui可用的格式"""
if icon_name in self._icon_cache:
return self._icon_cache[icon_name]
if icon_name in self._missing_icons:
return None
try:
# 构建图标路径
project_root = Path(__file__).resolve().parent.parent
icon_path = project_root / "icons" / f"{icon_name}.png"
if icon_path.exists():
# 使用base.imgui.loadTexture方法
if hasattr(base, 'imgui') and hasattr(base.imgui, 'loadTexture'):
# 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
# 注意: p3dimgui.loadTexture 仅支持 str 或 Texture不支持 Filename 对象
fn = Filename.fromOsSpecific(str(icon_path))
return base.imgui.loadTexture(fn.getFullpath())
else:
print(f"⚠ ImGui后端未初始化")
return None
else:
if not icon_path.exists():
print(f"⚠ 图标文件不存在: {icon_path}")
self._missing_icons.add(icon_name)
return None
base_app = getattr(self.world, "base", None) if self.world else None
if base_app is None:
try:
from direct.showbase.ShowBaseGlobal import base as base_app
except Exception:
base_app = None
imgui_runtime = getattr(base_app, "imgui", None) if base_app else None
texture_loader = getattr(imgui_runtime, "loadTexture", None)
if not callable(texture_loader):
print("⚠ ImGui后端未初始化")
return None
# 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
# 注意: p3dimgui.loadTexture 仅支持 str 或 Texture不支持 Filename 对象
fn = Filename.fromOsSpecific(str(icon_path))
texture = texture_loader(fn.getFullpath())
self._icon_cache[icon_name] = texture
return texture
except Exception as e:
print(f"⚠ 加载图标失败: {e}")
return None
@ -428,6 +446,72 @@ class ImGuiStyleManager:
def image_button(self, texture_id, size=(32, 32), bg_col=(0, 0, 0, 0), tint_col=(1, 1, 1, 1)):
"""绘制图像按钮"""
return imgui.image_button(texture_id, size, bg_col, tint_col)
def draw_stat_chip(self, label, tint=None, text_color=None):
"""绘制不可交互的状态胶囊。"""
if tint is None:
tint = self.colors['button_bg']
if text_color is None:
text_color = self.colors['text']
text_size = imgui.calc_text_size(label)
horizontal_padding = 8.0
vertical_padding = 4.0
chip_width = float(text_size.x) + horizontal_padding * 2.0
chip_height = float(text_size.y) + vertical_padding * 2.0
cursor_pos = imgui.get_cursor_screen_pos()
draw_list = imgui.get_window_draw_list()
bg_color = imgui.color_convert_float4_to_u32(tint)
fg_color = imgui.color_convert_float4_to_u32(text_color)
draw_list.add_rect_filled(
cursor_pos,
(cursor_pos.x + chip_width, cursor_pos.y + chip_height),
bg_color,
12.0,
)
draw_list.add_text(
(cursor_pos.x + horizontal_padding, cursor_pos.y + vertical_padding),
fg_color,
label,
)
imgui.dummy((chip_width, chip_height))
def draw_toolbar_button(self, label, active=False, size=(56, 28), tooltip=None, enabled=True):
"""绘制统一风格的工具栏按钮。"""
if active:
button_color = self.colors['primary']
hovered_color = self.colors['primary_dark']
text_color = (1.0, 1.0, 1.0, 1.0)
border_color = self.colors['primary_dark']
else:
button_color = self.colors['button_bg']
hovered_color = self.colors['panel_bg']
text_color = self.colors['text']
border_color = self.colors['border_secondary']
imgui.push_style_color(imgui.Col_.button, button_color)
imgui.push_style_color(imgui.Col_.button_hovered, hovered_color)
imgui.push_style_color(imgui.Col_.button_active, hovered_color)
imgui.push_style_color(imgui.Col_.text, text_color)
imgui.push_style_color(imgui.Col_.border, border_color)
imgui.push_style_var(imgui.StyleVar_.frame_rounding, 8.0)
imgui.push_style_var(imgui.StyleVar_.frame_border_size, 1.0)
imgui.push_style_var(imgui.StyleVar_.frame_padding, (10.0, 6.0))
if not enabled:
imgui.begin_disabled()
clicked = imgui.button(label, size)
if not enabled:
imgui.end_disabled()
if tooltip and imgui.is_item_hovered():
imgui.set_tooltip(tooltip)
imgui.pop_style_var(3)
imgui.pop_style_color(5)
return clicked
def get_icon_text_button(self, icon_texture, text, size=(0, 0)):
"""绘制带图标的文本按钮"""

View File

@ -93,6 +93,33 @@ class SelectionSystem:
print("✓ 选择和变换系统初始化完成")
def _is_valid_node(self, node, require_attached=False):
if node is None:
return False
try:
if node.isEmpty():
return False
except Exception:
return False
if require_attached:
try:
return bool(node.hasParent())
except Exception:
return False
return True
def _same_valid_node(self, left, right):
if left is None and right is None:
return True
if (left is None) != (right is None):
return False
if not self._is_valid_node(left) or not self._is_valid_node(right):
return False
try:
return left == right
except Exception:
return False
def _get_tree_widget(self):
"""统一获取场景树控件。"""
return self._editor_context.get_tree_widget()
@ -150,53 +177,28 @@ class SelectionSystem:
return self.gizmoTarget == nodePath
def _has_legacy_gizmo_input(self):
"""Return whether legacy gizmo hit-testing/dragging is active."""
if self._has_new_transform_gizmo():
return False
if not self.gizmo or not self.gizmoTarget:
return False
try:
return not self.gizmo.isEmpty() and not self.gizmoTarget.isEmpty()
except Exception:
return False
def hasLegacyGizmoInput(self):
"""Compatibility helper for event routing."""
return self._has_legacy_gizmo_input()
def isLegacyGizmoDragActive(self):
"""Return whether the legacy gizmo drag loop should keep consuming mouse input."""
return self.isDraggingGizmo and self._has_legacy_gizmo_input()
"""Legacy gizmo path is retired."""
return False
def _get_effective_selected_node(self):
"""Resolve the current editor selection across legacy and SSBO selection sources."""
"""Resolve the current editor selection across scene and SSBO sources."""
ssbo_editor = getattr(self.world, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection"):
try:
if ssbo_editor.has_active_selection():
node = ssbo_editor.get_selection_scene_node()
if node and not node.isEmpty():
if self._is_valid_node(node, require_attached=True):
return node
return None
except Exception:
pass
resolver = getattr(self.world, "_get_selection_node", None)
if callable(resolver):
try:
node = resolver()
if node and not node.isEmpty():
return node
except Exception:
pass
node = self.selectedNode
if not node:
if node is None:
return None
try:
return None if node.isEmpty() else node
except Exception:
if not self._is_valid_node(node, require_attached=True):
return None
return node
def _sync_rp_light_position(self, light_node, light_object=None):
"""同步灯光包装节点与 RenderPipeline 灯光对象位置。"""
@ -494,91 +496,19 @@ class SelectionSystem:
# ==================== 坐标轴(Gizmo)系统 ====================
def createGizmo(self, nodePath):
"""为选中的节点创建坐标轴工具 - 保留箭头版本"""
if self._has_new_transform_gizmo():
self.gizmo = None
self.gizmoTarget = nodePath
if not nodePath:
return
self.sync_transform_gizmo_mode()
try:
self.world.newTransform.attach(nodePath)
except Exception as e:
print(f"attach TransformGizmo failed: {e}")
"""Attach the unified TransformGizmo to the selected node."""
self.gizmo = None
self.gizmoTarget = nodePath
if not nodePath or not self._has_new_transform_gizmo():
return
self.sync_transform_gizmo_mode()
try:
#print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}")
# 如果已有坐标轴,先移除
if self.gizmo:
self.gizmo.removeNode()
self.gizmo = None
taskMgr.remove("updateGizmo")
if not nodePath:
return
# 创建坐标轴主节点
self.gizmo = self.world.render.attachNewNode("gizmo")
self.gizmoTarget = nodePath
# 添加标识标签,便于识别
self.gizmo.setTag("is_gizmo", "1")
if hasattr(nodePath, 'getName'):
self.gizmo.setTag("gizmo_target", nodePath.getName())
# 为各轴添加标签
if hasattr(self, 'gizmoXAxis') and self.gizmoXAxis:
self.gizmoXAxis.setTag("is_gizmo", "1")
self.gizmoXAxis.setTag("gizmo_axis", "x")
if hasattr(self, 'gizmoYAxis') and self.gizmoYAxis:
self.gizmoYAxis.setTag("is_gizmo", "1")
self.gizmoYAxis.setTag("gizmo_axis", "y")
if hasattr(self, 'gizmoZAxis') and self.gizmoZAxis:
self.gizmoZAxis.setTag("is_gizmo", "1")
self.gizmoZAxis.setTag("gizmo_axis", "z")
# 设置位置和朝向
minPoint = Point3()
maxPoint = Point3()
if nodePath.calcTightBounds(minPoint, maxPoint, self.world.render):
center = Point3((minPoint.x + maxPoint.x) * 0.5,
(minPoint.y + maxPoint.y) * 0.5,
(minPoint.z + maxPoint.z) * 0.5)
self.gizmo.setPos(center)
parent_node = nodePath.getParent()
if parent_node and parent_node != self.world.render:
self.gizmo.setHpr(parent_node.getHpr())
else:
self.gizmo.setHpr(0, 0, 0)
# 只调用一次几何体创建
self.createGizmoGeometry()
#只调用一次颜色设置
self.setGizmoAxisColor("x", self.gizmo_colors["x"])
self.setGizmoAxisColor("y", self.gizmo_colors["y"])
self.setGizmoAxisColor("z", self.gizmo_colors["z"])
self._updateGizmoScreenSize()
self._setupGizmoRendering()
self.setupGizmoCollision()
# 现在才显示坐标轴
self.gizmo.show()
# 只启动一次更新任务
taskMgr.add(self.updateGizmoTask, "updateGizmo")
#print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴")
self.world.newTransform.attach(nodePath)
except Exception as e:
print(f"创建坐标轴失败: {str(e)}")
print(f"attach TransformGizmo failed: {e}")
def createGizmoGeometry(self):
"""创建坐标轴的几何体"""
return
from panda3d.core import Material
try:
if not self.gizmo:
@ -717,6 +647,7 @@ class SelectionSystem:
print(f"创建坐标轴几何体失败: {str(e)}")
def _setupGizmoRendering(self):
return
try:
axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis]
axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis]
@ -807,6 +738,7 @@ class SelectionSystem:
def updateGizmoTask(self, task):
"""坐标轴更新任务 - 包含固定大小功能"""
return task.done
try:
# 限制更新频率
if not hasattr(self, '_last_gizmo_update'):
@ -990,26 +922,9 @@ class SelectionSystem:
self.world.newTransform.detach()
except Exception as e:
print(f"detach TransformGizmo failed: {e}")
self.gizmo = None
self.gizmoTarget = None
self.gizmoXAxis = None
self.gizmoYAxis = None
self.gizmoZAxis = None
self.isDraggingGizmo = False
self.dragGizmoAxis = None
self.dragStartMousePos = None
self.gizmoTargetStartPos = None
self.gizmoStartPos = None
self._resetCursor()
return
if self.gizmo:
self.gizmo.removeNode()
self.gizmo = None
taskMgr.remove("updateGizmo")
self.gizmoTarget = None
self.gizmo = None
self.gizmoXAxis = None
self.gizmoYAxis = None
self.gizmoZAxis = None
@ -1024,6 +939,7 @@ class SelectionSystem:
# """设置坐标轴颜色 - RenderPipeline 兼容版本"""
def setGizmoRotAxisColor(self, axis, color):
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
return
try:
from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib
@ -1118,6 +1034,7 @@ class SelectionSystem:
def setGizmoAxisColor(self, axis, color):
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
return
try:
from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib
@ -1291,6 +1208,7 @@ class SelectionSystem:
def checkGizmoClick(self, mouseX, mouseY):
"""使用屏幕空间检测是否点击了坐标轴"""
return None
if not self._has_legacy_gizmo_input():
return None
@ -1385,6 +1303,7 @@ class SelectionSystem:
def checkGizmoClickFallback(self, mouseX, mouseY):
"""备用检测方法:使用固定的屏幕区域"""
return None
# 获取准确的窗口尺寸
win_width, win_height = self.world.getWindowSize()
@ -1568,6 +1487,9 @@ class SelectionSystem:
def updateGizmoHighlight(self, mouseX, mouseY):
"""更新坐标轴高亮状态"""
self.gizmoHighlightAxis = None
self._resetCursor()
return
if not self._has_legacy_gizmo_input() or self.isDraggingGizmo:
self._resetCursor()
return
@ -1627,6 +1549,7 @@ class SelectionSystem:
def startGizmoDrag(self, axis, mouseX, mouseY):
"""开始坐标轴拖拽"""
return
try:
if not self._has_legacy_gizmo_input():
return
@ -1951,6 +1874,7 @@ class SelectionSystem:
def updateGizmoDrag(self, mouseX, mouseY):
"""更新坐标轴拖拽 - 使用正确的坐标系变换,支持旋转后的子节点拖拽"""
return
try:
if not self._validate_gizmo_drag_state():
return
@ -2001,6 +1925,16 @@ class SelectionSystem:
def stopGizmoDrag(self):
"""停止坐标轴拖拽并创建撤销命令"""
self.isDraggingGizmo = False
self.dragGizmoAxis = None
self.dragStartMousePos = None
self.gizmoTargetStartPos = None
self.gizmoTargetStartScale = None
self.gizmoTargetStartHpr = None
self.gizmoStartPos = None
self.gizmoHighlightAxis = None
self._resetCursor()
return
if not self.isDraggingGizmo:
return
@ -2102,6 +2036,7 @@ class SelectionSystem:
def _dragUpdateTask(self, task):
"""拖拽更新任务 - 持续更新拖拽状态"""
return task.done
try:
if not self.isDraggingGizmo:
return task.done
@ -2139,7 +2074,7 @@ class SelectionSystem:
def updateSelection(self, nodePath):
try:
if self.selectedNode == nodePath:
if self._same_valid_node(self.selectedNode, nodePath):
return
#print(f"\n=== 更新选择状态 ===")
@ -2150,7 +2085,7 @@ class SelectionSystem:
return
node_name = "None"
if nodePath and not nodePath.isEmpty():
if self._is_valid_node(nodePath, require_attached=True):
node_name = nodePath.getName()
#print(f"新选择的节点: {node_name}")
@ -2165,7 +2100,7 @@ class SelectionSystem:
# 添加兼容性属性
self.selectedObject = nodePath
if nodePath and not nodePath.isEmpty():
if self._is_valid_node(nodePath, require_attached=True):
node_name = nodePath.getName()
#print(f"开始为节点 {node_name} 创建选择框和坐标轴...")
@ -2215,24 +2150,6 @@ class SelectionSystem:
import traceback
traceback.print_exc()
def _reparentTreeItem(self, item, new_parent_item):
"""将树项重新父化到新的父项下"""
if not item or not new_parent_item:
return
# 从当前父项中移除
current_parent = item.parent()
if current_parent:
current_parent.removeChild(item)
else:
# 如果是顶级项
index = self.indexOfTopLevelItem(item)
if index >= 0:
self.takeTopLevelItem(index)
# 添加到新父项
new_parent_item.addChild(item)
# def _updateSelectionVisuals(self, nodePath):
# """更新选择的视觉效果(选择框和坐标轴)"""
# try:
@ -2315,16 +2232,19 @@ class SelectionSystem:
return self._get_effective_selected_node() is not None
def checkAndClearIfTargetDeleted(self):
if (self.gizmoTarget and self.gizmoTarget.isEmpty()):
if self.gizmoTarget is not None and (not self._is_valid_node(self.gizmoTarget, require_attached=True)):
self.clearGizmo()
if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()):
if self.selectionBoxTarget is not None and (not self._is_valid_node(self.selectionBoxTarget, require_attached=True)):
self.clearSelectionBox()
if self.selectedNode and self.selectedNode.isEmpty():
if self.selectedNode is not None and (not self._is_valid_node(self.selectedNode, require_attached=True)):
self.selectedNode = None
self.selectedObject = None
self._updateSelectionOutline(None)
def setupGizmoCollision(self):
return
if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis:
return
@ -2343,6 +2263,7 @@ class SelectionSystem:
self.createAxisCollision("z", self.gizmoZAxis)
def createAxisCollision(self, axis_name, axis_node):
return
# 为单个轴创建碰撞体
try:
handle_node = axis_node.find(f"{axis_name}_handle")
@ -2403,6 +2324,7 @@ class SelectionSystem:
traceback.print_exc()
def detectGizmoAxisWithCollision(self, mouseX, mouseY):
return None
# 使用碰撞体检测鼠标是否悬停在坐标轴上
if not self.gizmo:
return None
@ -2470,6 +2392,7 @@ class SelectionSystem:
return None
def debugGizmoCollision(self):
return
print("===碰撞体调试信息===")
for axis_name in ["x","y","z"]:
axis_node = getattr(self,f"gizmo{axis_name.upper()}Axis")

View File

@ -144,16 +144,7 @@ class TerrainManager:
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建高度图地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
qt_item = None
if tree_widget and parent_item:
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
if qt_item:
created_terrains.append((terrain_node, qt_item))
else:
created_terrains.append((terrain_node, None))
print("⚠️ Qt树节点添加跳过或失败但Panda3D对象已创建")
created_terrains.append((terrain_node, None))
# 添加到场景管理器ImGui环境使用
if hasattr(self.world, 'scene_manager') and self.world.scene_manager:
@ -176,13 +167,8 @@ class TerrainManager:
print("❌ 没有成功创建任何高度图地形")
return None
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
if created_terrains:
pass
print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形")
@ -294,16 +280,7 @@ class TerrainManager:
parent_name = parent_item.text(0) if parent_item else "root"
print(f"✅ 为 {parent_name} 创建平面地形: {terrain_name}")
# 在Qt树形控件中添加对应节点
qt_item = None
if tree_widget and parent_item:
qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE")
if qt_item:
created_terrains.append((terrain_node, qt_item))
else:
created_terrains.append((terrain_node, None))
print("⚠️ Qt树节点添加跳过或失败但Panda3D对象已创建")
created_terrains.append((terrain_node, None))
# 添加到场景管理器ImGui环境使用
if hasattr(self.world, 'scene_manager') and self.world.scene_manager:
@ -326,13 +303,8 @@ class TerrainManager:
print("❌ 没有成功创建任何平面地形")
return None
# 选中最后创建的光源
if created_terrains:
last_light_np, last_qt_item = created_terrains[-1]
if last_qt_item:
tree_widget.setCurrentItem(last_qt_item)
# 更新选择和属性面板
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
if created_terrains:
pass
print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形")

View File

@ -150,8 +150,8 @@ Size=101,226
Collapsed=0
[Window][LUI编辑器]
Pos=1193,20
Size=855,748
Pos=1690,20
Size=358,748
Collapsed=0
DockId=0x00000002,2
@ -206,6 +206,11 @@ Size=610,748
Collapsed=0
DockId=0x00000002,2
[Window][项目另存为]
Pos=794,432
Size=460,240
Collapsed=0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
DockNode ID=0x00000005 Parent=0x08BD597D SizeRef=2560,995 Split=X
@ -213,7 +218,7 @@ DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=2206,1084 Split=X
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1846,989 Split=Y
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,961 CentralNode=1
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=358,989 Selected=0x5DB6FF37
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3

View File

@ -138,12 +138,7 @@ class MyWorld(PanelDelegates, CoreWorld):
self.taskMgr.doMethodLater(0.05, self._sync_window_metrics_task, "window-metrics-sync")
print(f"✓ 窗口初始化完成: {self.win.getXSize()} x {self.win.getYSize()}")
# Legacy compatibility fields used by scene/project modules.
self.gui_elements = []
self.gui_manager = None
self.interface_manager = None
self.guiEditMode = False
self.currentGUITool = None
# 初始化选择和变换系统
self.selection = SelectionSystem(self)
@ -193,7 +188,6 @@ class MyWorld(PanelDelegates, CoreWorld):
self.project_manager = ProjectManager(self)
# print(f"[DEBUG] 项目管理系统初始化完成")
# Legacy manager removed in ImGui migration; keep attribute for compatibility.
self.info_panel_manager = None
self.command_manager = CommandManager()

View File

@ -17,7 +17,6 @@ from panda3d.core import (
from panda3d.egg import EggData, EggVertexPool
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
from scene import util
from scene.tree_roles import TREE_USER_ROLE
from core.editor_context import get_editor_context
class SceneManagerModelMixin:
@ -164,30 +163,7 @@ class SceneManagerModelMixin:
# 添加到模型列表
self.models.append(model)
# 更新场景树
# 获取树形控件并添加到Qt树中
tree_widget = self._get_tree_widget()
if tree_widget:
# 找到根节点项
root_item = None
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
if item.text(0) == "render" or item.data(0, TREE_USER_ROLE) == self.world.render:
root_item = item
break
if root_item:
qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE")
if qt_item:
#tree_widget.setCurrentItem(qt_item)
# 更新选择和属性面板
#tree_widget.update_selection_and_properties(model, qt_item)
print("✅ Qt树节点添加成功")
else:
print("⚠️ Qt树节点添加失败但Panda3D对象已创建")
else:
print("⚠️ 未找到根节点项无法添加到Qt树")
#self.updateSceneTree()
# ImGui scene tree is rendered directly from scene state.
print(f"=== 模型导入成功: {model_name} ===\n")
return model
@ -847,11 +823,8 @@ class SceneManagerModelMixin:
print(f"刷新碰撞框失败: {e}")
def updateSceneTree(self):
"""更新场景树显示 - 代理到interface_manager"""
interface_manager = get_editor_context(self.world).get_interface_manager()
if interface_manager and hasattr(interface_manager, "updateSceneTree"):
return interface_manager.updateSceneTree()
print("界面管理器未初始化,无法更新场景树")
"""ImGui scene tree renders directly from scene state; no explicit refresh hook needed."""
return True
def _get_script_file_path(self, script_class, script_name):
"""

View File

@ -312,6 +312,18 @@ class SSBOEditor:
except Exception:
pass
def _get_node_name(self, node, default_name=None):
if not node:
return default_name
try:
return node.get_name()
except Exception:
pass
try:
return node.getName()
except Exception:
return default_name
def _iter_children(self, node):
if not node:
return []
@ -472,6 +484,75 @@ class SSBOEditor:
self.source_model_root = None
self._sync_pick_scene_binding()
def _get_source_root_children(self):
root = self.source_model_root
if not root:
return []
return [child for child in self._iter_children(root) if self._node_is_valid(child)]
def _rebuild_or_clear_runtime_from_current_source(self, highlight_root_name=None):
if self._get_source_root_children():
self.source_model = self.source_model_root
self._rebuild_runtime_from_source_root(highlight_root_name=highlight_root_name)
return self.model
self.last_import_tree_key = None
self.last_import_root_name = None
self.source_model = self.source_model_root
self._sync_pick_scene_binding()
return None
def find_source_child_by_name(self, child_name):
if not child_name:
return None
for child in self._get_source_root_children():
if self._get_node_name(child) == child_name:
return child
return None
def detach_source_child(self, child_name=None, child_np=None):
target = child_np if self._node_is_valid(child_np) else self.find_source_child_by_name(child_name)
if not self._node_is_valid(target):
return None
if self.controller and self.model:
self._snapshot_top_level_transforms_to_source_root()
self._clear_runtime_state(preserve_source_models=True)
try:
target.detach_node()
except Exception:
try:
target.detachNode()
except Exception:
return None
self._rebuild_or_clear_runtime_from_current_source()
return target
def attach_source_child(self, child_np, highlight_root_name=None):
if not self._node_is_valid(child_np):
return None
if self.controller and self.model:
self._snapshot_top_level_transforms_to_source_root()
self._clear_runtime_state(preserve_source_models=True)
source_root = self._ensure_source_model_root()
target_name = highlight_root_name or self._get_node_name(child_np, "imported_model")
self._set_node_name(child_np, target_name)
try:
child_np.reparent_to(source_root)
except Exception:
try:
child_np.reparentTo(source_root)
except Exception:
return None
self._rebuild_or_clear_runtime_from_current_source(highlight_root_name=target_name)
return self.model
def _rebuild_runtime_from_source_root(self, highlight_root_name=None):
root = self._ensure_source_model_root()
working_holder = NodePath("ssbo_source_scene_work")
@ -535,6 +616,7 @@ class SSBOEditor:
self.source_model = source_root
self._rebuild_runtime_from_source_root(highlight_root_name=unique_root_name)
return imported_root
def _build_filename_candidates(self, path_text):
"""Build Filename candidates with wide-char first for Windows CJK paths."""
@ -1417,6 +1499,105 @@ class SSBOEditor:
if chunk_id is not None and chunk_id in self.controller.chunks:
self.controller.chunks[chunk_id]["dirty"] = True
def sync_scene_nodes_to_pick(self, nodes):
"""Sync transformed scene nodes to both pick data and visible static chunks."""
if not self.controller:
return
self._sync_pick_scene_binding()
self._sync_pick_root_transform()
valid_nodes = []
for node in nodes or []:
if not self._node_is_valid(node):
continue
duplicate = False
for existing in valid_nodes:
try:
if existing == node:
duplicate = True
break
except Exception:
pass
if not duplicate:
valid_nodes.append(node)
if not valid_nodes:
self._reset_pick_sync_cache()
return
affected_chunks = set()
for gid, obj_np in self.controller.id_to_object_np.items():
if not self._node_is_valid(obj_np):
continue
matched = False
for target in valid_nodes:
try:
if obj_np == target:
matched = True
break
except Exception:
pass
if not matched:
continue
is_attached = False
try:
is_attached = bool(obj_np.has_parent())
except Exception:
try:
is_attached = bool(obj_np.hasParent())
except Exception:
is_attached = False
is_visible = False
if is_attached:
try:
is_visible = not obj_np.is_hidden()
except Exception:
try:
is_visible = not obj_np.isHidden()
except Exception:
is_visible = True
pick_np = self.controller.id_to_pick_np.get(gid)
if pick_np and not pick_np.is_empty():
if is_visible:
try:
pick_np.show()
except Exception:
pass
try:
pick_np.set_mat(self.base.render, obj_np.get_mat(self.base.render))
except Exception:
try:
pick_np.setMat(self.base.render, obj_np.getMat(self.base.render))
except Exception:
pass
else:
try:
pick_np.hide()
except Exception:
pass
chunk_id = self.controller.id_to_chunk.get(gid)
if chunk_id is not None and chunk_id in self.controller.chunks:
self.controller.chunks[chunk_id]["dirty"] = True
affected_chunks.add(chunk_id)
for chunk_id in affected_chunks:
chunk = self.controller.chunks.get(chunk_id)
if not chunk or chunk.get("dynamic_enabled"):
continue
try:
self.controller._rebuild_static_chunk(chunk_id)
except Exception:
pass
self._reset_pick_sync_cache()
def _update_outline_for_selection(self):
if not self._outline_manager:
return

View File

@ -1571,6 +1571,7 @@ class AnimationTools:
owner_node = self._resolve_animation_owner_model(node)
node.setPythonTag("cached_anim_info", None)
node.setPythonTag("cached_processed_names", None)
node.setPythonTag("cached_has_animation_nodes", None)
node.setPythonTag("animation", None) # 同时清除动画检测结果
# 如果Actor在缓存中也需要清理

View File

@ -516,9 +516,91 @@ class AppActions:
def add_info_message(self, text):
"""添加信息消息"""
self.add_message(f" {text}", (0.157, 0.620, 1.0, 1.0))
# ==================== 编辑菜单功能实现 ====================
def _is_history_node_valid(self, node, require_attached=False):
if node is None:
return False
try:
if node.isEmpty():
return False
except Exception:
return False
if require_attached:
try:
return bool(node.hasParent())
except Exception:
return False
return True
def _has_active_ssbo_history_selection(self):
ssbo_editor = getattr(self, "ssbo_editor", None)
if not ssbo_editor or not hasattr(ssbo_editor, "has_active_selection"):
return False
try:
return bool(ssbo_editor.has_active_selection())
except Exception:
return False
def _sync_editor_state_after_history_change(self):
ssbo_editor = getattr(self, "ssbo_editor", None)
ssbo_active = self._has_active_ssbo_history_selection()
transform = getattr(self, "newTransform", None)
selection = getattr(self, "selection", None)
if ssbo_editor:
try:
if hasattr(ssbo_editor, "_sync_pick_scene_binding"):
ssbo_editor._sync_pick_scene_binding()
except Exception:
pass
try:
if hasattr(ssbo_editor, "sync_scene_nodes_to_pick"):
ssbo_editor.sync_scene_nodes_to_pick([])
except Exception:
pass
if selection:
if ssbo_active and ssbo_editor:
try:
selected_key = getattr(ssbo_editor, "selected_name", None)
if selected_key and hasattr(ssbo_editor, "select_node"):
ssbo_editor.select_node(selected_key, sync_world_selection=False)
if hasattr(ssbo_editor, "_sync_editor_selection_reference"):
ssbo_editor._sync_editor_selection_reference(ssbo_editor.get_selection_scene_node())
except Exception:
pass
else:
selected_node = None
try:
selected_node = selection.getSelectedNode() if hasattr(selection, "getSelectedNode") else getattr(selection, "selectedNode", None)
except Exception:
selected_node = None
if not self._is_history_node_valid(selected_node, require_attached=True):
try:
selection.clearSelection()
except Exception:
try:
selection.selectedNode = None
selection.selectedObject = None
except Exception:
pass
monitored_node = getattr(self, "_monitored_node", None)
target_node = getattr(transform, "target_node", None) if transform else None
if not self._is_history_node_valid(monitored_node, require_attached=True):
if not (ssbo_active and self._is_history_node_valid(target_node, require_attached=True)):
try:
self.stop_transform_monitoring()
except Exception:
pass
if transform and (not self._is_history_node_valid(target_node, require_attached=True)):
if not ssbo_active:
try:
transform.detach()
except Exception:
pass
def _on_undo(self):
"""处理撤销操作"""
@ -527,6 +609,7 @@ class AppActions:
if hasattr(self, 'command_manager') and self.command_manager and self.command_manager.can_undo():
success = self.command_manager.undo()
if success:
self._sync_editor_state_after_history_change()
self.add_success_message("撤销操作成功")
return
self.add_error_message("撤销操作失败")
@ -535,6 +618,7 @@ class AppActions:
# 2) 回退到 TransformGizmo 历史(拖拽位移/旋转/缩放)
tg = getattr(self, 'newTransform', None)
if tg and hasattr(tg, 'undo_last') and tg.undo_last():
self._sync_editor_state_after_history_change()
self.add_success_message("撤销操作成功")
return
@ -550,6 +634,7 @@ class AppActions:
if hasattr(self, 'command_manager') and self.command_manager and self.command_manager.can_redo():
success = self.command_manager.redo()
if success:
self._sync_editor_state_after_history_change()
self.add_success_message("重做操作成功")
return
self.add_error_message("重做操作失败")
@ -558,6 +643,7 @@ class AppActions:
# 2) 回退到 TransformGizmo 重做栈
tg = getattr(self, 'newTransform', None)
if tg and hasattr(tg, 'redo_last') and tg.redo_last():
self._sync_editor_state_after_history_change()
self.add_success_message("重做操作成功")
return
@ -1162,6 +1248,138 @@ class AppActions:
# ==================== 路径浏览器辅助方法 ====================
def _refresh_ssbo_runtime_import_bindings(self, file_path=None, scene_package_import=False):
ssbo_editor = getattr(self, 'ssbo_editor', None)
model_np = getattr(ssbo_editor, 'model', None) if ssbo_editor else None
scene_manager = getattr(self, 'scene_manager', None)
if scene_manager and hasattr(scene_manager, 'models'):
scene_manager.models = [model_np] if model_np else []
if not model_np:
return None
normalized_model_path = file_path
if file_path and not scene_package_import:
try:
from scene import util as scene_util
normalized_model_path = scene_util.normalize_model_path(file_path)
except Exception:
normalized_model_path = file_path
if normalized_model_path:
model_np.setTag("model_path", normalized_model_path)
model_np.setTag("saved_model_path", normalized_model_path)
if file_path:
model_np.setTag("original_path", file_path)
model_np.setTag("file", os.path.basename(file_path))
model_np.setTag("is_model_root", "1")
model_np.setTag("is_scene_element", "1")
ssbo_source_root = getattr(ssbo_editor, "source_model_root", None)
source_children = []
if ssbo_source_root is not None:
try:
source_children = [child for child in ssbo_source_root.getChildren() if not child.isEmpty()]
except Exception:
try:
source_children = [child for child in ssbo_source_root.get_children() if not child.is_empty()]
except Exception:
source_children = []
if len(source_children) > 1 and not scene_package_import:
model_np.setName("\u5bfc\u5165\u6a21\u578b")
elif file_path:
model_np.setName(os.path.basename(file_path))
elif len(source_children) == 1:
try:
model_np.setName(source_children[0].getName())
except Exception:
pass
if scene_manager:
try:
scene_manager.setupCollision(model_np)
if scene_package_import:
model_np.setTag("has_animations", "false")
model_np.setTag("has_animations_checked", "true")
model_np.setTag("scene_import_source", "project_scene_bam")
elif file_path:
scene_manager._processModelAnimations(model_np)
except Exception as e:
print(f"[SSBO] setup components failed: {e}")
return model_np
def _execute_ssbo_import_command(self, file_path):
if not getattr(self, 'ssbo_editor', None):
return None
from core.Command_System import SnapshotStateCommand
import_state = {
'source_child': None,
'root_name': None,
}
def apply_state(state):
mode = state.get('mode') if isinstance(state, dict) else state
if mode == 'after':
source_child = import_state['source_child']
source_child_valid = False
if source_child:
try:
source_child_valid = not source_child.isEmpty()
except Exception:
try:
source_child_valid = not source_child.is_empty()
except Exception:
source_child_valid = False
if source_child_valid:
self.ssbo_editor.attach_source_child(source_child, highlight_root_name=import_state['root_name'])
else:
imported_root = self.ssbo_editor.load_model(
file_path,
keep_source_model=False,
append=True,
)
import_state['source_child'] = imported_root
import_state['root_name'] = getattr(self.ssbo_editor, 'last_import_root_name', None)
return self._refresh_ssbo_runtime_import_bindings(file_path=file_path)
source_child = import_state['source_child']
if source_child:
self.ssbo_editor.detach_source_child(child_np=source_child)
return self._refresh_ssbo_runtime_import_bindings()
command = SnapshotStateCommand(
apply_state,
{'mode': 'before'},
{'mode': 'after'},
)
self.command_manager.execute_command(command)
return getattr(self.ssbo_editor, 'model', None)
def _execute_import_command(self, file_path, scene_package_import=False):
command_manager = getattr(self, 'command_manager', None)
if scene_package_import or not command_manager:
return self._import_model_for_runtime(file_path, scene_package_import=scene_package_import)
if self.use_ssbo_mouse_picking and getattr(self, 'ssbo_editor', None):
return self._execute_ssbo_import_command(file_path)
from core.Command_System import CreateNodeCommand
command = CreateNodeCommand(
lambda _parent: self._import_model_for_runtime(file_path, scene_package_import=False),
getattr(self, 'render', None),
world=self,
)
command_manager.execute_command(command)
return command.created_node
def _import_model_for_runtime(self, file_path, prefer_scene_manager=False, scene_package_import=False):
"""Import model through the active runtime path.
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
@ -1184,53 +1402,10 @@ class AppActions:
keep_source_model=scene_package_import,
append=not scene_package_import,
)
model_np = getattr(self.ssbo_editor, 'model', None)
# Keep legacy ray-pick fallback usable by adding a collision body.
if model_np:
normalized_model_path = file_path
if not scene_package_import:
try:
from scene import util as scene_util
normalized_model_path = scene_util.normalize_model_path(file_path)
except Exception:
normalized_model_path = file_path
# Apply vital tags manually since SSBO overrides SceneManager loader
model_np.setTag("model_path", normalized_model_path)
model_np.setTag("original_path", file_path)
model_np.setTag("saved_model_path", normalized_model_path)
model_np.setTag("is_model_root", "1")
model_np.setTag("is_scene_element", "1")
model_np.setTag("file", os.path.basename(file_path))
ssbo_source_root = getattr(self.ssbo_editor, "source_model_root", None)
source_children = []
if ssbo_source_root is not None:
try:
source_children = list(ssbo_source_root.getChildren())
except Exception:
try:
source_children = list(ssbo_source_root.get_children())
except Exception:
source_children = []
if len(source_children) > 1 and not scene_package_import:
model_np.setName("导入模型")
else:
model_np.setName(os.path.basename(file_path))
if hasattr(self, 'scene_manager') and self.scene_manager:
try:
self.scene_manager.setupCollision(model_np)
if scene_package_import:
model_np.setTag("has_animations", "false")
model_np.setTag("has_animations_checked", "true")
model_np.setTag("scene_import_source", "project_scene_bam")
else:
self.scene_manager._processModelAnimations(model_np)
except Exception as e:
print(f"[SSBO] setup components failed: {e}")
if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
self.scene_manager.models = [model_np]
return model_np
return self._refresh_ssbo_runtime_import_bindings(
file_path=file_path,
scene_package_import=scene_package_import,
)
except Exception as e:
print(f"[SSBO] load_model failed: {e}")
return None
@ -1272,7 +1447,7 @@ class AppActions:
if show_info_message:
self.add_info_message(f"正在导入模型: {file_name}")
model_node = self._import_model_for_runtime(normalized_path)
model_node = self._execute_import_command(normalized_path)
if not model_node:
self.add_error_message("模型导入失败")
return None
@ -1308,7 +1483,7 @@ class AppActions:
if hasattr(self.scene_manager, 'models'):
if getattr(self, "use_ssbo_mouse_picking", False):
self.scene_manager.models = [model_node]
else:
elif model_node not in self.scene_manager.models:
self.scene_manager.models.append(model_node)
if select_model:

View File

@ -4,6 +4,13 @@ from pathlib import Path
class EditorPanelsLeftMixin:
"""Auto-split mixin from editor_panels.py."""
@staticmethod
def _truncate_panel_text(text, limit=28):
text = text or ""
if len(text) <= limit:
return text
return text[: max(0, limit - 1)] + ""
def _draw_scene_tree(self):
"""绘制场景树面板"""
# 使用更少的限制性标志允许docking
@ -36,16 +43,28 @@ class EditorPanelsLeftMixin:
if selected_node and not selected_node.isEmpty():
selected_name = selected_node.getName() or "未命名对象"
imgui.text_disabled(f"模型 {model_count}")
self.app.style_manager.draw_stat_chip(f"模型 {model_count}", tint=(0.14, 0.17, 0.22, 1.0))
imgui.same_line()
imgui.text_disabled(f"当前: {selected_name}")
self.app.style_manager.draw_stat_chip(
f"当前 {self._truncate_panel_text(selected_name, 20)}",
tint=(0.16, 0.20, 0.27, 1.0),
)
filter_text = self._get_scene_tree_filter()
if filter_text:
imgui.same_line()
self.app.style_manager.draw_stat_chip(
f"筛选 {self._truncate_panel_text(filter_text, 16)}",
tint=(0.19, 0.24, 0.33, 1.0),
)
imgui.spacing()
imgui.text_disabled("筛选场景")
imgui.set_next_item_width(-64)
changed, search_text = imgui.input_text("##scene_tree_search", self.app._scene_tree_search_text, 256)
if changed:
self.app._scene_tree_search_text = search_text
imgui.same_line()
if imgui.button("清空##scene_tree_search"):
if self.app.style_manager.draw_toolbar_button("清空", size=(52, 26), enabled=bool(self.app._scene_tree_search_text)):
self.app._scene_tree_search_text = ""
def _get_scene_tree_filter(self):
@ -578,12 +597,24 @@ class EditorPanelsLeftMixin:
except ValueError:
location = str(rm.current_path)
imgui.text_disabled(location or ".")
self.app.style_manager.draw_stat_chip(
self._truncate_panel_text(location or ".", 30),
tint=(0.14, 0.17, 0.22, 1.0),
)
imgui.same_line()
imgui.text_disabled(f"{total_items}")
self.app.style_manager.draw_stat_chip(f"{total_items}", tint=(0.16, 0.19, 0.24, 1.0))
if rm.selected_files:
imgui.same_line()
imgui.text_colored((0.5, 0.8, 1.0, 1.0), f"已选 {len(rm.selected_files)}")
self.app.style_manager.draw_stat_chip(
f"已选 {len(rm.selected_files)}",
tint=(0.18, 0.24, 0.32, 1.0),
)
if rm.search_filter:
imgui.same_line()
self.app.style_manager.draw_stat_chip(
f"搜索 {self._truncate_panel_text(rm.search_filter, 18)}",
tint=(0.19, 0.24, 0.33, 1.0),
)
def _sync_resource_path_input(self, rm):
current_path_text = str(rm.current_path)
@ -606,22 +637,21 @@ class EditorPanelsLeftMixin:
"""绘制资源管理器顶部工具条与筛选输入。"""
self._sync_resource_path_input(rm)
if imgui.button(""):
if self.app.style_manager.draw_toolbar_button("", size=(36, 26), tooltip="后退"):
rm.navigate_back()
imgui.same_line()
if imgui.button(""):
if self.app.style_manager.draw_toolbar_button("", size=(36, 26), tooltip="前进"):
rm.navigate_forward()
imgui.same_line()
if imgui.button(""):
if self.app.style_manager.draw_toolbar_button("上级", size=(48, 26), tooltip="返回上级目录"):
rm.navigate_up()
imgui.same_line()
if imgui.button("主页"):
if self.app.style_manager.draw_toolbar_button("资源", size=(48, 26), tooltip="回到 Resources 根目录"):
rm.navigate_to(rm.project_root / "Resources")
imgui.same_line()
if imgui.button("刷新"):
if self.app.style_manager.draw_toolbar_button("刷新", size=(48, 26), tooltip="刷新当前目录"):
rm.force_refresh()
# 自动刷新开关
imgui.same_line()
changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled)
if changed:
@ -633,14 +663,14 @@ class EditorPanelsLeftMixin:
if changed:
self.app._resource_path_input = new_path
imgui.same_line()
if imgui.button("前往##resource_go"):
if self.app.style_manager.draw_toolbar_button("前往", size=(52, 26), tooltip="跳转到输入路径"):
self._navigate_resource_path_from_input(rm)
imgui.text_disabled("搜索")
imgui.set_next_item_width(-72)
changed, rm.search_filter = imgui.input_text("##resource_search", rm.search_filter, 256)
imgui.same_line()
if imgui.button("清除##resource_search"):
if self.app.style_manager.draw_toolbar_button("清空", size=(52, 26), tooltip="清空搜索条件", enabled=bool(rm.search_filter)):
rm.search_filter = ""
def _load_resource_icon(self, icon_name: str):
@ -672,8 +702,11 @@ class EditorPanelsLeftMixin:
def _handle_resource_file_double_click(self, rm, file_path: Path):
"""处理文件双击:模型导入,其他文件打开。"""
if self._is_model_file(file_path):
self.app.add_info_message(f"正在导入模型: {file_path.name}")
self.app._import_model_for_runtime(str(file_path))
self.app._import_model_with_menu_logic(
str(file_path),
show_info_message=True,
show_success_message=True,
)
else:
rm.open_file(file_path)
@ -697,8 +730,11 @@ class EditorPanelsLeftMixin:
imgui.separator()
if imgui.menu_item("导入到场景")[1]:
if self._is_model_file(rm.context_menu_file):
self.app.add_info_message(f"正在导入模型: {rm.context_menu_file.name}")
self.app._import_model_for_runtime(str(rm.context_menu_file))
self.app._import_model_with_menu_logic(
str(rm.context_menu_file),
show_info_message=True,
show_success_message=True,
)
if imgui.menu_item("重命名")[1]:
print(f"重命名文件: {rm.context_menu_file.name}")
if imgui.menu_item("删除")[1]:
@ -726,6 +762,8 @@ class EditorPanelsLeftMixin:
imgui.end_popup()
@staticmethod
def _is_model_file(path: Path) -> bool:
return path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']

View File

@ -1,4 +1,7 @@
from pathlib import Path
from imgui_bundle import imgui, imgui_ctx
from panda3d.core import TransparencyAttrib
from ui.panels.editor_panels_right_collision import EditorPanelsRightCollisionMixin
from ui.panels.editor_panels_right_material import EditorPanelsRightMaterialMixin
@ -12,6 +15,65 @@ class EditorPanelsRightMixin(
):
"""Right panel aggregator mixin."""
def _apply_gui_alpha(self, gui_element, alpha_value):
alpha_value = max(0.0, min(1.0, float(alpha_value)))
gui_element.alpha = alpha_value
if hasattr(gui_element, "setTransparency"):
gui_element.setTransparency(
TransparencyAttrib.MAlpha if alpha_value < 0.999 else TransparencyAttrib.MNone
)
color_scale = None
if hasattr(gui_element, "getColorScale"):
try:
color_scale = gui_element.getColorScale()
except Exception:
color_scale = None
if color_scale is not None and hasattr(gui_element, "setColorScale"):
try:
gui_element.setColorScale(
float(color_scale[0]),
float(color_scale[1]),
float(color_scale[2]),
alpha_value,
)
return
except Exception:
pass
if hasattr(gui_element, "setAlphaScale"):
try:
gui_element.setAlphaScale(alpha_value)
return
except Exception:
pass
if hasattr(gui_element, "setColorScale"):
try:
gui_element.setColorScale(1.0, 1.0, 1.0, alpha_value)
except Exception:
pass
def _get_cached_animation_structure_state(self, anim_node, force_refresh=False):
if not force_refresh:
cached_state = anim_node.getPythonTag("cached_has_animation_nodes")
if cached_state is not None:
return bool(cached_state)
has_animation_nodes = False
try:
has_animation_nodes = (
anim_node.findAllMatches("**/+Character").getNumPaths() > 0 or
anim_node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
)
except Exception:
has_animation_nodes = False
anim_node.setPythonTag("cached_has_animation_nodes", bool(has_animation_nodes))
return has_animation_nodes
def _record_visibility_change(self, node, old_visible, new_visible):
if not hasattr(self.app, "command_manager") or not self.app.command_manager:
node.setPythonTag("user_visible", bool(new_visible))
@ -22,7 +84,7 @@ class EditorPanelsRightMixin(
return
from core.Command_System import VisibilityNodeCommand
self.app.command_manager.execute_command(
VisibilityNodeCommand(node, old_visible, new_visible)
VisibilityNodeCommand(node, old_visible, new_visible, world=self.app)
)
def _record_name_change(self, node, old_name, new_name):
@ -212,6 +274,41 @@ class EditorPanelsRightMixin(
imgui.text_disabled(f"{node_type} · 父级: {parent_name}")
self._draw_status_badges(node, node_type)
def _node_has_collision_badge(self, node):
child_count = 0
try:
child_count = int(node.getNumChildren())
except Exception:
child_count = 0
cached = None
try:
cached = node.getPythonTag("cached_collision_badge_state")
except Exception:
cached = None
if isinstance(cached, dict) and cached.get("child_count") == child_count:
return bool(cached.get("has_collision", False))
has_collision = False
try:
for child in node.getChildren():
child_name = child.getName()
if child_name and "Collision" in child_name:
has_collision = True
break
except Exception:
has_collision = False
try:
node.setPythonTag(
"cached_collision_badge_state",
{"child_count": child_count, "has_collision": bool(has_collision)},
)
except Exception:
pass
return has_collision
def _draw_status_badges(self, node, node_type=None):
"""绘制精简后的对象状态徽章行。"""
if node_type is None:
@ -222,9 +319,7 @@ class EditorPanelsRightMixin(
if node.is_hidden():
badges.append(("已隐藏", (0.65, 0.65, 0.65, 1.0)))
has_collision = hasattr(node, "getChild") and any(
"Collision" in child.getName() for child in node.getChildren() if child.getName()
)
has_collision = hasattr(node, "getChild") and self._node_has_collision_badge(node)
if has_collision:
badges.append(("碰撞", (0.35, 0.65, 1.0, 1.0)))
@ -401,11 +496,7 @@ class EditorPanelsRightMixin(
current_alpha = getattr(gui_element, 'alpha', 1.0)
changed, new_alpha = imgui.slider_float("Alpha", current_alpha, 0.0, 1.0)
if changed:
gui_element.alpha = new_alpha
if hasattr(gui_element, 'setTransparency'):
# 将0.0-1.0范围转换为Panda3D的透明度格式
panda_transparency = int((1.0 - new_alpha) * 255)
gui_element.setTransparency(panda_transparency)
self._apply_gui_alpha(gui_element, new_alpha)
# 渲染顺序
imgui.text("渲染顺序")
@ -538,22 +629,30 @@ class EditorPanelsRightMixin(
except Exception:
pass
# 先刷新一次模型动画标签,避免“导入后未初始化”导致误判
has_animation_checked = (
anim_node.hasTag("has_animations_checked") and
anim_node.getTag("has_animations_checked").lower() == "true"
)
# 只在模型还没做过动画探测时刷新标签,避免属性面板每帧重复扫描。
try:
if hasattr(self, "scene_manager") and self.scene_manager and hasattr(self.scene_manager, "_processModelAnimations"):
if (
not has_animation_checked and
hasattr(self, "scene_manager") and
self.scene_manager and
hasattr(self.scene_manager, "_processModelAnimations")
):
self.scene_manager._processModelAnimations(anim_node)
except Exception:
pass
has_animation_tag = anim_node.hasTag("has_animations") and anim_node.getTag("has_animations").lower() == "true"
has_animation_nodes = False
try:
has_animation_nodes = (
anim_node.findAllMatches("**/+Character").getNumPaths() > 0 or
anim_node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
)
except Exception:
pass
has_cached_animation = anim_node.getPythonTag("animation") is True
has_animation_nodes = (
has_animation_tag or
has_cached_animation or
self._get_cached_animation_structure_state(anim_node)
)
# 检查是否已经缓存了动画信息
cached_anim_info = anim_node.getPythonTag("cached_anim_info")
@ -581,13 +680,17 @@ class EditorPanelsRightMixin(
anim_node.setPythonTag("animation", None)
should_force_probe = False
has_cached_animation = anim_node.getPythonTag("animation") is True
if not (has_animation_tag or has_animation_nodes or has_cached_animation):
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型未检测到动画结构")
if imgui.button("尝试强制检测##force_detect_animation"):
if self.app.style_manager.draw_toolbar_button(
"尝试强制检测",
size=(108, 26),
tooltip="重新扫描当前模型的动画结构",
):
should_force_probe = True
anim_node.setPythonTag("cached_anim_info", None)
anim_node.setPythonTag("cached_processed_names", None)
has_animation_nodes = self._get_cached_animation_structure_state(anim_node, force_refresh=True)
else:
return
@ -683,22 +786,22 @@ class EditorPanelsRightMixin(
imgui.text("控制:")
# 播放按钮
if imgui.button("播放##play_animation"):
if self.app.style_manager.draw_toolbar_button("播放", size=(60, 26), tooltip="播放当前动画"):
self._playAnimation(anim_node)
imgui.same_line()
# 暂停按钮
if imgui.button("暂停##pause_animation"):
if self.app.style_manager.draw_toolbar_button("暂停", size=(60, 26), tooltip="暂停当前动画"):
self._pauseAnimation(anim_node)
imgui.same_line()
# 停止按钮
if imgui.button("停止##stop_animation"):
if self.app.style_manager.draw_toolbar_button("停止", size=(60, 26), tooltip="停止当前动画"):
self._stopAnimation(anim_node)
imgui.same_line()
# 循环按钮
if imgui.button("循环##loop_animation"):
if self.app.style_manager.draw_toolbar_button("循环", size=(60, 26), tooltip="循环播放当前动画"):
self._loopAnimation(anim_node)
imgui.spacing()
@ -725,7 +828,7 @@ class EditorPanelsRightMixin(
def _draw_property_actions(self, node):
"""绘制属性操作按钮"""
# 重置变换
if imgui.button("重置变换"):
if self.app.style_manager.draw_toolbar_button("重置变换", size=(84, 28), tooltip="将位置、旋转、缩放恢复为默认值"):
if hasattr(self.app, "command_manager") and self.app.command_manager:
from core.Command_System import (
CompositeCommand,
@ -756,19 +859,23 @@ class EditorPanelsRightMixin(
# 切换可见性
is_visible = not node.is_hidden()
visibility_text = "隐藏" if is_visible else "显示"
if imgui.button(visibility_text):
if self.app.style_manager.draw_toolbar_button(
visibility_text,
size=(64, 28),
tooltip="切换当前对象的可见状态",
):
self._record_visibility_change(node, is_visible, not is_visible)
imgui.same_line()
# 聚焦到对象
if imgui.button("聚焦"):
if self.app.style_manager.draw_toolbar_button("聚焦", size=(64, 28), tooltip="将相机聚焦到当前对象"):
if hasattr(self, 'selection') and self.selection:
self.selection.focusCameraOnSelectedNodeAdvanced()
# 删除对象
imgui.same_line()
if imgui.button("删除"):
if self.app.style_manager.draw_toolbar_button("删除", size=(64, 28), tooltip="删除当前对象"):
if hasattr(self, 'selection') and self.selection:
self.selection.deleteSelectedNode()

View File

@ -3,6 +3,30 @@ from imgui_bundle import imgui, imgui_ctx
class EditorPanelsTopMixin:
"""Auto-split mixin from editor_panels.py."""
@staticmethod
def _truncate_toolbar_text(text, limit=20):
text = text or ""
if len(text) <= limit:
return text
return text[: max(0, limit - 1)] + ""
def _get_toolbar_project_name(self):
project_manager = getattr(self.app, "project_manager", None)
project_path = getattr(project_manager, "current_project_path", None) if project_manager else None
if not project_path:
return "未命名项目"
import os
return os.path.basename(project_path) or "未命名项目"
def _get_toolbar_selection_name(self):
selected_node = self.app._get_selection_node()
if selected_node and not selected_node.isEmpty():
return selected_node.getName() or "未命名对象"
ssbo_summary = self.app._get_ssbo_selection_summary()
if ssbo_summary:
return ssbo_summary.get("display_name") or "SSBO 选择"
return "未选择"
def draw_menu_bar(self):
"""绘制菜单栏"""
with imgui_ctx.begin_main_menu_bar() as main_menu:
@ -265,86 +289,61 @@ class EditorPanelsTopMixin:
return
self.app.showToolbar = opened
# 选择工具按钮
select_active = self.app.tool_manager.isSelectionTool()
if self.app.icons.get('select'):
tint_col = (1.0, 1.0, 0.0, 1.0) if select_active else (1.0, 1.0, 1.0, 1.0)
if self.app.style_manager.image_button(self.app.icons['select'], (24, 24), tint_col=tint_col):
self.app.tool_manager.setCurrentTool("选择")
if imgui.is_item_hovered():
imgui.set_tooltip("选择工具 (Q)")
imgui.same_line()
else:
if imgui.button("选择##select_tool"):
self.app.tool_manager.setCurrentTool("选择")
if select_active:
draw_list = imgui.get_window_draw_list()
button_min = imgui.get_item_rect_min()
button_max = imgui.get_item_rect_max()
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
imgui.same_line()
# 移动工具按钮
move_active = self.app.tool_manager.isMoveTool()
if self.app.icons.get('move'):
# 使用不同颜色表示活动状态
tint_col = (1.0, 1.0, 0.0, 1.0) if move_active else (1.0, 1.0, 1.0, 1.0) # 活动时显示黄色
if self.app.style_manager.image_button(self.app.icons['move'], (24, 24), tint_col=tint_col):
self.app.tool_manager.setCurrentTool("移动")
if imgui.is_item_hovered():
imgui.set_tooltip("移动工具 (W)")
imgui.same_line()
else:
if imgui.button("移动##move_tool"):
self.app.tool_manager.setCurrentTool("移动")
if move_active:
# 为活动按钮添加背景色
draw_list = imgui.get_window_draw_list()
button_min = imgui.get_item_rect_min()
button_max = imgui.get_item_rect_max()
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
imgui.same_line()
# 旋转工具按钮
rotate_active = self.app.tool_manager.isRotateTool()
if self.app.icons.get('rotate'):
tint_col = (1.0, 1.0, 0.0, 1.0) if rotate_active else (1.0, 1.0, 1.0, 1.0)
if self.app.style_manager.image_button(self.app.icons['rotate'], (24, 24), tint_col=tint_col):
self.app.tool_manager.setCurrentTool("旋转")
if imgui.is_item_hovered():
imgui.set_tooltip("旋转工具 (E)")
imgui.same_line()
else:
if imgui.button("旋转##rotate_tool"):
self.app.tool_manager.setCurrentTool("旋转")
if rotate_active:
draw_list = imgui.get_window_draw_list()
button_min = imgui.get_item_rect_min()
button_max = imgui.get_item_rect_max()
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
imgui.same_line()
# 缩放工具按钮
scale_active = self.app.tool_manager.isScaleTool()
if self.app.icons.get('scale'):
tint_col = (1.0, 1.0, 0.0, 1.0) if scale_active else (1.0, 1.0, 1.0, 1.0)
if self.app.style_manager.image_button(self.app.icons['scale'], (24, 24), tint_col=tint_col):
self.app.tool_manager.setCurrentTool("缩放")
if imgui.is_item_hovered():
imgui.set_tooltip("缩放工具 (R)")
else:
if imgui.button("缩放##scale_tool"):
self.app.tool_manager.setCurrentTool("缩放")
if scale_active:
draw_list = imgui.get_window_draw_list()
button_min = imgui.get_item_rect_min()
button_max = imgui.get_item_rect_max()
draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
style_manager = self.app.style_manager
command_manager = getattr(self.app, "command_manager", None)
can_undo = command_manager.can_undo() if command_manager else False
can_redo = command_manager.can_redo() if command_manager else False
selection_name = self._truncate_toolbar_text(self._get_toolbar_selection_name(), 18)
project_name = self._truncate_toolbar_text(self._get_toolbar_project_name(), 18)
tool_name = getattr(self.app.tool_manager, "getCurrentTool", lambda: "选择")()
imgui.push_style_var(imgui.StyleVar_.item_spacing, (8.0, 6.0))
imgui.push_style_var(imgui.StyleVar_.frame_padding, (8.0, 5.0))
tool_buttons = [
("Q 选择", "选择", self.app.tool_manager.isSelectionTool(), "选择工具 (Q)"),
("W 移动", "移动", self.app.tool_manager.isMoveTool(), "移动工具 (W)"),
("E 旋转", "旋转", self.app.tool_manager.isRotateTool(), "旋转工具 (E)"),
("R 缩放", "缩放", self.app.tool_manager.isScaleTool(), "缩放工具 (R)"),
]
for index, (label, tool_id, is_active, tooltip) in enumerate(tool_buttons):
if style_manager.draw_toolbar_button(label, active=is_active, size=(74, 28), tooltip=tooltip):
self.app.tool_manager.setCurrentTool(tool_id)
if index != len(tool_buttons) - 1:
imgui.same_line()
imgui.same_line()
imgui.separator()
imgui.same_line()
# 工具按钮已移除(导入、保存、播放)
if style_manager.draw_toolbar_button("撤销", size=(56, 28), tooltip="撤销上一步 (Ctrl+Z)", enabled=can_undo):
self.app._on_undo()
imgui.same_line()
if style_manager.draw_toolbar_button("重做", size=(56, 28), tooltip="重做上一步 (Ctrl+Y)", enabled=can_redo):
self.app._on_redo()
imgui.same_line()
if style_manager.draw_toolbar_button(
"聚焦",
size=(56, 28),
tooltip="聚焦当前选择 (F)",
enabled=selection_name != "未选择",
):
self.app.onFocusKeyPressed()
imgui.same_line()
imgui.separator()
imgui.same_line()
style_manager.draw_stat_chip(f"项目 {project_name}", tint=(0.12, 0.14, 0.18, 1.0))
imgui.same_line()
style_manager.draw_stat_chip(f"工具 {tool_name}", tint=(0.16, 0.19, 0.24, 1.0))
imgui.same_line()
style_manager.draw_stat_chip(f"选择 {selection_name}", tint=(0.18, 0.24, 0.32, 1.0))
imgui.same_line()
style_manager.draw_stat_chip(
f"历史 {command_manager.get_undo_count() if command_manager else 0}/{command_manager.get_redo_count() if command_manager else 0}",
tint=(0.16, 0.18, 0.2, 1.0),
)
imgui.pop_style_var(2)

View File

@ -2,30 +2,38 @@
class PanelDelegates:
def _node_is_valid(self, node):
if not node:
def _node_is_valid(self, node, require_attached=False):
if node is None:
return False
try:
return not node.is_empty()
if node.is_empty():
return False
except Exception:
try:
return not node.isEmpty()
if node.isEmpty():
return False
except Exception:
return False
if require_attached:
try:
return bool(node.hasParent())
except Exception:
return False
return True
def _get_selection_node(self):
"""Return the current editor selection, preferring the active SSBO state."""
ssbo_editor = getattr(self, "ssbo_editor", None)
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
ssbo_node = ssbo_editor.get_selection_scene_node()
return ssbo_node if self._node_is_valid(ssbo_node) else None
return ssbo_node if self._node_is_valid(ssbo_node, require_attached=True) else None
selection = getattr(self, "selection", None)
if selection and hasattr(selection, "getSelectedNode"):
node = selection.getSelectedNode()
else:
node = getattr(selection, "selectedNode", None) if selection else None
return node if self._node_is_valid(node) else None
return node if self._node_is_valid(node, require_attached=True) else None
def _get_ssbo_selection_summary(self):
ssbo_editor = getattr(self, "ssbo_editor", None)

View File

@ -1014,6 +1014,13 @@ class PropertyHelpers:
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}")
@ -1025,17 +1032,27 @@ class PropertyHelpers:
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():
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
except Exception:
pass
for geom_path in node.find_all_matches("**/+GeomNode"):
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:
@ -1071,13 +1088,65 @@ class PropertyHelpers:
unique_materials = []
seen_keys = set()
for material in materials:
key = getattr(material, "this", None) or id(material)
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:
@ -1141,10 +1210,9 @@ class PropertyHelpers:
if previous_surface_type == 3:
opacity = self._get_material_opacity(material)
else:
# Entering transparent mode should keep the object fully
# visible by default. Reusing historical base-color alpha
# makes some assets appear to "disappear" immediately.
opacity = 0.92
# 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:
@ -1469,22 +1537,27 @@ class PropertyHelpers:
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
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["render_forward"] else "0",
"1" if options["render_gbuffer"] else "0",
"1" if options["parallax_mapping"] else "0",
"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 ""
@ -1509,12 +1582,6 @@ class PropertyHelpers:
surface_type = self._get_material_surface_type(material)
if surface_type != 3:
surface_type = 3
else:
# The RP forward transparent path in this editor build becomes
# visually unusable at exact opacity 1.0. Treat transparent
# mode as a slightly blended surface; users can switch back to
# "不透明" when they want a fully opaque result.
opacity_value = min(opacity_value, 0.92)
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity_value, float(emission.w)))
base_color = list(self._get_material_base_color(material))
@ -1525,30 +1592,76 @@ class PropertyHelpers:
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
for target_node in self._iter_material_state_nodes(node):
target_material = self._get_renderable_node_material(target_node) or material
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
# 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
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:
@ -2239,7 +2352,7 @@ class PropertyHelpers:
def start_transform_monitoring(self, node):
"""开始变换监控"""
if node and not node.isEmpty():
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
@ -2257,7 +2370,7 @@ class PropertyHelpers:
def _update_last_transform_values(self):
"""更新最后记录的变换值"""
if self._monitored_node and not self._monitored_node.isEmpty():
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()
@ -2274,7 +2387,10 @@ class PropertyHelpers:
def _check_transform_changes(self):
"""检查变换变化"""
if not self._transform_monitoring or not self._monitored_node:
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:

View File

@ -49,7 +49,7 @@ class RuntimeActions:
return None
def _create_simple_gui_button(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建简单的GUI按钮不依赖QT树形控件"""
"""创建简单的GUI按钮"""
try:
from direct.gui.DirectGui import DirectButton
@ -178,7 +178,7 @@ class RuntimeActions:
return None
def _create_simple_gui_label(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建简单的GUI标签不依赖QT树形控件"""
"""创建简单的GUI标签"""
try:
from direct.gui.DirectGui import DirectLabel
@ -225,7 +225,7 @@ class RuntimeActions:
return None
def _create_simple_gui_entry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建简单的GUI输入框不依赖QT树形控件"""
"""创建简单的GUI输入框"""
try:
from direct.gui.DirectGui import DirectEntry
@ -274,7 +274,7 @@ class RuntimeActions:
return None
def _create_simple_gui_image(self, pos=(0, 0, 0), image_path=None, size=2):
"""创建简单的GUI图片不依赖QT树形控件"""
"""创建简单的GUI图片"""
try:
from direct.gui.DirectGui import DirectFrame
from panda3d.core import Filename

View File

@ -104,7 +104,7 @@ class ScriptPanels:
def _draw_script_panel(self):
"""绘制脚本管理面板与Qt版本功能一致"""
"""绘制脚本管理面板"""
# 使用面板类型的窗口标志支持docking
flags = self.style_manager.get_window_flags("panel")

View File

@ -0,0 +1,8 @@
{
"name": "新项目_副本",
"path": "D:\\IMGUI\\EG\\新项目_副本",
"last_modified": "2026-03-13 16:55:57",
"scene_file": "scenes\\scene.bam",
"created_at": "2026-03-13 16:55:57",
"version": "1.0"
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB