Fix SSBO picking sync and deletion cleanup

This commit is contained in:
Your Name 2026-02-27 15:39:23 +08:00
parent 1fd7e1d7ac
commit 756db5b010
3 changed files with 95 additions and 7 deletions

View File

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

View File

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

View File

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