From 74b6a3307c13f8d19066eacc1dac1513edf2a632 Mon Sep 17 00:00:00 2001 From: Hector <145347438+hudomn@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:43:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=A8=A1=E5=9E=8B=E5=AD=90?= =?UTF-8?q?=E7=BA=A7=E6=A8=A1=E5=9E=8B=E4=BD=8D=E7=BD=AE=E7=A7=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scene/scene_manager_io_mixin.py | 2 +- ssbo_component/ssbo_editor.py | 95 +++++++------- ui/panels/app_actions.py | 99 +++++++++------ ui/panels/editor_panels_left.py | 2 + ui/panels/editor_panels_right.py | 9 ++ ui/panels/editor_panels_right_material.py | 18 ++- ui/panels/editor_panels_top.py | 9 ++ ui/panels/property_helpers.py | 143 +++++++++++++++++++++- ui/panels/script_panels.py | 18 +++ 9 files changed, 313 insertions(+), 82 deletions(-) diff --git a/scene/scene_manager_io_mixin.py b/scene/scene_manager_io_mixin.py index 01e46c65..4564b0fc 100644 --- a/scene/scene_manager_io_mixin.py +++ b/scene/scene_manager_io_mixin.py @@ -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: diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index 5739669b..417f966d 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -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() diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index f86cafbd..38730e7b 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -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 # ==================== 消息系统 ==================== diff --git a/ui/panels/editor_panels_left.py b/ui/panels/editor_panels_left.py index d7702a5d..0864ede2 100644 --- a/ui/panels/editor_panels_left.py +++ b/ui/panels/editor_panels_left.py @@ -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 diff --git a/ui/panels/editor_panels_right.py b/ui/panels/editor_panels_right.py index 5db184ba..3192eba6 100644 --- a/ui/panels/editor_panels_right.py +++ b/ui/panels/editor_panels_right.py @@ -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 组件选择 diff --git a/ui/panels/editor_panels_right_material.py b/ui/panels/editor_panels_right_material.py index 03b5a1d4..4871cd60 100644 --- a/ui/panels/editor_panels_right_material.py +++ b/ui/panels/editor_panels_right_material.py @@ -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), "无材质") diff --git a/ui/panels/editor_panels_top.py b/ui/panels/editor_panels_top.py index 0e40fb59..e5789054 100644 --- a/ui/panels/editor_panels_top.py +++ b/ui/panels/editor_panels_top.py @@ -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 diff --git a/ui/panels/property_helpers.py b/ui/panels/property_helpers.py index 90aed919..dd534e65 100644 --- a/ui/panels/property_helpers.py +++ b/ui/panels/property_helpers.py @@ -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: # 重置为默认材质属性 diff --git a/ui/panels/script_panels.py b/ui/panels/script_panels.py index 68710ad1..96fd943d 100644 --- a/ui/panels/script_panels.py +++ b/ui/panels/script_panels.py @@ -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()