保存模型子级模型位置移动

This commit is contained in:
Hector 2026-03-18 14:43:18 +08:00
parent 4635458300
commit 74b6a3307c
9 changed files with 313 additions and 82 deletions

View File

@ -261,7 +261,7 @@ class SceneManagerIOMixin:
all_nodes.extend(self.Spotlight)
all_nodes.extend(self.Pointlight)
# SSBO模式下先把运行时编辑后的层变换同步回source_model_root
# SSBO模式下先把运行时编辑后的变换同步回source_model_root
# 再从source树保存避免把chunk_*运行时结构写入scene.bam。
ssbo_editor = getattr(self.world, "ssbo_editor", None)
if ssbo_editor:

View File

@ -407,75 +407,78 @@ class SSBOEditor:
return list(root_node.get("children", []))
def _snapshot_top_level_transforms_to_source_root(self):
"""Persist current top-level imported model transforms back into the source scene root."""
"""Persist current runtime transforms back into the source scene tree."""
if not self.controller or not self.model or not self.source_model_root:
return
source_children = {}
for child in self._iter_children(self.source_model_root):
try:
source_children[child.get_name()] = child
except Exception:
try:
source_children[child.getName()] = child
except Exception:
continue
for key in self._get_top_level_group_keys():
display_name = self.controller.display_names.get(key, key)
source_child = source_children.get(display_name)
if not source_child:
grouped_entries = {}
for gid, obj_np in self.controller.id_to_object_np.items():
if not self._node_is_valid(obj_np):
continue
group_ids = self.controller.name_to_ids.get(key, [])
if not group_ids:
owner_key = self.controller.id_to_name.get(gid)
if not owner_key or owner_key == getattr(self.controller, "tree_root_key", None):
continue
representative_id = None
for gid in group_ids:
obj_np = self.controller.id_to_object_np.get(gid)
if obj_np and not obj_np.is_empty():
representative_id = gid
break
if representative_id is None:
source_node = self._resolve_source_node_by_tree_key(owner_key)
if not self._node_is_valid(source_node):
continue
try:
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].get_mat(self.model))
current_net_mat = LMatrix4f(obj_np.get_mat(self.model))
except Exception:
try:
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].getMat(self.model))
current_net_mat = LMatrix4f(obj_np.getMat(self.model))
except Exception:
continue
if representative_id >= len(self.controller.global_transforms):
existing_entry = grouped_entries.get(owner_key)
if existing_entry is None:
grouped_entries[owner_key] = {
"source_node": source_node,
"current_net_mat": current_net_mat,
}
for owner_key in sorted(grouped_entries.keys(), key=lambda key: str(key).count("/")):
entry = grouped_entries.get(owner_key) or {}
source_node = entry.get("source_node")
current_net_mat = entry.get("current_net_mat")
if not self._node_is_valid(source_node) or current_net_mat is None:
continue
original_mat = LMatrix4f(self.controller.global_transforms[representative_id])
inv_original = LMatrix4f(original_mat)
try:
inv_original.invertInPlace()
source_parent = source_node.get_parent()
except Exception:
try:
inv_original.invert_in_place()
source_parent = source_node.getParent()
except Exception:
continue
source_parent = None
base_child_mat = self._source_child_base_mats.get(display_name)
if base_child_mat is None:
parent_net_mat = LMatrix4f.ident_mat()
if self._node_is_valid(source_parent) and source_parent != self.source_model_root:
try:
base_child_mat = LMatrix4f(source_child.get_mat())
parent_net_mat = LMatrix4f(source_parent.get_mat(self.source_model_root))
except Exception:
try:
base_child_mat = LMatrix4f(source_child.getMat())
parent_net_mat = LMatrix4f(source_parent.getMat(self.source_model_root))
except Exception:
continue
parent_net_mat = LMatrix4f.ident_mat()
delta_mat = current_mat * inv_original
inv_parent_mat = LMatrix4f(parent_net_mat)
try:
source_child.set_mat(delta_mat * base_child_mat)
inv_parent_mat.invertInPlace()
except Exception:
try:
source_child.setMat(delta_mat * base_child_mat)
inv_parent_mat.invert_in_place()
except Exception:
continue
local_mat = current_net_mat * inv_parent_mat
try:
source_node.set_mat(local_mat)
except Exception:
try:
source_node.setMat(local_mat)
except Exception:
continue
@ -1794,14 +1797,22 @@ class SSBOEditor:
def on_mouse_click(self):
io = imgui.get_io()
if io.want_capture_mouse: return
# Skip SSBO picking when user is interacting with the TransformGizmo,
# otherwise pick_object would clear the selection and detach the gizmo
# before the gizmo's own mouse handler fires.
if self._transform_gizmo and self._transform_gizmo.is_hovering:
return
if self.base.mouseWatcherNode.has_mouse():
try:
win_width, win_height = self.base.win.getSize()
mpos = self.base.mouseWatcherNode.get_mouse()
window_x = (float(mpos.x) + 1.0) * 0.5 * float(win_width)
window_y = (1.0 - float(mpos.y)) * 0.5 * float(win_height)
process_imgui_click = getattr(self.base, "processImGuiMouseClick", None)
if callable(process_imgui_click) and process_imgui_click(window_x, window_y):
return
except Exception:
pass
self._sync_pick_model_transform()
self._refresh_ssbo_proxy_center()
mpos = self.base.mouseWatcherNode.get_mouse()

View File

@ -420,29 +420,22 @@ class AppActions:
def _is_mouse_over_imgui(self):
"""检测鼠标是否在ImGui窗口上"""
try:
# 检查是否有任何ImGui窗口想要捕获鼠标
if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
point = self._get_mouse_window_point()
if point and self._is_point_in_known_imgui_rects(point):
return True
# 检查鼠标是否在任何ImGui窗口内
mouse_pos = self.mouseWatcherNode.getMouse()
if not mouse_pos:
return False
# 简单的边界检查(可以根据需要扩展)
display_size = imgui.get_io().display_size
mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2
mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)
# 检查是否在常见的ImGui界面区域内
# 这里可以根据实际的ImGui窗口位置进行更精确的检测
if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏)
return True
if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏)
return True
if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域
return True
try:
if imgui.is_any_item_active() or imgui.is_any_item_hovered():
return True
except Exception:
pass
try:
if point and imgui.is_any_window_hovered() and self._is_point_in_known_imgui_rects(point):
return True
except Exception:
pass
return False
except Exception as e:
print(f"ImGui界面检测失败: {e}")
@ -452,28 +445,64 @@ class AppActions:
def processImGuiMouseClick(self, x, y):
"""处理ImGui鼠标点击事件返回是否消费了该事件"""
try:
# ImGui优先策略如果ImGui想要捕获鼠标则由ImGui处理
if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
point = (float(x), float(y))
if self._is_point_in_known_imgui_rects(point):
return True
# 检查是否有任何ImGui窗口悬停
try:
if imgui.is_any_window_hovered():
if imgui.is_any_item_active() or imgui.is_any_item_hovered():
return True
except AttributeError:
# 如果方法不存在,跳过这个检查
except Exception:
pass
# 检查鼠标是否在ImGui界面区域内
if self._is_mouse_over_imgui():
return True
# 如果以上条件都不满足则让3D场景处理该事件
try:
if imgui.is_any_window_hovered() and self._is_point_in_known_imgui_rects(point):
return True
except Exception:
pass
return False
except Exception as e:
print(f"ImGui鼠标点击处理失败: {e}")
return False
def _get_mouse_window_point(self):
try:
if not self.mouseWatcherNode.hasMouse():
return None
mouse_pos = self.mouseWatcherNode.getMouse()
if not mouse_pos:
return None
display_size = imgui.get_io().display_size
return (
float(mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2),
float(display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)),
)
except Exception:
return None
@staticmethod
def _point_in_rect(point, rect):
if not point or not rect:
return False
x, y = point
rx, ry, rw, rh = rect
return rx <= x <= rx + rw and ry <= y <= ry + rh
def _is_point_in_known_imgui_rects(self, point):
for rect_name in (
"_resource_manager_window_rect",
"_scene_tree_window_rect",
"_property_panel_window_rect",
"_script_panel_window_rect",
"_console_window_rect",
"_toolbar_window_rect",
):
if self._point_in_rect(point, getattr(self, rect_name, None)):
return True
return False
# ==================== 消息系统 ====================

View File

@ -19,6 +19,7 @@ class EditorPanelsLeftMixin:
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags) as (_, opened):
if not opened:
self.app.showSceneTree = False
self.app._scene_tree_window_rect = None
return
self.app.showSceneTree = opened
@ -430,6 +431,7 @@ class EditorPanelsLeftMixin:
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags) as (_, opened):
if not opened:
self.app.showResourceManager = False
self.app._resource_manager_window_rect = None
return
self.app.showResourceManager = opened

View File

@ -143,9 +143,18 @@ class EditorPanelsRightMixin(
with self.app.style_manager.begin_styled_window("属性面板", self.app.showPropertyPanel, flags) as (_, opened):
if not opened:
self.app.showPropertyPanel = False
self.app._property_panel_window_rect = None
return
self.app.showPropertyPanel = opened
window_pos = imgui.get_window_pos()
window_size = imgui.get_window_size()
self.app._property_panel_window_rect = (
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
)
# --- LUI Component Properties ---
# 优先检查 LUI 组件选择

View File

@ -8,7 +8,19 @@ class EditorPanelsRightMaterialMixin:
self._material_edit_sessions = {}
return self._material_edit_sessions
def _ensure_node_materials_are_editable(self, node):
ensure_unique_fn = getattr(self.app, "_ensure_unique_materials_for_node", None)
if callable(ensure_unique_fn):
try:
materials = ensure_unique_fn(node)
if materials:
return materials
except Exception:
pass
return self.app._get_node_materials(node)
def _begin_material_edit_session(self, node, session_key):
self._ensure_node_materials_are_editable(node)
sessions = self._ensure_material_edit_sessions()
sessions.setdefault(session_key, self.app._capture_node_material_snapshot(node))
@ -42,12 +54,14 @@ class EditorPanelsRightMaterialMixin:
self._record_material_snapshot_command(node, before_snapshot, after_snapshot)
def _apply_material_change_with_history(self, node, apply_callback):
self._ensure_node_materials_are_editable(node)
before_snapshot = self.app._capture_node_material_snapshot(node)
apply_callback()
after_snapshot = self.app._capture_node_material_snapshot(node)
self._record_material_snapshot_command(node, before_snapshot, after_snapshot)
def _select_texture_for_material_with_history(self, node, material, texture_type):
self._ensure_node_materials_are_editable(node)
before_snapshot = self.app._capture_node_material_snapshot(node)
changed = self._select_texture_for_material(node, material, texture_type)
if not changed:
@ -58,7 +72,7 @@ class EditorPanelsRightMaterialMixin:
def _draw_appearance_properties(self, node):
"""绘制材质属性Unity风格主材质入口"""
materials = self.app._get_node_materials(node)
materials = self._ensure_node_materials_are_editable(node)
if not materials:
fallback_material = self.app._ensure_material_for_node(node)
materials = [fallback_material] if fallback_material else []
@ -168,7 +182,7 @@ class EditorPanelsRightMaterialMixin:
def _draw_material_properties(self, node):
"""绘制材质属性"""
materials = self.app._get_node_materials(node)
materials = self._ensure_node_materials_are_editable(node)
if not materials:
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质")

View File

@ -286,9 +286,18 @@ class EditorPanelsTopMixin:
with self.app.style_manager.begin_styled_window("工具栏", self.app.showToolbar, flags) as (_, opened):
if not opened:
self.app.showToolbar = False
self.app._toolbar_window_rect = None
return
self.app.showToolbar = opened
window_pos = imgui.get_window_pos()
window_size = imgui.get_window_size()
self.app._toolbar_window_rect = (
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
)
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

View File

@ -1206,6 +1206,142 @@ class PropertyHelpers:
unique_materials.append(material)
return unique_materials
def _clone_material_for_node(self, material, node):
"""Clone one material so edits stay scoped to the selected node only."""
try:
from panda3d.core import Material
cloned_material = Material(material)
source_name = ""
try:
source_name = material.get_name() or ""
except Exception:
try:
source_name = material.getName() or ""
except Exception:
source_name = ""
node_name = self._get_node_name(node, "node") if hasattr(self, "_get_node_name") else "node"
clone_name = f"{source_name or 'material'}__editable__{node_name}"
try:
cloned_material.set_name(clone_name)
except Exception:
try:
cloned_material.setName(clone_name)
except Exception:
pass
return cloned_material
except Exception:
return material
def _ensure_unique_materials_for_node(self, node):
"""Detach shared/inherited materials so editing one child does not affect siblings."""
try:
from panda3d.core import MaterialAttrib
if not node or node.isEmpty():
return []
materials = self._get_node_materials(node)
if not materials:
return []
current_signature = tuple(
self._get_material_identity_key(material)
for material in materials
if material is not None
)
try:
stored_signature = tuple(node.getPythonTag("_editable_material_signature"))
except Exception:
stored_signature = None
if stored_signature == current_signature:
return materials
node_material_key = None
try:
if node.hasMaterial():
node_material_key = self._get_material_identity_key(node.getMaterial())
except Exception:
node_material_key = None
changed = False
cloned_materials = []
for material in materials:
if material is None:
continue
cloned_material = self._clone_material_for_node(material, node)
cloned_materials.append(cloned_material)
source_key = self._get_material_identity_key(material)
if cloned_material is material:
continue
try:
if node_material_key is not None and source_key == node_material_key:
node.setMaterial(cloned_material, 1)
changed = True
except Exception:
pass
geom_paths = self._get_geom_paths_for_material(node, material)
if not geom_paths and node_material_key is None:
try:
node.setMaterial(cloned_material, 1)
changed = True
except Exception:
pass
for geom_path in geom_paths:
try:
geom_path.setMaterial(cloned_material, 1)
changed = True
except Exception:
pass
try:
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(cloned_material)))
except Exception:
pass
try:
geom_node = geom_path.node()
for geom_index in range(geom_node.getNumGeoms()):
geom_state = geom_node.getGeomState(geom_index)
geom_node.setGeomState(
geom_index,
geom_state.setAttrib(MaterialAttrib.make(cloned_material)),
)
changed = True
except Exception:
pass
if changed:
self._invalidate_material_render_cache()
latest_materials = self._get_node_materials(node)
if not latest_materials:
latest_materials = cloned_materials
latest_signature = tuple(
self._get_material_identity_key(material)
for material in latest_materials
if material is not None
)
try:
node.setPythonTag("_editable_material_signature", latest_signature)
except Exception:
pass
return latest_materials
except Exception as e:
print(f"隔离节点材质实例失败: {e}")
return self._get_node_materials(node)
def _get_material_identity_key(self, material):
try:
return getattr(material, "this", None) or id(material)
@ -1932,6 +2068,11 @@ class PropertyHelpers:
def _reset_material(self, node):
"""重置节点材质"""
try:
materials = self._ensure_unique_materials_for_node(node)
if not materials:
fallback_material = self._ensure_material_for_node(node)
materials = [fallback_material] if fallback_material else []
# 先清理贴图与effect标签避免后续再次设置贴图时被旧状态污染
try:
self._clear_all_textures(node)
@ -1942,8 +2083,6 @@ class PropertyHelpers:
node.clearTag("material_render_effect_signature")
except Exception:
pass
materials = list(node.find_all_materials())
for material in materials:
# 重置为默认材质属性

View File

@ -25,9 +25,18 @@ class ScriptPanels:
with self.style_manager.begin_styled_window("控制台", self.app.showConsole, flags) as (_, opened):
if not opened:
self.app.showConsole = False
self.app._console_window_rect = None
return
self.app.showConsole = opened
window_pos = imgui.get_window_pos()
window_size = imgui.get_window_size()
self.app._console_window_rect = (
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
)
imgui.text("控制台输出")
imgui.separator()
@ -111,9 +120,18 @@ class ScriptPanels:
with self.style_manager.begin_styled_window("脚本管理", self.app.showScriptPanel, flags) as (_, opened):
if not opened:
self.app.showScriptPanel = False
self.app._script_panel_window_rect = None
return
self.app.showScriptPanel = opened
window_pos = imgui.get_window_pos()
window_size = imgui.get_window_size()
self.app._script_panel_window_rect = (
float(window_pos.x),
float(window_pos.y),
float(window_size.x),
float(window_size.y),
)
# 1. 脚本系统状态组
self._draw_script_status_group()