Fix transparency shader responses
This commit is contained in:
parent
7e6b1d54d9
commit
49d630f967
11
.codex/environments/environment-2.toml
Normal file
11
.codex/environments/environment-2.toml
Normal 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"
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
@ -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)):
|
||||
"""绘制带图标的文本按钮"""
|
||||
|
||||
@ -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):
|
||||
|
||||
11
imgui.ini
11
imgui.ini
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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在缓存中,也需要清理
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
8
新项目_副本/project.json
Normal file
8
新项目_副本/project.json
Normal 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"
|
||||
}
|
||||
BIN
新项目_副本/scenes/scene.bam
Normal file
BIN
新项目_副本/scenes/scene.bam
Normal file
Binary file not shown.
BIN
新项目_副本/新项目_副本.png
Normal file
BIN
新项目_副本/新项目_副本.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Loading…
Reference in New Issue
Block a user