diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py index e18f63f7..082c8489 100644 --- a/ssbo_component/ssbo_editor.py +++ b/ssbo_component/ssbo_editor.py @@ -735,7 +735,8 @@ class SSBOEditor: return False try: - source_node.set_mat(self.source_model_root, current_net_mat) + reference_node = self._get_transform_snapshot_reference_node(source_node) + source_node.set_mat(reference_node, current_net_mat) source_node.setTag("scene_transform_dirty", "true") try: self._cache_top_level_source_child_base_mat(source_node, source_node.get_mat()) @@ -747,7 +748,7 @@ class SSBOEditor: return True except Exception: try: - source_node.setMat(self.source_model_root, current_net_mat) + source_node.setMat(reference_node, current_net_mat) source_node.setTag("scene_transform_dirty", "true") try: self._cache_top_level_source_child_base_mat(source_node, source_node.get_mat()) @@ -807,7 +808,8 @@ class SSBOEditor: # Use robust Panda3D set_mat(ref, mat) to handle all coordinate conversions. # This ensures the source node's net transform relative to the source root # exactly matches the runtime node's net transform relative to self.model. - source_node.set_mat(self.source_model_root, current_net_mat) + reference_node = self._get_transform_snapshot_reference_node(source_node) + source_node.set_mat(reference_node, current_net_mat) source_node.setTag("scene_transform_dirty", "true") # Update top-level child baseline cache for structural sync @@ -1093,7 +1095,8 @@ class SSBOEditor: try: # Corrected: use Panda3D native world-to-local projection relative to source root - source_node.set_mat(self.source_model_root, current_net_mat) + reference_node = self._get_transform_snapshot_reference_node(source_node) + source_node.set_mat(reference_node, current_net_mat) source_node.setTag("scene_transform_dirty", "true") try: self._cache_top_level_source_child_base_mat(source_node, source_node.get_mat()) @@ -1132,6 +1135,48 @@ class SSBOEditor: return None return node + def _is_node_in_subtree(self, node, subtree_root): + if not self._node_is_valid(node) or not self._node_is_valid(subtree_root): + return False + current = node + while self._node_is_valid(current): + if current == subtree_root: + return True + try: + current = current.get_parent() + except Exception: + try: + current = current.getParent() + except Exception: + return False + return False + + def _get_transform_snapshot_reference_node(self, source_node): + """ + Pick the correct reference node when snapshotting runtime transforms. + + In the single-top-level-model path, the runtime moves that only top-level + child transform onto self.model. Descendant runtime matrices are then + relative to the sole source child, not to source_model_root. Saving those + descendants against source_model_root bakes the inverse root transform into + children, so reopening restores children but loses the whole-model pose. + """ + source_root = self.source_model_root + if not self._node_is_valid(source_node) or not self._node_is_valid(source_root): + return source_root + + source_children = self._get_source_root_children() + if len(source_children) != 1 or not self._node_is_valid(source_children[0]): + return source_root + + sole_source_child = source_children[0] + if source_node == sole_source_child: + return source_root + + if self._is_node_in_subtree(source_node, sole_source_child): + return sole_source_child + return source_root + def _resolve_tree_key_for_source_node(self, node): """Resolve a source-tree NodePath back to the controller tree key.""" source_root = self._ensure_source_model_root()