保存、打开项目优化
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][场景树]
|
||||
Pos=0,20
|
||||
Size=339,1084
|
||||
Size=339,1008
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1694,20
|
||||
Size=346,1084
|
||||
Pos=1506,20
|
||||
Size=346,1008
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=341,705
|
||||
Size=1351,399
|
||||
Pos=341,629
|
||||
Size=1163,399
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
@ -59,7 +59,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=2040,1084
|
||||
Size=1852,1008
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -78,17 +78,17 @@ Size=93,65
|
||||
Collapsed=0
|
||||
|
||||
[Window][新建项目]
|
||||
Pos=1080,525
|
||||
Pos=824,402
|
||||
Size=400,300
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择路径]
|
||||
Pos=720,302
|
||||
Pos=626,264
|
||||
Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][打开项目]
|
||||
Pos=770,352
|
||||
Pos=676,314
|
||||
Size=500,400
|
||||
Collapsed=0
|
||||
|
||||
@ -98,8 +98,8 @@ Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=341,705
|
||||
Size=1351,399
|
||||
Pos=341,629
|
||||
Size=1163,399
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
@ -226,7 +226,7 @@ Size=460,260
|
||||
Collapsed=0
|
||||
|
||||
[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=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1871,1084 Split=Y
|
||||
|
||||
@ -47,6 +47,102 @@ class ProjectManager:
|
||||
def _get_repo_root(self):
|
||||
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):
|
||||
project_path = os.path.normpath(project_path or self.current_project_path or "")
|
||||
if not project_path:
|
||||
@ -689,6 +785,10 @@ class ProjectManager:
|
||||
"""
|
||||
# print(f"\n[DEBUG] ===== 开始打开项目: {project_path} =====")
|
||||
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:
|
||||
print("错误: 项目路径不能为空")
|
||||
return False
|
||||
@ -913,6 +1013,14 @@ class ProjectManager:
|
||||
scene_file_path = os.path.join(project_path, scene_entry["path"].replace("/", os.sep))
|
||||
save_json(scene_file_path, scene_description)
|
||||
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
|
||||
|
||||
def _load_scene_description_into_editor(self, scene_description, project_path, scene_entry=None):
|
||||
@ -921,6 +1029,14 @@ class ProjectManager:
|
||||
return False
|
||||
|
||||
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)
|
||||
|
||||
scene_root, keep_nodes = self._build_scene_root_from_description(
|
||||
@ -945,6 +1061,22 @@ class ProjectManager:
|
||||
except Exception:
|
||||
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:
|
||||
child.reparentTo(self.world.render)
|
||||
if child.hasTag("is_model_root") or child.hasTag("asset_guid"):
|
||||
@ -981,6 +1113,28 @@ class ProjectManager:
|
||||
except Exception:
|
||||
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 {})
|
||||
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)
|
||||
@ -1008,6 +1162,7 @@ class ProjectManager:
|
||||
load_lui_fn(temp_stub)
|
||||
except Exception:
|
||||
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)
|
||||
|
||||
def _iter_top_level_scene_asset_nodes(self, scene_description):
|
||||
@ -1038,7 +1193,7 @@ class ProjectManager:
|
||||
top_level_nodes.append(node)
|
||||
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):
|
||||
return
|
||||
|
||||
@ -1049,11 +1204,24 @@ class ProjectManager:
|
||||
position = list(transform.get("position", [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])
|
||||
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]))
|
||||
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]))
|
||||
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]))
|
||||
|
||||
visibility = dict(node.get("visibility", {}) or {})
|
||||
@ -1120,7 +1288,8 @@ class ProjectManager:
|
||||
target_np.setTag("has_scripts", "true")
|
||||
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):
|
||||
"""Rebuild runtime material state from serialized material_* tags."""
|
||||
@ -1221,12 +1390,17 @@ class ProjectManager:
|
||||
except Exception:
|
||||
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."""
|
||||
if not target_np or target_np.isEmpty() or not isinstance(node, dict):
|
||||
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()
|
||||
if not node_id:
|
||||
@ -1257,7 +1431,13 @@ class ProjectManager:
|
||||
child_index = _node_index(child_entry)
|
||||
if child_index < 0 or child_index >= len(runtime_children):
|
||||
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):
|
||||
if not getattr(self.world, "use_ssbo_mouse_picking", False):
|
||||
@ -1273,7 +1453,7 @@ class ProjectManager:
|
||||
if not top_level_asset_nodes:
|
||||
return False
|
||||
|
||||
loaded_any = False
|
||||
candidate_nodes = []
|
||||
for node in top_level_asset_nodes:
|
||||
components = dict(node.get("components", {}) or {})
|
||||
model_component = dict(components.get("model", {}) or {})
|
||||
@ -1293,6 +1473,13 @@ class ProjectManager:
|
||||
if not os.path.exists(asset_abs):
|
||||
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
|
||||
try:
|
||||
metadata_component = dict(components.get("metadata", {}) or {})
|
||||
@ -1326,6 +1513,7 @@ class ProjectManager:
|
||||
keep_source_model=False,
|
||||
append=loaded_any,
|
||||
scene_package_import=False,
|
||||
rebuild_runtime=False,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"⚠️ SSBO 恢复场景模型失败 {asset_abs}: {e}")
|
||||
@ -1335,11 +1523,16 @@ class ProjectManager:
|
||||
if target_root is None:
|
||||
continue
|
||||
|
||||
self._apply_scene_description_state_to_subtree(target_root, node, project_path, node_lookup)
|
||||
# Keep runtime aligned with source after restoring per-node transforms.
|
||||
# Otherwise the next append=True import snapshots stale runtime poses
|
||||
# back into source_model_root and overwrites the restored transform.
|
||||
if callable(refresh_runtime_fn):
|
||||
self._apply_scene_description_state_to_subtree(
|
||||
target_root,
|
||||
node,
|
||||
project_path,
|
||||
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:
|
||||
refresh_runtime_fn(preserve_selection=False)
|
||||
except Exception:
|
||||
@ -1349,13 +1542,16 @@ class ProjectManager:
|
||||
if not loaded_any:
|
||||
return False
|
||||
|
||||
clear_runtime_fn = getattr(ssbo_editor, "_clear_runtime_state", None)
|
||||
rebuild_fn = getattr(ssbo_editor, "_rebuild_runtime_from_source_root", None)
|
||||
if callable(rebuild_fn):
|
||||
if callable(refresh_runtime_fn):
|
||||
try:
|
||||
if callable(clear_runtime_fn):
|
||||
clear_runtime_fn(preserve_source_models=True)
|
||||
rebuild_fn(highlight_root_name=None)
|
||||
refresh_runtime_fn(preserve_selection=False)
|
||||
except Exception:
|
||||
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:
|
||||
pass
|
||||
|
||||
@ -1660,6 +1856,24 @@ class ProjectManager:
|
||||
if skip_asset_nodes:
|
||||
if self._node_belongs_to_asset_hierarchy(node, node_lookup):
|
||||
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)
|
||||
if include_mode == "interactive" and not is_interactive:
|
||||
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:
|
||||
if not node:
|
||||
return False
|
||||
# Imported model roots and their descendants must keep hierarchy state so
|
||||
# per-child transforms/material overrides survive save/load.
|
||||
if asset_guid:
|
||||
return True
|
||||
if not asset_guid:
|
||||
return True
|
||||
if scripts:
|
||||
return True
|
||||
if runtime_interactive:
|
||||
@ -256,6 +252,12 @@ def _should_serialize_child_nodes(node, asset_guid: str, scripts: list[dict], ru
|
||||
return True
|
||||
if material_component:
|
||||
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
|
||||
|
||||
|
||||
@ -290,11 +292,61 @@ def _build_scene_description_payload(
|
||||
referenced_asset_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=""):
|
||||
for index, child in enumerate(list(children or [])):
|
||||
if not child or child.isEmpty():
|
||||
continue
|
||||
node_id = _node_path_id(parent_id, index)
|
||||
if not _node_is_serializable(child):
|
||||
continue
|
||||
|
||||
node_name = child.getName()
|
||||
imported_node_key = _resolve_imported_node_key(child, node_id)
|
||||
@ -387,6 +439,9 @@ def _build_scene_description_payload(
|
||||
runtime_interactive,
|
||||
light_component,
|
||||
material_component,
|
||||
) or any(
|
||||
_node_is_serializable(grandchild)
|
||||
for grandchild in list(child.getChildren()) if grandchild and not grandchild.isEmpty()
|
||||
):
|
||||
try:
|
||||
child_nodes = list(child.getChildren())
|
||||
|
||||
@ -242,6 +242,81 @@ class ObjectController:
|
||||
|
||||
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):
|
||||
"""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:
|
||||
return
|
||||
|
||||
proxy = getattr(self, "_group_proxy", None)
|
||||
if self._node_is_valid(proxy):
|
||||
self._snapshot_active_selection_transform_to_source()
|
||||
|
||||
# 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:
|
||||
selection_key = str(proxy.getTag("ssbo_selection_key") or "").strip()
|
||||
model_root_mat = LMatrix4f(self.model.getMat(self.base.render))
|
||||
except Exception:
|
||||
selection_key = ""
|
||||
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
|
||||
model_root_mat = None
|
||||
|
||||
grouped_entries = {}
|
||||
for gid, obj_np in self.controller.id_to_object_np.items():
|
||||
if not self._node_is_valid(obj_np):
|
||||
if model_root_mat is None:
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
owner_key = self.controller.id_to_name.get(gid)
|
||||
if not owner_key or owner_key == getattr(self.controller, "tree_root_key", None):
|
||||
child_name = self._get_node_name(source_child, None)
|
||||
if not child_name:
|
||||
continue
|
||||
|
||||
source_node = self._resolve_source_node_by_tree_key(owner_key)
|
||||
if not self._node_is_valid(source_node):
|
||||
continue
|
||||
|
||||
try:
|
||||
current_net_mat = LMatrix4f(obj_np.get_mat(self.model))
|
||||
except Exception:
|
||||
base_mat = self._source_child_base_mats.get(child_name)
|
||||
if base_mat is None:
|
||||
try:
|
||||
current_net_mat = LMatrix4f(obj_np.getMat(self.model))
|
||||
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))
|
||||
base_mat = LMatrix4f(source_child.get_mat())
|
||||
except Exception:
|
||||
try:
|
||||
parent_net_mat = LMatrix4f(source_parent.getMat(self.source_model_root))
|
||||
base_mat = LMatrix4f(source_child.getMat())
|
||||
except Exception:
|
||||
parent_net_mat = LMatrix4f.ident_mat()
|
||||
|
||||
inv_parent_mat = LMatrix4f(parent_net_mat)
|
||||
continue
|
||||
composed_mat = LMatrix4f(base_mat)
|
||||
composed_mat *= model_root_mat
|
||||
try:
|
||||
inv_parent_mat.invertInPlace()
|
||||
source_child.set_mat(composed_mat)
|
||||
source_child.setTag("scene_transform_dirty", "true")
|
||||
except Exception:
|
||||
try:
|
||||
inv_parent_mat.invert_in_place()
|
||||
source_child.setMat(composed_mat)
|
||||
source_child.setTag("scene_transform_dirty", "true")
|
||||
except Exception:
|
||||
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:
|
||||
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:
|
||||
try:
|
||||
source_node.setMat(local_mat)
|
||||
parent_net_mat = LMatrix4f(source_parent.getMat(self.source_model_root))
|
||||
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):
|
||||
"""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)
|
||||
if target_snapshot is not None:
|
||||
apply_snapshot_fn(source_node, target_snapshot)
|
||||
source_node.setTag("scene_material_dirty", "true")
|
||||
synced += 1
|
||||
continue
|
||||
except Exception:
|
||||
@ -1145,6 +1162,7 @@ class SSBOEditor:
|
||||
try:
|
||||
if callable(set_src_state):
|
||||
set_src_state(src_geom_index, runtime_geom_state)
|
||||
source_node.setTag("scene_material_dirty", "true")
|
||||
synced += 1
|
||||
except Exception:
|
||||
continue
|
||||
@ -1223,6 +1241,7 @@ class SSBOEditor:
|
||||
self.clear_selection()
|
||||
self._cleanup_group_proxy()
|
||||
self._reset_pick_sync_cache()
|
||||
self._teardown_gpu_picking()
|
||||
|
||||
controller = self.controller
|
||||
pick_model = getattr(controller, "pick_model", None) if controller else None
|
||||
@ -1440,6 +1459,7 @@ class SSBOEditor:
|
||||
keep_source_model=False,
|
||||
append=False,
|
||||
scene_package_import=False,
|
||||
rebuild_runtime=True,
|
||||
):
|
||||
"""Load and process one model into the aggregated SSBO scene."""
|
||||
print(f"[SSBOEditor] Loading model: {model_path}")
|
||||
@ -1511,7 +1531,8 @@ class SSBOEditor:
|
||||
self.source_model = source_root
|
||||
self._restore_saved_material_bindings_from_tags(source_root)
|
||||
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:
|
||||
return imported_roots[0]
|
||||
return source_root
|
||||
@ -1529,7 +1550,8 @@ class SSBOEditor:
|
||||
self.source_model = source_root
|
||||
|
||||
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
|
||||
|
||||
def _build_filename_candidates(self, path_text):
|
||||
@ -2106,7 +2128,8 @@ class SSBOEditor:
|
||||
|
||||
def setup_gpu_picking(self):
|
||||
"""Setup GPU Picking (Basic implementation)"""
|
||||
# ... (Buffer setup code remains same) ...
|
||||
self._teardown_gpu_picking()
|
||||
|
||||
win_props = WindowProperties()
|
||||
win_props.set_size(1, 1)
|
||||
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_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):
|
||||
"""Sync pick-scene clone to current source model world transform."""
|
||||
if not self.controller or not self.model:
|
||||
@ -2744,6 +2796,7 @@ class SSBOEditor:
|
||||
try:
|
||||
self._clear_runtime_state(preserve_source_models=True)
|
||||
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:
|
||||
try:
|
||||
if (
|
||||
@ -2759,6 +2812,74 @@ class SSBOEditor:
|
||||
print(f"[SSBOEditor] 从 source 刷新 runtime 失败: {e}")
|
||||
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):
|
||||
selection = getattr(self.base, "selection", None)
|
||||
if not selection:
|
||||
@ -2819,6 +2940,7 @@ class SSBOEditor:
|
||||
self.clear_selection(sync_world_selection=False)
|
||||
|
||||
def clear_selection(self, sync_world_selection=True):
|
||||
self._snapshot_active_selection_transform_to_source()
|
||||
self._stop_pick_sync_task()
|
||||
self._reset_pick_sync_cache()
|
||||
self._cleanup_group_proxy()
|
||||
@ -2877,6 +2999,7 @@ class SSBOEditor:
|
||||
pass # No selection mask texture needed without custom shader
|
||||
|
||||
def select_node(self, key, sync_world_selection=True):
|
||||
self._snapshot_active_selection_transform_to_source()
|
||||
# Clean up previous group proxy before changing selection
|
||||
self._cleanup_group_proxy()
|
||||
self._reset_pick_sync_cache()
|
||||
|
||||
@ -1540,6 +1540,10 @@ class AppActions:
|
||||
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
||||
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))
|
||||
if animated_model:
|
||||
prefer_scene_manager = True
|
||||
@ -1584,6 +1588,77 @@ class AppActions:
|
||||
return self.scene_manager.importModel(file_path)
|
||||
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(
|
||||
self,
|
||||
file_path,
|
||||
@ -1667,6 +1742,16 @@ class AppActions:
|
||||
|
||||
if show_success_message:
|
||||
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
|
||||
except Exception as e:
|
||||
self.add_error_message(f"导入模型失败: {e}")
|
||||
|
||||
@ -155,6 +155,12 @@ class EditorPanelsLeftMixin:
|
||||
count += 1
|
||||
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):
|
||||
filter_text = self._get_scene_tree_filter()
|
||||
if not filter_text:
|
||||
@ -446,10 +452,11 @@ class EditorPanelsLeftMixin:
|
||||
obj_count = len(controller.name_to_ids.get(key, []))
|
||||
children = node_data["children"]
|
||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||
tree_epoch = self._get_scene_tree_epoch()
|
||||
|
||||
if not children:
|
||||
# Leaf node: selectable
|
||||
label = f"{display} ({obj_count})##{key}"
|
||||
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||||
if imgui.selectable(label, is_selected)[0]:
|
||||
ssbo_editor.select_node(key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
@ -465,7 +472,7 @@ class EditorPanelsLeftMixin:
|
||||
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||||
if is_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)
|
||||
if imgui.is_item_clicked(0):
|
||||
ssbo_editor.select_node(key)
|
||||
@ -495,6 +502,7 @@ class EditorPanelsLeftMixin:
|
||||
obj_count = len(controller.name_to_ids.get(key, []))
|
||||
is_selected = (getattr(ssbo_editor, "selected_name", None) == key)
|
||||
children = list((virtual_node.get("children", {}) or {}).values())
|
||||
tree_epoch = self._get_scene_tree_epoch()
|
||||
|
||||
if display.strip().lower() == "root" and children:
|
||||
for child in children:
|
||||
@ -504,7 +512,7 @@ class EditorPanelsLeftMixin:
|
||||
return
|
||||
|
||||
if not children:
|
||||
label = f"{display} ({obj_count})##{key}"
|
||||
label = f"{display} ({obj_count})##{tree_epoch}:{key}"
|
||||
if imgui.selectable(label, is_selected)[0]:
|
||||
ssbo_editor.select_node(key)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
@ -520,7 +528,7 @@ class EditorPanelsLeftMixin:
|
||||
flags = imgui.TreeNodeFlags_.open_on_arrow
|
||||
if is_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)
|
||||
if imgui.is_item_clicked(0):
|
||||
ssbo_editor.select_node(key)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user