保存、打开项目优化
This commit is contained in:
parent
062bd4e720
commit
5c0a61d253
Binary file not shown.
|
Before Width: | Height: | Size: 785 KiB |
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "smoke_project",
|
|
||||||
"path": "D:\\IMGUI\\EG\\eg_smoke_x76dg4m2\\smoke_project",
|
|
||||||
"created_at": "2026-03-15 17:36:38",
|
|
||||||
"last_modified": "2026-03-15 17:36:39",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"engine_version": "1.0.0",
|
|
||||||
"scene_file": "scenes\\scene.bam"
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 785 KiB |
24
imgui.ini
24
imgui.ini
@ -31,19 +31,19 @@ DockId=0x0000000D,0
|
|||||||
|
|
||||||
[Window][场景树]
|
[Window][场景树]
|
||||||
Pos=0,20
|
Pos=0,20
|
||||||
Size=339,1084
|
Size=339,1008
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000007,0
|
DockId=0x00000007,0
|
||||||
|
|
||||||
[Window][属性面板]
|
[Window][属性面板]
|
||||||
Pos=1694,20
|
Pos=1506,20
|
||||||
Size=346,1084
|
Size=346,1008
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000002,0
|
||||||
|
|
||||||
[Window][控制台]
|
[Window][控制台]
|
||||||
Pos=341,705
|
Pos=341,629
|
||||||
Size=1351,399
|
Size=1163,399
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,1
|
DockId=0x00000006,1
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ Collapsed=0
|
|||||||
|
|
||||||
[Window][WindowOverViewport_11111111]
|
[Window][WindowOverViewport_11111111]
|
||||||
Pos=0,20
|
Pos=0,20
|
||||||
Size=2040,1084
|
Size=1852,1008
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][测试窗口1]
|
[Window][测试窗口1]
|
||||||
@ -78,17 +78,17 @@ Size=93,65
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][新建项目]
|
[Window][新建项目]
|
||||||
Pos=1080,525
|
Pos=824,402
|
||||||
Size=400,300
|
Size=400,300
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][选择路径]
|
[Window][选择路径]
|
||||||
Pos=720,302
|
Pos=626,264
|
||||||
Size=600,500
|
Size=600,500
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][打开项目]
|
[Window][打开项目]
|
||||||
Pos=770,352
|
Pos=676,314
|
||||||
Size=500,400
|
Size=500,400
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@ -98,8 +98,8 @@ Size=600,500
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][资源管理器]
|
[Window][资源管理器]
|
||||||
Pos=341,705
|
Pos=341,629
|
||||||
Size=1351,399
|
Size=1163,399
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,0
|
DockId=0x00000006,0
|
||||||
|
|
||||||
@ -226,7 +226,7 @@ Size=460,260
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Docking][Data]
|
[Docking][Data]
|
||||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2040,1084 Split=X
|
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1852,1008 Split=X
|
||||||
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=2212,1012 Split=X
|
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=2212,1012 Split=X
|
||||||
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051
|
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051
|
||||||
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1871,1084 Split=Y
|
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1871,1084 Split=Y
|
||||||
|
|||||||
@ -47,6 +47,102 @@ class ProjectManager:
|
|||||||
def _get_repo_root(self):
|
def _get_repo_root(self):
|
||||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
def _vector_differs(self, current_value, target_values, tolerance=1e-4):
|
||||||
|
try:
|
||||||
|
current_list = [float(current_value[i]) for i in range(3)]
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
current_list = [float(current_value.x), float(current_value.y), float(current_value.z)]
|
||||||
|
except Exception:
|
||||||
|
current_list = []
|
||||||
|
if len(current_list) < 3:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
target_list = [float(target_values[0]), float(target_values[1]), float(target_values[2])]
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
return any(abs(current_list[i] - target_list[i]) > tolerance for i in range(3))
|
||||||
|
|
||||||
|
def _log_render_runtime_stats(self, label):
|
||||||
|
"""Print renderer/task stats to compare manual import vs project-open paths."""
|
||||||
|
world = getattr(self, "world", None)
|
||||||
|
if not world:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
win = getattr(world, "win", None)
|
||||||
|
num_regions = win.getNumDisplayRegions() if win else 0
|
||||||
|
region_lines = []
|
||||||
|
if win:
|
||||||
|
for index in range(num_regions):
|
||||||
|
try:
|
||||||
|
dr = win.getDisplayRegion(index)
|
||||||
|
camera = dr.getCamera()
|
||||||
|
camera_name = "None"
|
||||||
|
if camera and not camera.isEmpty():
|
||||||
|
camera_name = camera.getName()
|
||||||
|
region_lines.append(
|
||||||
|
f"{index}:{dr.getSort()}:{int(bool(dr.isActive()))}:{camera_name}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
graphics_engine = getattr(world, "graphicsEngine", None)
|
||||||
|
num_windows = graphics_engine.getNumWindows() if graphics_engine else 0
|
||||||
|
|
||||||
|
render = getattr(world, "render", None)
|
||||||
|
camera_count = 0
|
||||||
|
special_camera_counts = {}
|
||||||
|
if render and not render.isEmpty():
|
||||||
|
for pattern in ("**/+Camera",):
|
||||||
|
try:
|
||||||
|
camera_count += render.find_all_matches(pattern).get_num_paths()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for camera_name in (
|
||||||
|
"gizmo_overlay_cam",
|
||||||
|
"pick_camera",
|
||||||
|
"selection_outline_mask_camera",
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
special_camera_counts[camera_name] = render.find_all_matches(
|
||||||
|
f"**/{camera_name}"
|
||||||
|
).get_num_paths()
|
||||||
|
except Exception:
|
||||||
|
special_camera_counts[camera_name] = 0
|
||||||
|
|
||||||
|
task_mgr = getattr(world, "taskMgr", None) or getattr(world, "task_mgr", None)
|
||||||
|
task_names = []
|
||||||
|
if task_mgr:
|
||||||
|
try:
|
||||||
|
for task in list(task_mgr.getTasks()):
|
||||||
|
try:
|
||||||
|
task_names.append(task.name)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
task_names = []
|
||||||
|
interesting_tasks = [
|
||||||
|
name for name in task_names
|
||||||
|
if (
|
||||||
|
"gizmo" in str(name).lower()
|
||||||
|
or "outline" in str(name).lower()
|
||||||
|
or "pick" in str(name).lower()
|
||||||
|
or "lui" in str(name).lower()
|
||||||
|
or "canvas" in str(name).lower()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[RenderStats:{label}] regions={num_regions} windows={num_windows} "
|
||||||
|
f"render_cameras={camera_count} special={special_camera_counts} "
|
||||||
|
f"interesting_tasks={interesting_tasks}"
|
||||||
|
)
|
||||||
|
if region_lines:
|
||||||
|
print(f"[RenderStats:{label}] region_detail={' | '.join(region_lines)}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def get_project_scripts_dir(self, project_path=None):
|
def get_project_scripts_dir(self, project_path=None):
|
||||||
project_path = os.path.normpath(project_path or self.current_project_path or "")
|
project_path = os.path.normpath(project_path or self.current_project_path or "")
|
||||||
if not project_path:
|
if not project_path:
|
||||||
@ -689,6 +785,10 @@ class ProjectManager:
|
|||||||
"""
|
"""
|
||||||
# print(f"\n[DEBUG] ===== 开始打开项目: {project_path} =====")
|
# print(f"\n[DEBUG] ===== 开始打开项目: {project_path} =====")
|
||||||
try:
|
try:
|
||||||
|
try:
|
||||||
|
self.world._scene_tree_epoch = int(getattr(self.world, "_scene_tree_epoch", 0) or 0) + 1
|
||||||
|
except Exception:
|
||||||
|
self.world._scene_tree_epoch = 1
|
||||||
if not project_path:
|
if not project_path:
|
||||||
print("错误: 项目路径不能为空")
|
print("错误: 项目路径不能为空")
|
||||||
return False
|
return False
|
||||||
@ -913,6 +1013,14 @@ class ProjectManager:
|
|||||||
scene_file_path = os.path.join(project_path, scene_entry["path"].replace("/", os.sep))
|
scene_file_path = os.path.join(project_path, scene_entry["path"].replace("/", os.sep))
|
||||||
save_json(scene_file_path, scene_description)
|
save_json(scene_file_path, scene_description)
|
||||||
self._write_scene_sidecars(scene_paths, gui_snapshot, lui_snapshot)
|
self._write_scene_sidecars(scene_paths, gui_snapshot, lui_snapshot)
|
||||||
|
try:
|
||||||
|
print(
|
||||||
|
f"[SceneSave] roots={len(root_nodes)} nodes={len(scene_description.get('nodes', []) or [])} "
|
||||||
|
f"assets={len(scene_description.get('referenced_asset_guids', []) or [])} "
|
||||||
|
f"scripts={len(scene_description.get('referenced_script_guids', []) or [])}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return scene_description
|
return scene_description
|
||||||
|
|
||||||
def _load_scene_description_into_editor(self, scene_description, project_path, scene_entry=None):
|
def _load_scene_description_into_editor(self, scene_description, project_path, scene_entry=None):
|
||||||
@ -921,6 +1029,14 @@ class ProjectManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
self._clearCurrentScene()
|
self._clearCurrentScene()
|
||||||
|
self._log_render_runtime_stats("after_clear")
|
||||||
|
try:
|
||||||
|
print(
|
||||||
|
f"[SceneOpen] scene_nodes={len(scene_description.get('nodes', []) or [])} "
|
||||||
|
f"assets={len(scene_description.get('referenced_asset_guids', []) or [])}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
ssbo_loaded = self._load_scene_description_via_ssbo(scene_description, project_path, asset_database)
|
ssbo_loaded = self._load_scene_description_via_ssbo(scene_description, project_path, asset_database)
|
||||||
|
|
||||||
scene_root, keep_nodes = self._build_scene_root_from_description(
|
scene_root, keep_nodes = self._build_scene_root_from_description(
|
||||||
@ -945,6 +1061,22 @@ class ProjectManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
root_children = []
|
root_children = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||||
|
runtime_model = getattr(ssbo_editor, "model", None) if ssbo_editor else None
|
||||||
|
runtime_desc = 0
|
||||||
|
if runtime_model and not runtime_model.isEmpty():
|
||||||
|
try:
|
||||||
|
runtime_desc = runtime_model.find_all_matches("**").get_num_paths()
|
||||||
|
except Exception:
|
||||||
|
runtime_desc = 0
|
||||||
|
print(
|
||||||
|
f"[SceneOpen] ssbo_loaded={ssbo_loaded} keep_nodes={len(keep_nodes)} "
|
||||||
|
f"scene_root_children={len(root_children)} runtime_desc={runtime_desc}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
for child in root_children:
|
for child in root_children:
|
||||||
child.reparentTo(self.world.render)
|
child.reparentTo(self.world.render)
|
||||||
if child.hasTag("is_model_root") or child.hasTag("asset_guid"):
|
if child.hasTag("is_model_root") or child.hasTag("asset_guid"):
|
||||||
@ -981,6 +1113,28 @@ class ProjectManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
render_desc = self.world.render.find_all_matches("**").get_num_paths()
|
||||||
|
print(
|
||||||
|
f"[SceneOpen] render_desc={render_desc} built_models={len(built_model_nodes)} "
|
||||||
|
f"spot={len(built_spot_lights)} point={len(built_point_lights)}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
controller = getattr(ssbo_editor, "controller", None) if ssbo_loaded else None
|
||||||
|
if controller and hasattr(controller, "get_runtime_structure_stats"):
|
||||||
|
print(f"[SceneOpen] runtime_structure={controller.get_runtime_structure_stats()}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if ssbo_loaded and ssbo_editor and hasattr(ssbo_editor, "get_source_tree_stats"):
|
||||||
|
print(f"[SceneOpen] source_tree={ssbo_editor.get_source_tree_stats()}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._log_render_runtime_stats("after_scene_rebuild")
|
||||||
scene_components = dict(scene_description.get("scene_components", {}) or {})
|
scene_components = dict(scene_description.get("scene_components", {}) or {})
|
||||||
camera_state = dict(scene_components.get("camera", {}) or scene_description.get("camera", {}) or {})
|
camera_state = dict(scene_components.get("camera", {}) or scene_description.get("camera", {}) or {})
|
||||||
camera = getattr(self.world, "camera", None) or getattr(self.world, "cam", None)
|
camera = getattr(self.world, "camera", None) or getattr(self.world, "cam", None)
|
||||||
@ -1008,6 +1162,7 @@ class ProjectManager:
|
|||||||
load_lui_fn(temp_stub)
|
load_lui_fn(temp_stub)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
self._log_render_runtime_stats("after_lui_restore")
|
||||||
return bool(ssbo_loaded or built_model_nodes or built_spot_lights or built_point_lights or scene_components)
|
return bool(ssbo_loaded or built_model_nodes or built_spot_lights or built_point_lights or scene_components)
|
||||||
|
|
||||||
def _iter_top_level_scene_asset_nodes(self, scene_description):
|
def _iter_top_level_scene_asset_nodes(self, scene_description):
|
||||||
@ -1038,7 +1193,7 @@ class ProjectManager:
|
|||||||
top_level_nodes.append(node)
|
top_level_nodes.append(node)
|
||||||
return top_level_nodes, node_lookup
|
return top_level_nodes, node_lookup
|
||||||
|
|
||||||
def _apply_scene_description_state_to_node(self, target_np, node, project_path):
|
def _apply_scene_description_state_to_node(self, target_np, node, project_path, apply_material_state=True):
|
||||||
if not target_np or target_np.isEmpty() or not isinstance(node, dict):
|
if not target_np or target_np.isEmpty() or not isinstance(node, dict):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1049,11 +1204,24 @@ class ProjectManager:
|
|||||||
position = list(transform.get("position", [0, 0, 0]) or [0, 0, 0])
|
position = list(transform.get("position", [0, 0, 0]) or [0, 0, 0])
|
||||||
rotation = list(transform.get("rotation", [0, 0, 0]) or [0, 0, 0])
|
rotation = list(transform.get("rotation", [0, 0, 0]) or [0, 0, 0])
|
||||||
scale = list(transform.get("scale", [1, 1, 1]) or [1, 1, 1])
|
scale = list(transform.get("scale", [1, 1, 1]) or [1, 1, 1])
|
||||||
if len(position) >= 3:
|
try:
|
||||||
|
current_pos = target_np.getPos()
|
||||||
|
except Exception:
|
||||||
|
current_pos = None
|
||||||
|
try:
|
||||||
|
current_hpr = target_np.getHpr()
|
||||||
|
except Exception:
|
||||||
|
current_hpr = None
|
||||||
|
try:
|
||||||
|
current_scale = target_np.getScale()
|
||||||
|
except Exception:
|
||||||
|
current_scale = None
|
||||||
|
|
||||||
|
if len(position) >= 3 and self._vector_differs(current_pos, position):
|
||||||
target_np.setPos(float(position[0]), float(position[1]), float(position[2]))
|
target_np.setPos(float(position[0]), float(position[1]), float(position[2]))
|
||||||
if len(rotation) >= 3:
|
if len(rotation) >= 3 and self._vector_differs(current_hpr, rotation):
|
||||||
target_np.setHpr(float(rotation[0]), float(rotation[1]), float(rotation[2]))
|
target_np.setHpr(float(rotation[0]), float(rotation[1]), float(rotation[2]))
|
||||||
if len(scale) >= 3:
|
if len(scale) >= 3 and self._vector_differs(current_scale, scale):
|
||||||
target_np.setScale(float(scale[0]), float(scale[1]), float(scale[2]))
|
target_np.setScale(float(scale[0]), float(scale[1]), float(scale[2]))
|
||||||
|
|
||||||
visibility = dict(node.get("visibility", {}) or {})
|
visibility = dict(node.get("visibility", {}) or {})
|
||||||
@ -1120,7 +1288,8 @@ class ProjectManager:
|
|||||||
target_np.setTag("has_scripts", "true")
|
target_np.setTag("has_scripts", "true")
|
||||||
target_np.setTag("scripts_info", json.dumps(scripts, ensure_ascii=False))
|
target_np.setTag("scripts_info", json.dumps(scripts, ensure_ascii=False))
|
||||||
|
|
||||||
self._apply_saved_material_tags_to_node(target_np)
|
if apply_material_state:
|
||||||
|
self._apply_saved_material_tags_to_node(target_np)
|
||||||
|
|
||||||
def _apply_saved_material_tags_to_node(self, target_np):
|
def _apply_saved_material_tags_to_node(self, target_np):
|
||||||
"""Rebuild runtime material state from serialized material_* tags."""
|
"""Rebuild runtime material state from serialized material_* tags."""
|
||||||
@ -1221,12 +1390,17 @@ class ProjectManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _apply_scene_description_state_to_subtree(self, target_np, node, project_path, node_lookup):
|
def _apply_scene_description_state_to_subtree(self, target_np, node, project_path, node_lookup, apply_material_state=True):
|
||||||
"""Apply saved state to one imported node and its serialized descendants by child order."""
|
"""Apply saved state to one imported node and its serialized descendants by child order."""
|
||||||
if not target_np or target_np.isEmpty() or not isinstance(node, dict):
|
if not target_np or target_np.isEmpty() or not isinstance(node, dict):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._apply_scene_description_state_to_node(target_np, node, project_path)
|
self._apply_scene_description_state_to_node(
|
||||||
|
target_np,
|
||||||
|
node,
|
||||||
|
project_path,
|
||||||
|
apply_material_state=apply_material_state,
|
||||||
|
)
|
||||||
|
|
||||||
node_id = str(node.get("node_id", "") or "").strip()
|
node_id = str(node.get("node_id", "") or "").strip()
|
||||||
if not node_id:
|
if not node_id:
|
||||||
@ -1257,7 +1431,13 @@ class ProjectManager:
|
|||||||
child_index = _node_index(child_entry)
|
child_index = _node_index(child_entry)
|
||||||
if child_index < 0 or child_index >= len(runtime_children):
|
if child_index < 0 or child_index >= len(runtime_children):
|
||||||
continue
|
continue
|
||||||
self._apply_scene_description_state_to_subtree(runtime_children[child_index], child_entry, project_path, node_lookup)
|
self._apply_scene_description_state_to_subtree(
|
||||||
|
runtime_children[child_index],
|
||||||
|
child_entry,
|
||||||
|
project_path,
|
||||||
|
node_lookup,
|
||||||
|
apply_material_state=apply_material_state,
|
||||||
|
)
|
||||||
|
|
||||||
def _load_scene_description_via_ssbo(self, scene_description, project_path, asset_database):
|
def _load_scene_description_via_ssbo(self, scene_description, project_path, asset_database):
|
||||||
if not getattr(self.world, "use_ssbo_mouse_picking", False):
|
if not getattr(self.world, "use_ssbo_mouse_picking", False):
|
||||||
@ -1273,7 +1453,7 @@ class ProjectManager:
|
|||||||
if not top_level_asset_nodes:
|
if not top_level_asset_nodes:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
loaded_any = False
|
candidate_nodes = []
|
||||||
for node in top_level_asset_nodes:
|
for node in top_level_asset_nodes:
|
||||||
components = dict(node.get("components", {}) or {})
|
components = dict(node.get("components", {}) or {})
|
||||||
model_component = dict(components.get("model", {}) or {})
|
model_component = dict(components.get("model", {}) or {})
|
||||||
@ -1293,6 +1473,13 @@ class ProjectManager:
|
|||||||
if not os.path.exists(asset_abs):
|
if not os.path.exists(asset_abs):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
candidate_nodes.append((node, components, model_component, asset_abs))
|
||||||
|
|
||||||
|
loaded_any = False
|
||||||
|
total_candidates = len(candidate_nodes)
|
||||||
|
for index, (node, components, model_component, asset_abs) in enumerate(candidate_nodes):
|
||||||
|
has_more_assets = index < (total_candidates - 1)
|
||||||
|
|
||||||
has_saved_animation = False
|
has_saved_animation = False
|
||||||
try:
|
try:
|
||||||
metadata_component = dict(components.get("metadata", {}) or {})
|
metadata_component = dict(components.get("metadata", {}) or {})
|
||||||
@ -1326,6 +1513,7 @@ class ProjectManager:
|
|||||||
keep_source_model=False,
|
keep_source_model=False,
|
||||||
append=loaded_any,
|
append=loaded_any,
|
||||||
scene_package_import=False,
|
scene_package_import=False,
|
||||||
|
rebuild_runtime=False,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ SSBO 恢复场景模型失败 {asset_abs}: {e}")
|
print(f"⚠️ SSBO 恢复场景模型失败 {asset_abs}: {e}")
|
||||||
@ -1335,11 +1523,16 @@ class ProjectManager:
|
|||||||
if target_root is None:
|
if target_root is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._apply_scene_description_state_to_subtree(target_root, node, project_path, node_lookup)
|
self._apply_scene_description_state_to_subtree(
|
||||||
# Keep runtime aligned with source after restoring per-node transforms.
|
target_root,
|
||||||
# Otherwise the next append=True import snapshots stale runtime poses
|
node,
|
||||||
# back into source_model_root and overwrites the restored transform.
|
project_path,
|
||||||
if callable(refresh_runtime_fn):
|
node_lookup,
|
||||||
|
apply_material_state=False,
|
||||||
|
)
|
||||||
|
# Only rebuild between imports, or once after the whole batch below.
|
||||||
|
# Otherwise reopening a project pays one extra full SSBO rebuild per model.
|
||||||
|
if has_more_assets and callable(refresh_runtime_fn):
|
||||||
try:
|
try:
|
||||||
refresh_runtime_fn(preserve_selection=False)
|
refresh_runtime_fn(preserve_selection=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -1349,13 +1542,16 @@ class ProjectManager:
|
|||||||
if not loaded_any:
|
if not loaded_any:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
clear_runtime_fn = getattr(ssbo_editor, "_clear_runtime_state", None)
|
if callable(refresh_runtime_fn):
|
||||||
rebuild_fn = getattr(ssbo_editor, "_rebuild_runtime_from_source_root", None)
|
|
||||||
if callable(rebuild_fn):
|
|
||||||
try:
|
try:
|
||||||
if callable(clear_runtime_fn):
|
refresh_runtime_fn(preserve_selection=False)
|
||||||
clear_runtime_fn(preserve_source_models=True)
|
except Exception:
|
||||||
rebuild_fn(highlight_root_name=None)
|
pass
|
||||||
|
|
||||||
|
force_static_idle_fn = getattr(ssbo_editor, "force_static_chunk_idle_state", None)
|
||||||
|
if callable(force_static_idle_fn):
|
||||||
|
try:
|
||||||
|
force_static_idle_fn()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -1660,6 +1856,24 @@ class ProjectManager:
|
|||||||
if skip_asset_nodes:
|
if skip_asset_nodes:
|
||||||
if self._node_belongs_to_asset_hierarchy(node, node_lookup):
|
if self._node_belongs_to_asset_hierarchy(node, node_lookup):
|
||||||
continue
|
continue
|
||||||
|
imported_node_key = str(
|
||||||
|
node.get("imported_node_key", "")
|
||||||
|
or ((node.get("components", {}) or {}).get("model", {}) or {}).get("imported_node_key", "")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
if imported_node_key:
|
||||||
|
continue
|
||||||
|
if not self._is_scene_description_node_interactive(node):
|
||||||
|
components = dict(node.get("components", {}) or {})
|
||||||
|
tags = dict(node.get("tags", {}) or {})
|
||||||
|
has_light = bool(components.get("light"))
|
||||||
|
has_gui_tag = (
|
||||||
|
str(tags.get("is_gui_element", "")).lower() == "true"
|
||||||
|
or str(tags.get("gui_type", "")).strip() != ""
|
||||||
|
)
|
||||||
|
has_scene_element_tag = str(tags.get("is_scene_element", "")).lower() == "true"
|
||||||
|
if not has_light and not has_gui_tag and not has_scene_element_tag:
|
||||||
|
continue
|
||||||
is_interactive = self._is_scene_description_node_interactive(node)
|
is_interactive = self._is_scene_description_node_interactive(node)
|
||||||
if include_mode == "interactive" and not is_interactive:
|
if include_mode == "interactive" and not is_interactive:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -242,12 +242,8 @@ def _resolve_imported_node_key(node, fallback_key: str) -> str:
|
|||||||
def _should_serialize_child_nodes(node, asset_guid: str, scripts: list[dict], runtime_interactive: bool, light_component: dict, material_component: dict) -> bool:
|
def _should_serialize_child_nodes(node, asset_guid: str, scripts: list[dict], runtime_interactive: bool, light_component: dict, material_component: dict) -> bool:
|
||||||
if not node:
|
if not node:
|
||||||
return False
|
return False
|
||||||
# Imported model roots and their descendants must keep hierarchy state so
|
|
||||||
# per-child transforms/material overrides survive save/load.
|
|
||||||
if asset_guid:
|
if asset_guid:
|
||||||
return True
|
return True
|
||||||
if not asset_guid:
|
|
||||||
return True
|
|
||||||
if scripts:
|
if scripts:
|
||||||
return True
|
return True
|
||||||
if runtime_interactive:
|
if runtime_interactive:
|
||||||
@ -256,6 +252,12 @@ def _should_serialize_child_nodes(node, asset_guid: str, scripts: list[dict], ru
|
|||||||
return True
|
return True
|
||||||
if material_component:
|
if material_component:
|
||||||
return True
|
return True
|
||||||
|
if node.hasTag("scene_transform_dirty") and node.getTag("scene_transform_dirty").lower() == "true":
|
||||||
|
return True
|
||||||
|
if node.hasTag("scene_material_dirty") and node.getTag("scene_material_dirty").lower() == "true":
|
||||||
|
return True
|
||||||
|
if node.hasTag("user_visible") and node.getTag("user_visible").lower() != "true":
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -290,11 +292,61 @@ def _build_scene_description_payload(
|
|||||||
referenced_asset_guids = set()
|
referenced_asset_guids = set()
|
||||||
referenced_script_guids = set()
|
referenced_script_guids = set()
|
||||||
|
|
||||||
|
subtree_serializable_cache = {}
|
||||||
|
|
||||||
|
def _node_is_serializable(node) -> bool:
|
||||||
|
if not node or node.isEmpty():
|
||||||
|
return False
|
||||||
|
|
||||||
|
cache_key = id(node)
|
||||||
|
if cache_key in subtree_serializable_cache:
|
||||||
|
return subtree_serializable_cache[cache_key]
|
||||||
|
|
||||||
|
runtime_interactive = (
|
||||||
|
node.hasTag("runtime_interactive")
|
||||||
|
and node.getTag("runtime_interactive").lower() == "true"
|
||||||
|
)
|
||||||
|
scripts = []
|
||||||
|
if node.hasTag("scripts_info"):
|
||||||
|
try:
|
||||||
|
scripts = json.loads(node.getTag("scripts_info"))
|
||||||
|
except Exception:
|
||||||
|
scripts = []
|
||||||
|
|
||||||
|
asset_guid = ""
|
||||||
|
if node.hasTag("is_model_root"):
|
||||||
|
asset_guid = "root"
|
||||||
|
|
||||||
|
light_component = _collect_light_component(node)
|
||||||
|
material_component = _collect_material_override_component(node)
|
||||||
|
|
||||||
|
serializable_here = _should_serialize_child_nodes(
|
||||||
|
node,
|
||||||
|
asset_guid,
|
||||||
|
scripts,
|
||||||
|
runtime_interactive,
|
||||||
|
light_component,
|
||||||
|
material_component,
|
||||||
|
)
|
||||||
|
if serializable_here:
|
||||||
|
subtree_serializable_cache[cache_key] = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
children = list(node.getChildren())
|
||||||
|
except Exception:
|
||||||
|
children = []
|
||||||
|
result = any(_node_is_serializable(child) for child in children if child and not child.isEmpty())
|
||||||
|
subtree_serializable_cache[cache_key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
def walk_nodes(children, parent_id=""):
|
def walk_nodes(children, parent_id=""):
|
||||||
for index, child in enumerate(list(children or [])):
|
for index, child in enumerate(list(children or [])):
|
||||||
if not child or child.isEmpty():
|
if not child or child.isEmpty():
|
||||||
continue
|
continue
|
||||||
node_id = _node_path_id(parent_id, index)
|
node_id = _node_path_id(parent_id, index)
|
||||||
|
if not _node_is_serializable(child):
|
||||||
|
continue
|
||||||
|
|
||||||
node_name = child.getName()
|
node_name = child.getName()
|
||||||
imported_node_key = _resolve_imported_node_key(child, node_id)
|
imported_node_key = _resolve_imported_node_key(child, node_id)
|
||||||
@ -387,6 +439,9 @@ def _build_scene_description_payload(
|
|||||||
runtime_interactive,
|
runtime_interactive,
|
||||||
light_component,
|
light_component,
|
||||||
material_component,
|
material_component,
|
||||||
|
) or any(
|
||||||
|
_node_is_serializable(grandchild)
|
||||||
|
for grandchild in list(child.getChildren()) if grandchild and not grandchild.isEmpty()
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
child_nodes = list(child.getChildren())
|
child_nodes = list(child.getChildren())
|
||||||
|
|||||||
@ -242,6 +242,81 @@ class ObjectController:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_runtime_structure_stats(self):
|
||||||
|
"""Summarize hybrid runtime structure to diagnose idle-state regressions."""
|
||||||
|
stats = {
|
||||||
|
"chunks_total": 0,
|
||||||
|
"chunks_dynamic_enabled": 0,
|
||||||
|
"chunk_static_nodes": 0,
|
||||||
|
"chunk_dynamic_nodes": 0,
|
||||||
|
"chunk_static_visible": 0,
|
||||||
|
"chunk_static_stashed": 0,
|
||||||
|
"chunk_dynamic_visible": 0,
|
||||||
|
"chunk_dynamic_stashed": 0,
|
||||||
|
"dynamic_object_nodes": 0,
|
||||||
|
"pick_nodes": 0,
|
||||||
|
"model_descendants": 0,
|
||||||
|
"pick_descendants": 0,
|
||||||
|
"model_geom_nodes": 0,
|
||||||
|
"pick_geom_nodes": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks = getattr(self, "chunks", {}) or {}
|
||||||
|
stats["chunks_total"] = len(chunks)
|
||||||
|
for chunk in chunks.values():
|
||||||
|
if not isinstance(chunk, dict):
|
||||||
|
continue
|
||||||
|
if chunk.get("dynamic_enabled"):
|
||||||
|
stats["chunks_dynamic_enabled"] += 1
|
||||||
|
dynamic_np = chunk.get("dynamic_np")
|
||||||
|
static_np = chunk.get("static_np")
|
||||||
|
try:
|
||||||
|
if dynamic_np and not dynamic_np.is_empty():
|
||||||
|
stats["chunk_dynamic_nodes"] += 1
|
||||||
|
try:
|
||||||
|
if dynamic_np.is_stashed():
|
||||||
|
stats["chunk_dynamic_stashed"] += 1
|
||||||
|
elif not dynamic_np.is_hidden():
|
||||||
|
stats["chunk_dynamic_visible"] += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
stats["dynamic_object_nodes"] += len(list(dynamic_np.get_children()))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if static_np and not static_np.is_empty():
|
||||||
|
stats["chunk_static_nodes"] += 1
|
||||||
|
try:
|
||||||
|
if static_np.is_stashed():
|
||||||
|
stats["chunk_static_stashed"] += 1
|
||||||
|
elif not static_np.is_hidden():
|
||||||
|
stats["chunk_static_visible"] += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
model = getattr(self, "model", None)
|
||||||
|
if model:
|
||||||
|
try:
|
||||||
|
if not model.is_empty():
|
||||||
|
stats["model_descendants"] = model.find_all_matches("**").get_num_paths()
|
||||||
|
stats["model_geom_nodes"] = model.find_all_matches("**/+GeomNode").get_num_paths()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
pick_model = getattr(self, "pick_model", None)
|
||||||
|
if pick_model:
|
||||||
|
try:
|
||||||
|
if not pick_model.is_empty():
|
||||||
|
stats["pick_descendants"] = pick_model.find_all_matches("**").get_num_paths()
|
||||||
|
stats["pick_geom_nodes"] = pick_model.find_all_matches("**/+GeomNode").get_num_paths()
|
||||||
|
stats["pick_nodes"] = len(list(pick_model.get_children()))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
def get_preferred_selection_ids(self, key):
|
def get_preferred_selection_ids(self, key):
|
||||||
"""Return the IDs that should be selected when a tree node is clicked.
|
"""Return the IDs that should be selected when a tree node is clicked.
|
||||||
|
|
||||||
|
|||||||
@ -561,101 +561,117 @@ class SSBOEditor:
|
|||||||
if not self.controller or not self.model or not self.source_model_root:
|
if not self.controller or not self.model or not self.source_model_root:
|
||||||
return
|
return
|
||||||
|
|
||||||
proxy = getattr(self, "_group_proxy", None)
|
self._snapshot_active_selection_transform_to_source()
|
||||||
if self._node_is_valid(proxy):
|
|
||||||
|
# Persist whole-model transforms that live on the aggregated runtime root.
|
||||||
|
# Child/object sync above only captures transforms relative to self.model,
|
||||||
|
# so moving the whole imported model root would otherwise be lost on save.
|
||||||
|
model_root_mat = None
|
||||||
|
try:
|
||||||
|
model_root_mat = LMatrix4f(self.model.get_mat(self.base.render))
|
||||||
|
except Exception:
|
||||||
try:
|
try:
|
||||||
selection_key = str(proxy.getTag("ssbo_selection_key") or "").strip()
|
model_root_mat = LMatrix4f(self.model.getMat(self.base.render))
|
||||||
except Exception:
|
except Exception:
|
||||||
selection_key = ""
|
model_root_mat = None
|
||||||
if selection_key and selection_key != getattr(self.controller, "tree_root_key", None):
|
|
||||||
source_group_node = self._resolve_source_node_by_tree_key(selection_key)
|
|
||||||
if self._node_is_valid(source_group_node):
|
|
||||||
try:
|
|
||||||
group_mat = LMatrix4f(proxy.get_mat(self.base.render))
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
group_mat = LMatrix4f(proxy.getMat(self.base.render))
|
|
||||||
except Exception:
|
|
||||||
group_mat = None
|
|
||||||
if group_mat is not None:
|
|
||||||
try:
|
|
||||||
source_group_node.set_mat(group_mat)
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
source_group_node.setMat(group_mat)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
grouped_entries = {}
|
if model_root_mat is None:
|
||||||
for gid, obj_np in self.controller.id_to_object_np.items():
|
return
|
||||||
if not self._node_is_valid(obj_np):
|
|
||||||
|
source_children = self._get_source_root_children()
|
||||||
|
if not source_children:
|
||||||
|
return
|
||||||
|
|
||||||
|
for source_child in source_children:
|
||||||
|
if not self._node_is_valid(source_child):
|
||||||
continue
|
continue
|
||||||
|
child_name = self._get_node_name(source_child, None)
|
||||||
owner_key = self.controller.id_to_name.get(gid)
|
if not child_name:
|
||||||
if not owner_key or owner_key == getattr(self.controller, "tree_root_key", None):
|
|
||||||
continue
|
continue
|
||||||
|
base_mat = self._source_child_base_mats.get(child_name)
|
||||||
source_node = self._resolve_source_node_by_tree_key(owner_key)
|
if base_mat is None:
|
||||||
if not self._node_is_valid(source_node):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
current_net_mat = LMatrix4f(obj_np.get_mat(self.model))
|
|
||||||
except Exception:
|
|
||||||
try:
|
try:
|
||||||
current_net_mat = LMatrix4f(obj_np.getMat(self.model))
|
base_mat = LMatrix4f(source_child.get_mat())
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
|
||||||
source_parent = source_node.get_parent()
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
source_parent = source_node.getParent()
|
|
||||||
except Exception:
|
|
||||||
source_parent = None
|
|
||||||
|
|
||||||
parent_net_mat = LMatrix4f.ident_mat()
|
|
||||||
if self._node_is_valid(source_parent) and source_parent != self.source_model_root:
|
|
||||||
try:
|
|
||||||
parent_net_mat = LMatrix4f(source_parent.get_mat(self.source_model_root))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
parent_net_mat = LMatrix4f(source_parent.getMat(self.source_model_root))
|
base_mat = LMatrix4f(source_child.getMat())
|
||||||
except Exception:
|
except Exception:
|
||||||
parent_net_mat = LMatrix4f.ident_mat()
|
continue
|
||||||
|
composed_mat = LMatrix4f(base_mat)
|
||||||
inv_parent_mat = LMatrix4f(parent_net_mat)
|
composed_mat *= model_root_mat
|
||||||
try:
|
try:
|
||||||
inv_parent_mat.invertInPlace()
|
source_child.set_mat(composed_mat)
|
||||||
|
source_child.setTag("scene_transform_dirty", "true")
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
inv_parent_mat.invert_in_place()
|
source_child.setMat(composed_mat)
|
||||||
|
source_child.setTag("scene_transform_dirty", "true")
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
local_mat = current_net_mat * inv_parent_mat
|
def _snapshot_active_selection_transform_to_source(self):
|
||||||
|
"""Persist only the actively edited selection back into the source tree."""
|
||||||
|
if not self.controller or not self.model or not self.source_model_root:
|
||||||
|
return False
|
||||||
|
|
||||||
|
selected_key = self.selected_name
|
||||||
|
root_key = getattr(self.controller, "tree_root_key", None)
|
||||||
|
if not selected_key or selected_key == root_key:
|
||||||
|
return False
|
||||||
|
|
||||||
|
scene_node = self.get_selection_scene_node()
|
||||||
|
source_node = self.get_selection_source_node()
|
||||||
|
if not self._node_is_valid(scene_node) or not self._node_is_valid(source_node):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_net_mat = LMatrix4f(scene_node.get_mat(self.model))
|
||||||
|
except Exception:
|
||||||
try:
|
try:
|
||||||
source_node.set_mat(local_mat)
|
current_net_mat = LMatrix4f(scene_node.getMat(self.model))
|
||||||
|
except Exception:
|
||||||
|
current_net_mat = None
|
||||||
|
if current_net_mat is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_parent = source_node.get_parent()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
source_parent = source_node.getParent()
|
||||||
|
except Exception:
|
||||||
|
source_parent = None
|
||||||
|
|
||||||
|
parent_net_mat = LMatrix4f.ident_mat()
|
||||||
|
if self._node_is_valid(source_parent) and source_parent != self.source_model_root:
|
||||||
|
try:
|
||||||
|
parent_net_mat = LMatrix4f(source_parent.get_mat(self.source_model_root))
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
source_node.setMat(local_mat)
|
parent_net_mat = LMatrix4f(source_parent.getMat(self.source_model_root))
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
parent_net_mat = LMatrix4f.ident_mat()
|
||||||
|
|
||||||
|
inv_parent_mat = LMatrix4f(parent_net_mat)
|
||||||
|
try:
|
||||||
|
inv_parent_mat.invertInPlace()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
inv_parent_mat.invert_in_place()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
local_mat = current_net_mat * inv_parent_mat
|
||||||
|
try:
|
||||||
|
source_node.set_mat(local_mat)
|
||||||
|
source_node.setTag("scene_transform_dirty", "true")
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
source_node.setMat(local_mat)
|
||||||
|
source_node.setTag("scene_transform_dirty", "true")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _resolve_source_node_by_tree_key(self, tree_key):
|
def _resolve_source_node_by_tree_key(self, tree_key):
|
||||||
"""Resolve controller tree key (e.g. 0/1/2) to source_model_root node."""
|
"""Resolve controller tree key (e.g. 0/1/2) to source_model_root node."""
|
||||||
@ -1115,6 +1131,7 @@ class SSBOEditor:
|
|||||||
target_snapshot = _clone_snapshot_for_target(source_snapshot, source_node)
|
target_snapshot = _clone_snapshot_for_target(source_snapshot, source_node)
|
||||||
if target_snapshot is not None:
|
if target_snapshot is not None:
|
||||||
apply_snapshot_fn(source_node, target_snapshot)
|
apply_snapshot_fn(source_node, target_snapshot)
|
||||||
|
source_node.setTag("scene_material_dirty", "true")
|
||||||
synced += 1
|
synced += 1
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -1145,6 +1162,7 @@ class SSBOEditor:
|
|||||||
try:
|
try:
|
||||||
if callable(set_src_state):
|
if callable(set_src_state):
|
||||||
set_src_state(src_geom_index, runtime_geom_state)
|
set_src_state(src_geom_index, runtime_geom_state)
|
||||||
|
source_node.setTag("scene_material_dirty", "true")
|
||||||
synced += 1
|
synced += 1
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
@ -1223,6 +1241,7 @@ class SSBOEditor:
|
|||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
self._cleanup_group_proxy()
|
self._cleanup_group_proxy()
|
||||||
self._reset_pick_sync_cache()
|
self._reset_pick_sync_cache()
|
||||||
|
self._teardown_gpu_picking()
|
||||||
|
|
||||||
controller = self.controller
|
controller = self.controller
|
||||||
pick_model = getattr(controller, "pick_model", None) if controller else None
|
pick_model = getattr(controller, "pick_model", None) if controller else None
|
||||||
@ -1440,6 +1459,7 @@ class SSBOEditor:
|
|||||||
keep_source_model=False,
|
keep_source_model=False,
|
||||||
append=False,
|
append=False,
|
||||||
scene_package_import=False,
|
scene_package_import=False,
|
||||||
|
rebuild_runtime=True,
|
||||||
):
|
):
|
||||||
"""Load and process one model into the aggregated SSBO scene."""
|
"""Load and process one model into the aggregated SSBO scene."""
|
||||||
print(f"[SSBOEditor] Loading model: {model_path}")
|
print(f"[SSBOEditor] Loading model: {model_path}")
|
||||||
@ -1511,7 +1531,8 @@ class SSBOEditor:
|
|||||||
self.source_model = source_root
|
self.source_model = source_root
|
||||||
self._restore_saved_material_bindings_from_tags(source_root)
|
self._restore_saved_material_bindings_from_tags(source_root)
|
||||||
self._capture_source_child_base_mats()
|
self._capture_source_child_base_mats()
|
||||||
self._rebuild_runtime_from_source_root(highlight_root_name=None)
|
if rebuild_runtime:
|
||||||
|
self._rebuild_runtime_from_source_root(highlight_root_name=None)
|
||||||
if len(imported_roots) == 1:
|
if len(imported_roots) == 1:
|
||||||
return imported_roots[0]
|
return imported_roots[0]
|
||||||
return source_root
|
return source_root
|
||||||
@ -1529,7 +1550,8 @@ class SSBOEditor:
|
|||||||
self.source_model = source_root
|
self.source_model = source_root
|
||||||
|
|
||||||
self._capture_source_child_base_mats()
|
self._capture_source_child_base_mats()
|
||||||
self._rebuild_runtime_from_source_root(highlight_root_name=unique_root_name)
|
if rebuild_runtime:
|
||||||
|
self._rebuild_runtime_from_source_root(highlight_root_name=unique_root_name)
|
||||||
return imported_root
|
return imported_root
|
||||||
|
|
||||||
def _build_filename_candidates(self, path_text):
|
def _build_filename_candidates(self, path_text):
|
||||||
@ -2106,7 +2128,8 @@ class SSBOEditor:
|
|||||||
|
|
||||||
def setup_gpu_picking(self):
|
def setup_gpu_picking(self):
|
||||||
"""Setup GPU Picking (Basic implementation)"""
|
"""Setup GPU Picking (Basic implementation)"""
|
||||||
# ... (Buffer setup code remains same) ...
|
self._teardown_gpu_picking()
|
||||||
|
|
||||||
win_props = WindowProperties()
|
win_props = WindowProperties()
|
||||||
win_props.set_size(1, 1)
|
win_props.set_size(1, 1)
|
||||||
fb_props = FrameBufferProperties()
|
fb_props = FrameBufferProperties()
|
||||||
@ -2169,6 +2192,35 @@ class SSBOEditor:
|
|||||||
self.pick_buffer.set_clear_color(Vec4(0, 0, 0, 0))
|
self.pick_buffer.set_clear_color(Vec4(0, 0, 0, 0))
|
||||||
self.pick_buffer.set_clear_color_active(True)
|
self.pick_buffer.set_clear_color_active(True)
|
||||||
|
|
||||||
|
def _teardown_gpu_picking(self):
|
||||||
|
"""Release old GPU picking resources before rebuilding them."""
|
||||||
|
pick_cam_np = getattr(self, "pick_cam_np", None)
|
||||||
|
if pick_cam_np is not None:
|
||||||
|
try:
|
||||||
|
if not pick_cam_np.is_empty():
|
||||||
|
pick_cam_np.remove_node()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
if not pick_cam_np.isEmpty():
|
||||||
|
pick_cam_np.removeNode()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.pick_cam_np = None
|
||||||
|
self.pick_cam = None
|
||||||
|
self.pick_lens = None
|
||||||
|
|
||||||
|
pick_buffer = getattr(self, "pick_buffer", None)
|
||||||
|
if pick_buffer is not None:
|
||||||
|
try:
|
||||||
|
self.base.graphicsEngine.remove_window(pick_buffer)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.base.graphicsEngine.removeWindow(pick_buffer)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.pick_buffer = None
|
||||||
|
self.pick_texture = None
|
||||||
|
|
||||||
def _sync_pick_model_transform(self):
|
def _sync_pick_model_transform(self):
|
||||||
"""Sync pick-scene clone to current source model world transform."""
|
"""Sync pick-scene clone to current source model world transform."""
|
||||||
if not self.controller or not self.model:
|
if not self.controller or not self.model:
|
||||||
@ -2744,6 +2796,7 @@ class SSBOEditor:
|
|||||||
try:
|
try:
|
||||||
self._clear_runtime_state(preserve_source_models=True)
|
self._clear_runtime_state(preserve_source_models=True)
|
||||||
self._rebuild_runtime_from_source_root(highlight_root_name=None)
|
self._rebuild_runtime_from_source_root(highlight_root_name=None)
|
||||||
|
self.force_static_chunk_idle_state()
|
||||||
if preserve_selection and selected_key and self.controller:
|
if preserve_selection and selected_key and self.controller:
|
||||||
try:
|
try:
|
||||||
if (
|
if (
|
||||||
@ -2759,6 +2812,74 @@ class SSBOEditor:
|
|||||||
print(f"[SSBOEditor] 从 source 刷新 runtime 失败: {e}")
|
print(f"[SSBOEditor] 从 source 刷新 runtime 失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_source_tree_stats(self):
|
||||||
|
root = getattr(self, "source_model_root", None)
|
||||||
|
stats = {
|
||||||
|
"valid": False,
|
||||||
|
"has_parent": False,
|
||||||
|
"hidden": False,
|
||||||
|
"stashed": False,
|
||||||
|
"descendants": 0,
|
||||||
|
"geom_nodes": 0,
|
||||||
|
"top_children": 0,
|
||||||
|
"parent_name": "",
|
||||||
|
}
|
||||||
|
if not self._node_is_valid(root):
|
||||||
|
return stats
|
||||||
|
|
||||||
|
stats["valid"] = True
|
||||||
|
try:
|
||||||
|
stats["has_parent"] = bool(root.has_parent())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
stats["hidden"] = bool(root.is_hidden())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
stats["stashed"] = bool(root.is_stashed())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
parent = root.get_parent()
|
||||||
|
if parent and not parent.is_empty():
|
||||||
|
stats["parent_name"] = parent.get_name() or ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
stats["descendants"] = root.find_all_matches("**").get_num_paths()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
stats["geom_nodes"] = len(list(root.find_all_matches("**/+GeomNode")))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
stats["top_children"] = len([child for child in root.get_children() if not child.is_empty()])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def force_static_chunk_idle_state(self):
|
||||||
|
"""Force hybrid chunk runtime back to fully static idle mode."""
|
||||||
|
controller = getattr(self, "controller", None)
|
||||||
|
chunks = getattr(controller, "chunks", None) if controller else None
|
||||||
|
if not controller or not isinstance(chunks, dict) or not chunks:
|
||||||
|
return False
|
||||||
|
|
||||||
|
rebuilt = 0
|
||||||
|
for chunk_id in sorted(chunks):
|
||||||
|
try:
|
||||||
|
controller._rebuild_static_chunk(chunk_id)
|
||||||
|
controller._set_chunk_dynamic(chunk_id, False)
|
||||||
|
rebuilt += 1
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rebuilt:
|
||||||
|
print(f"[SSBOEditor] Forced static idle chunks: {rebuilt}")
|
||||||
|
return rebuilt > 0
|
||||||
|
|
||||||
def _sync_editor_selection_reference(self, node):
|
def _sync_editor_selection_reference(self, node):
|
||||||
selection = getattr(self.base, "selection", None)
|
selection = getattr(self.base, "selection", None)
|
||||||
if not selection:
|
if not selection:
|
||||||
@ -2819,6 +2940,7 @@ class SSBOEditor:
|
|||||||
self.clear_selection(sync_world_selection=False)
|
self.clear_selection(sync_world_selection=False)
|
||||||
|
|
||||||
def clear_selection(self, sync_world_selection=True):
|
def clear_selection(self, sync_world_selection=True):
|
||||||
|
self._snapshot_active_selection_transform_to_source()
|
||||||
self._stop_pick_sync_task()
|
self._stop_pick_sync_task()
|
||||||
self._reset_pick_sync_cache()
|
self._reset_pick_sync_cache()
|
||||||
self._cleanup_group_proxy()
|
self._cleanup_group_proxy()
|
||||||
@ -2877,6 +2999,7 @@ class SSBOEditor:
|
|||||||
pass # No selection mask texture needed without custom shader
|
pass # No selection mask texture needed without custom shader
|
||||||
|
|
||||||
def select_node(self, key, sync_world_selection=True):
|
def select_node(self, key, sync_world_selection=True):
|
||||||
|
self._snapshot_active_selection_transform_to_source()
|
||||||
# Clean up previous group proxy before changing selection
|
# Clean up previous group proxy before changing selection
|
||||||
self._cleanup_group_proxy()
|
self._cleanup_group_proxy()
|
||||||
self._reset_pick_sync_cache()
|
self._reset_pick_sync_cache()
|
||||||
|
|||||||
@ -1540,6 +1540,10 @@ class AppActions:
|
|||||||
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
||||||
Legacy mode: load via SceneManager.
|
Legacy mode: load via SceneManager.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
self._scene_tree_epoch = int(getattr(self, "_scene_tree_epoch", 0) or 0) + 1
|
||||||
|
except Exception:
|
||||||
|
self._scene_tree_epoch = 1
|
||||||
animated_model = bool(file_path and self._model_file_has_animation(file_path))
|
animated_model = bool(file_path and self._model_file_has_animation(file_path))
|
||||||
if animated_model:
|
if animated_model:
|
||||||
prefer_scene_manager = True
|
prefer_scene_manager = True
|
||||||
@ -1584,6 +1588,77 @@ class AppActions:
|
|||||||
return self.scene_manager.importModel(file_path)
|
return self.scene_manager.importModel(file_path)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _log_render_runtime_stats(self, label):
|
||||||
|
"""Print renderer/task stats for import/open path comparison."""
|
||||||
|
try:
|
||||||
|
win = getattr(self, "win", None)
|
||||||
|
num_regions = win.getNumDisplayRegions() if win else 0
|
||||||
|
region_lines = []
|
||||||
|
if win:
|
||||||
|
for index in range(num_regions):
|
||||||
|
try:
|
||||||
|
dr = win.getDisplayRegion(index)
|
||||||
|
camera = dr.getCamera()
|
||||||
|
camera_name = "None"
|
||||||
|
if camera and not camera.isEmpty():
|
||||||
|
camera_name = camera.getName()
|
||||||
|
region_lines.append(
|
||||||
|
f"{index}:{dr.getSort()}:{int(bool(dr.isActive()))}:{camera_name}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
graphics_engine = getattr(self, "graphicsEngine", None)
|
||||||
|
num_windows = graphics_engine.getNumWindows() if graphics_engine else 0
|
||||||
|
|
||||||
|
render = getattr(self, "render", None)
|
||||||
|
camera_count = 0
|
||||||
|
special_camera_counts = {}
|
||||||
|
if render and not render.isEmpty():
|
||||||
|
try:
|
||||||
|
camera_count = render.find_all_matches("**/+Camera").get_num_paths()
|
||||||
|
except Exception:
|
||||||
|
camera_count = 0
|
||||||
|
for camera_name in (
|
||||||
|
"gizmo_overlay_cam",
|
||||||
|
"pick_camera",
|
||||||
|
"selection_outline_mask_camera",
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
special_camera_counts[camera_name] = render.find_all_matches(
|
||||||
|
f"**/{camera_name}"
|
||||||
|
).get_num_paths()
|
||||||
|
except Exception:
|
||||||
|
special_camera_counts[camera_name] = 0
|
||||||
|
|
||||||
|
task_mgr = getattr(self, "taskMgr", None) or getattr(self, "task_mgr", None)
|
||||||
|
task_names = []
|
||||||
|
if task_mgr:
|
||||||
|
try:
|
||||||
|
task_names = [task.name for task in list(task_mgr.getTasks())]
|
||||||
|
except Exception:
|
||||||
|
task_names = []
|
||||||
|
interesting_tasks = [
|
||||||
|
name for name in task_names
|
||||||
|
if (
|
||||||
|
"gizmo" in str(name).lower()
|
||||||
|
or "outline" in str(name).lower()
|
||||||
|
or "pick" in str(name).lower()
|
||||||
|
or "lui" in str(name).lower()
|
||||||
|
or "canvas" in str(name).lower()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[RenderStats:{label}] regions={num_regions} windows={num_windows} "
|
||||||
|
f"render_cameras={camera_count} special={special_camera_counts} "
|
||||||
|
f"interesting_tasks={interesting_tasks}"
|
||||||
|
)
|
||||||
|
if region_lines:
|
||||||
|
print(f"[RenderStats:{label}] region_detail={' | '.join(region_lines)}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _import_model_with_menu_logic(
|
def _import_model_with_menu_logic(
|
||||||
self,
|
self,
|
||||||
file_path,
|
file_path,
|
||||||
@ -1667,6 +1742,16 @@ class AppActions:
|
|||||||
|
|
||||||
if show_success_message:
|
if show_success_message:
|
||||||
self.add_success_message(f"模型导入成功: {file_name}")
|
self.add_success_message(f"模型导入成功: {file_name}")
|
||||||
|
try:
|
||||||
|
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||||
|
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||||
|
if controller and hasattr(controller, "get_runtime_structure_stats"):
|
||||||
|
print(f"[ManualImport] runtime_structure={controller.get_runtime_structure_stats()}")
|
||||||
|
if ssbo_editor and hasattr(ssbo_editor, "get_source_tree_stats"):
|
||||||
|
print(f"[ManualImport] source_tree={ssbo_editor.get_source_tree_stats()}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._log_render_runtime_stats("manual_import")
|
||||||
return model_node
|
return model_node
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.add_error_message(f"导入模型失败: {e}")
|
self.add_error_message(f"导入模型失败: {e}")
|
||||||
|
|||||||
@ -155,6 +155,12 @@ class EditorPanelsLeftMixin:
|
|||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
def _get_scene_tree_epoch(self):
|
||||||
|
try:
|
||||||
|
return int(getattr(self.app, "_scene_tree_epoch", 0) or 0)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
def _ssbo_key_matches_scene_filter(self, controller, key):
|
def _ssbo_key_matches_scene_filter(self, controller, key):
|
||||||
filter_text = self._get_scene_tree_filter()
|
filter_text = self._get_scene_tree_filter()
|
||||||
if not filter_text:
|
if not filter_text:
|
||||||
@ -446,10 +452,11 @@ class EditorPanelsLeftMixin:
|
|||||||
obj_count = len(controller.name_to_ids.get(key, []))
|
obj_count = len(controller.name_to_ids.get(key, []))
|
||||||
children = node_data["children"]
|
children = node_data["children"]
|
||||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||||
|
tree_epoch = self._get_scene_tree_epoch()
|
||||||
|
|
||||||
if not children:
|
if not children:
|
||||||
# Leaf node: selectable
|
# Leaf node: selectable
|
||||||
label = f"{display} ({obj_count})##{key}"
|
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||||||
if imgui.selectable(label, is_selected)[0]:
|
if imgui.selectable(label, is_selected)[0]:
|
||||||
ssbo_editor.select_node(key)
|
ssbo_editor.select_node(key)
|
||||||
if hasattr(self.app, "lui_manager"):
|
if hasattr(self.app, "lui_manager"):
|
||||||
@ -465,7 +472,7 @@ class EditorPanelsLeftMixin:
|
|||||||
flags = imgui.TreeNodeFlags_.open_on_arrow
|
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||||||
if is_selected:
|
if is_selected:
|
||||||
flags |= imgui.TreeNodeFlags_.selected
|
flags |= imgui.TreeNodeFlags_.selected
|
||||||
label = f"{display} ({obj_count})##{key}"
|
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||||||
opened = imgui.tree_node_ex(label, flags)
|
opened = imgui.tree_node_ex(label, flags)
|
||||||
if imgui.is_item_clicked(0):
|
if imgui.is_item_clicked(0):
|
||||||
ssbo_editor.select_node(key)
|
ssbo_editor.select_node(key)
|
||||||
@ -495,6 +502,7 @@ class EditorPanelsLeftMixin:
|
|||||||
obj_count = len(controller.name_to_ids.get(key, []))
|
obj_count = len(controller.name_to_ids.get(key, []))
|
||||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||||
children = list((virtual_node.get("children", {}) or {}).values())
|
children = list((virtual_node.get("children", {}) or {}).values())
|
||||||
|
tree_epoch = self._get_scene_tree_epoch()
|
||||||
|
|
||||||
if display.strip().lower() == "root" and children:
|
if display.strip().lower() == "root" and children:
|
||||||
for child in children:
|
for child in children:
|
||||||
@ -504,7 +512,7 @@ class EditorPanelsLeftMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not children:
|
if not children:
|
||||||
label = f"{display} ({obj_count})##{key}"
|
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||||||
if imgui.selectable(label, is_selected)[0]:
|
if imgui.selectable(label, is_selected)[0]:
|
||||||
ssbo_editor.select_node(key)
|
ssbo_editor.select_node(key)
|
||||||
if hasattr(self.app, "lui_manager"):
|
if hasattr(self.app, "lui_manager"):
|
||||||
@ -520,7 +528,7 @@ class EditorPanelsLeftMixin:
|
|||||||
flags = imgui.TreeNodeFlags_.open_on_arrow
|
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||||||
if is_selected:
|
if is_selected:
|
||||||
flags |= imgui.TreeNodeFlags_.selected
|
flags |= imgui.TreeNodeFlags_.selected
|
||||||
label = f"{display} ({obj_count})##{key}"
|
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||||||
opened = imgui.tree_node_ex(label, flags)
|
opened = imgui.tree_node_ex(label, flags)
|
||||||
if imgui.is_item_clicked(0):
|
if imgui.is_item_clicked(0):
|
||||||
ssbo_editor.select_node(key)
|
ssbo_editor.select_node(key)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user