From 756db5b010f4fdecb34e7782278a6e38b8984d4c Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 27 Feb 2026 15:39:23 +0800 Subject: [PATCH] Fix SSBO picking sync and deletion cleanup --- ssbo_component/ssbo_controller.py | 2 +- ssbo_component/ssbo_editor.py | 91 +++++++++++++++++++++++++++++-- ui/panels/app_actions.py | 9 +++ 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/ssbo_component/ssbo_controller.py b/ssbo_component/ssbo_controller.py index 03e6c9ab..4c94495b 100644 --- a/ssbo_component/ssbo_controller.py +++ b/ssbo_component/ssbo_controller.py @@ -215,7 +215,7 @@ class ObjectController: # Build hierarchy metadata first so UI can mirror source model tree. self._build_scene_tree(model) - root_name = (model.get_name() or "scene") + "_hybrid" + root_name = model.get_name() or "scene" scene_root = NodePath(root_name) pick_root = NodePath(root_name + "_pick") self.model = scene_root diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index fa7845db..d440117e 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -77,6 +77,7 @@ class SSBOEditor: self.keys = {} self.pick_mask = BitMask32.bit(29) self.pick_buffer = None + self._empty_pick_scene = NodePath("ssbo_pick_empty") # Initialize ImGui Backend if not already present if not hasattr(self.base, 'imgui_backend'): @@ -272,8 +273,9 @@ class SSBOEditor: frag_src = f.read().replace('\r', '') pick_shader = Shader.make(Shader.SL_GLSL, vert_src, frag_src) pick_scene = getattr(self.controller, "pick_model", None) or self.model - pick_scene.show(self.pick_mask) - self.pick_cam.set_scene(pick_scene) + if pick_scene and not pick_scene.is_empty(): + pick_scene.show(self.pick_mask) + self.pick_cam.set_scene(pick_scene or self._empty_pick_scene) initial_state = NodePath("initial") initial_state.set_shader(pick_shader, 100) # Remove global SSBO input, Chunks have their own inputs @@ -288,8 +290,36 @@ class SSBOEditor: self.pick_buffer.set_clear_color(Vec4(0, 0, 0, 0)) self.pick_buffer.set_clear_color_active(True) + def _is_model_attached(self): + """Whether the SSBO render root is still attached to scene graph.""" + if not self.model or self.model.is_empty(): + return False + parent = self.model.get_parent() + return bool(parent) and not parent.is_empty() + + def _sync_pick_scene_binding(self): + """Switch pick camera scene based on current model attachment state.""" + if not hasattr(self, "pick_cam") or not self.pick_cam: + return + + if self._is_model_attached() and self.controller: + target_scene = getattr(self.controller, "pick_model", None) or self.model + else: + target_scene = self._empty_pick_scene + + if not target_scene: + target_scene = self._empty_pick_scene + + if self.pick_cam.get_scene() != target_scene: + self.pick_cam.set_scene(target_scene) + def pick_object(self, mx, my): if not self.pick_buffer: return + self._sync_pick_scene_binding() + if not self._is_model_attached(): + if self.selected_ids or getattr(self, "_group_proxy", None): + self.clear_selection() + return self.pick_lens.set_fov(0.1) self.pick_lens.set_film_offset(0, 0) @@ -359,17 +389,54 @@ class SSBOEditor: self._sync_pick_transforms() return task.cont + def _matrices_close(self, a, b, eps=1e-5): + """Small helper for robust matrix change detection.""" + for r in range(4): + ra = a.get_row(r) + rb = b.get_row(r) + if (abs(ra[0] - rb[0]) > eps or + abs(ra[1] - rb[1]) > eps or + abs(ra[2] - rb[2]) > eps or + abs(ra[3] - rb[3]) > eps): + return False + return True + + def _sync_pick_root_transform(self): + """ + Keep pick root aligned with the render root transform. + This covers transforms applied to the whole imported model + (for example, moving box.glb from scene hierarchy). + """ + if not self.controller or not self.model or not self._is_model_attached(): + return + + pick_root = getattr(self.controller, "pick_model", None) + if not pick_root: + return + + if self.model.is_empty() or pick_root.is_empty(): + return + + pick_root.set_mat(self.base.render, self.model.get_mat(self.base.render)) + def _sync_pick_transforms(self): """Sync pick model transforms to match render model transforms.""" if not self.controller: return + self._sync_pick_root_transform() for gid in self.selected_ids: obj_np = self.controller.id_to_object_np.get(gid) pick_np = self.controller.id_to_pick_np.get(gid) if obj_np and pick_np and not obj_np.is_empty() and not pick_np.is_empty(): - # pick_np is direct child of pick_root (identity transform), - # so local mat = world-space mat. - pick_np.set_mat(obj_np.get_mat(self.base.render)) + obj_world_mat = obj_np.get_mat(self.base.render) + pick_world_mat = pick_np.get_mat(self.base.render) + if not self._matrices_close(obj_world_mat, pick_world_mat): + # Sync by world transform so this stays correct even when + # the model root itself has been moved in scene hierarchy. + pick_np.set_mat(self.base.render, obj_world_mat) + 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 def clear_selection(self): self._stop_pick_sync_task() @@ -381,6 +448,15 @@ class SSBOEditor: if self._transform_gizmo: self._transform_gizmo.detach() + def on_model_deleted(self, deleted_node): + """Called by app deletion flow when SSBO root model is deleted.""" + if not deleted_node or deleted_node.is_empty() or not self.model: + return + if deleted_node != self.model: + return + self.clear_selection() + self._sync_pick_scene_binding() + def _cleanup_group_proxy(self): """Reparent objects back to their chunk and remove the group proxy.""" proxy = getattr(self, '_group_proxy', None) @@ -392,7 +468,7 @@ class SSBOEditor: obj_np = self.controller.id_to_object_np.get(gid) pick_np = self.controller.id_to_pick_np.get(gid) if obj_np and pick_np and not obj_np.is_empty() and not pick_np.is_empty(): - pick_np.set_mat(obj_np.get_mat(self.base.render)) + pick_np.set_mat(self.base.render, obj_np.get_mat(self.base.render)) 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 @@ -559,6 +635,9 @@ class SSBOEditor: def update_task(self, task): dt = globalClock.getDt() io = imgui.get_io() + self._sync_pick_scene_binding() + # Scene-hierarchy transforms may move the whole SSBO model root; keep pick root in sync. + self._sync_pick_root_transform() if io.want_capture_keyboard: return task.cont diff --git a/ui/panels/app_actions.py b/ui/panels/app_actions.py index a9d6a855..c5f883e1 100644 --- a/ui/panels/app_actions.py +++ b/ui/panels/app_actions.py @@ -735,6 +735,9 @@ class AppActions: node_name = node.getName() or "未命名节点" parent = node.getParent() + ssbo_editor = getattr(self, "ssbo_editor", None) + ssbo_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None + deleting_ssbo_root = bool(ssbo_model and (node == ssbo_model)) # 创建删除命令 if hasattr(self, 'command_manager') and self.command_manager: @@ -747,6 +750,12 @@ class AppActions: print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}") self._perform_node_cleanup(node) node.removeNode() + + if deleting_ssbo_root and ssbo_editor: + try: + ssbo_editor.on_model_deleted(node) + except Exception as e: + print(f"[SSBO] 删除模型后清理失败: {e}") print(f"[删除] 成功删除节点: {node_name}") return True