Fix transparency shader responses

This commit is contained in:
ayuan9957 2026-03-15 16:19:32 +08:00
parent 7e6b1d54d9
commit 49d630f967
20 changed files with 1691 additions and 739 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

@ -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()
@ -175,28 +202,18 @@ class SelectionSystem:
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 灯光对象位置。"""
@ -2139,7 +2156,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 +2167,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 +2182,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} 创建选择框和坐标轴...")
@ -2315,13 +2332,15 @@ 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):

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

@ -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

@ -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