imgui优化
This commit is contained in:
parent
c5dbc6be6f
commit
780536203e
BIN
Resources/models/box1.glb
Normal file
BIN
Resources/models/box1.glb
Normal file
Binary file not shown.
@ -238,7 +238,7 @@ class EventHandler:
|
||||
|
||||
# 优先检查是否点击了坐标轴
|
||||
#print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}")
|
||||
if self.world.selection.gizmo:
|
||||
if self.world.selection.hasLegacyGizmoInput():
|
||||
#print("准备检查坐标轴点击...")
|
||||
try:
|
||||
highlighted_axis = self.world.selection.gizmoHighlightAxis
|
||||
@ -553,7 +553,7 @@ class EventHandler:
|
||||
def mouseReleaseEventLeft(self, evt):
|
||||
"""处理鼠标左键释放事件"""
|
||||
# 处理坐标轴拖拽结束
|
||||
if self.world.selection.isDraggingGizmo:
|
||||
if self.world.selection.isLegacyGizmoDragActive():
|
||||
self.world.selection.stopGizmoDrag()
|
||||
return
|
||||
|
||||
@ -599,7 +599,7 @@ class EventHandler:
|
||||
return
|
||||
|
||||
# 处理坐标轴拖拽
|
||||
if self.world.selection.isDraggingGizmo:
|
||||
if self.world.selection.isLegacyGizmoDragActive():
|
||||
x = evt.get('x', 0)
|
||||
y = evt.get('y', 0)
|
||||
|
||||
@ -615,7 +615,7 @@ class EventHandler:
|
||||
return
|
||||
|
||||
# 更新坐标轴高亮(鼠标悬停效果)
|
||||
if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo:
|
||||
if self.world.selection.hasLegacyGizmoInput() and not self.world.selection.isLegacyGizmoDragActive():
|
||||
x = evt.get('x', 0)
|
||||
y = evt.get('y', 0)
|
||||
# 减少高亮调试输出,只在需要时输出
|
||||
|
||||
@ -339,6 +339,7 @@ class ImGuiStyleManager:
|
||||
style.scrollbar_rounding = self.sizes['frame_rounding']
|
||||
style.grab_min_size = 10.0
|
||||
style.grab_rounding = self.sizes['frame_rounding']
|
||||
style.window_menu_button_position = imgui.Dir_.none
|
||||
|
||||
# 禁用一些ImGui的默认效果,使其更像Qt
|
||||
style.window_border_size = 1.0
|
||||
@ -377,6 +378,14 @@ class ImGuiStyleManager:
|
||||
flags = self.get_window_flags(window_type)
|
||||
|
||||
return imgui_ctx.begin(name, open, flags)
|
||||
|
||||
def prepare_centered_dialog(self, width, height, cond=imgui.Cond_.appearing):
|
||||
"""Place a modal/dialog in the center of the current main viewport."""
|
||||
viewport = imgui.get_main_viewport()
|
||||
center_x = viewport.pos.x + (viewport.size.x - width) / 2
|
||||
center_y = viewport.pos.y + (viewport.size.y - height) / 2
|
||||
imgui.set_next_window_size((width, height), cond)
|
||||
imgui.set_next_window_pos((center_x, center_y), cond)
|
||||
|
||||
def styled_button(self, label, size=(0, 0)):
|
||||
"""绘制带样式的按钮"""
|
||||
@ -428,4 +437,4 @@ class ImGuiStyleManager:
|
||||
imgui.same_line()
|
||||
|
||||
# 再绘制文本按钮
|
||||
return imgui.button(text, size)
|
||||
return imgui.button(text, size)
|
||||
|
||||
@ -72,7 +72,7 @@ class ScriptBase(ABC):
|
||||
class ScriptComponent:
|
||||
"""脚本组件 - 挂载到游戏对象上的脚本实例"""
|
||||
|
||||
def __init__(self, script_instance: ScriptBase, game_object, script_manager):
|
||||
def __init__(self, script_instance: ScriptBase, game_object, script_manager, script_key: Optional[str] = None):
|
||||
self.script_instance = script_instance
|
||||
self.game_object = game_object
|
||||
self.script_manager = script_manager
|
||||
@ -80,6 +80,7 @@ class ScriptComponent:
|
||||
|
||||
# 保存脚本名称,便于UI显示
|
||||
self.script_name = script_instance.__class__.__name__
|
||||
self.script_key = script_key or self.script_name
|
||||
|
||||
# 设置脚本实例的引用
|
||||
script_instance.gameObject = game_object
|
||||
@ -248,7 +249,7 @@ class ScriptLoader:
|
||||
# 移除所有使用此脚本的组件
|
||||
components_to_remove = []
|
||||
for component in self.script_manager.engine.script_components:
|
||||
if component.script_instance.__class__.__name__ == script_name:
|
||||
if self.script_manager._script_matches(component, script_name):
|
||||
components_to_remove.append(component)
|
||||
|
||||
for component in components_to_remove:
|
||||
@ -510,6 +511,19 @@ class ExampleScript(ScriptBase):
|
||||
self.engine.stop_engine()
|
||||
self.stop_hot_reload()
|
||||
print("✓ 脚本系统已停止")
|
||||
|
||||
def reset_scene_state(self):
|
||||
"""Clear all mounted script components before loading/replacing a scene."""
|
||||
try:
|
||||
for component in list(self.engine.script_components):
|
||||
try:
|
||||
self.engine.remove_script_component(component)
|
||||
except Exception as e:
|
||||
print(f"移除脚本组件失败: {e}")
|
||||
self.object_scripts.clear()
|
||||
print("✓ 脚本场景状态已清空")
|
||||
except Exception as e:
|
||||
print(f"清空脚本场景状态失败: {e}")
|
||||
|
||||
def start_hot_reload(self):
|
||||
"""启动热重载监控"""
|
||||
@ -534,7 +548,8 @@ class ExampleScript(ScriptBase):
|
||||
|
||||
def create_script_file(self, script_name: str, template: str = "basic") -> str:
|
||||
"""创建新的脚本文件"""
|
||||
script_path = os.path.join(self.scripts_directory, f"{script_name}.py")
|
||||
script_base_name = os.path.splitext(script_name.strip())[0]
|
||||
script_path = os.path.join(self.scripts_directory, f"{script_base_name}.py")
|
||||
|
||||
if os.path.exists(script_path):
|
||||
print(f"脚本文件已存在: {script_path}")
|
||||
@ -542,21 +557,33 @@ class ExampleScript(ScriptBase):
|
||||
|
||||
# 根据模板创建脚本
|
||||
if template == "basic":
|
||||
script_content = self._get_basic_script_template(script_name)
|
||||
script_content = self._get_basic_script_template(script_base_name)
|
||||
elif template == "movement":
|
||||
script_content = self._get_movement_script_template(script_name)
|
||||
script_content = self._get_movement_script_template(script_base_name)
|
||||
else:
|
||||
script_content = self._get_basic_script_template(script_name)
|
||||
script_content = self._get_basic_script_template(script_base_name)
|
||||
|
||||
with open(script_path, 'w', encoding='utf-8') as f:
|
||||
f.write(script_content)
|
||||
|
||||
print(f"✓ 创建脚本文件: {script_path}")
|
||||
return script_path
|
||||
|
||||
def _build_script_class_name(self, script_name: str) -> str:
|
||||
normalized_parts = []
|
||||
for raw_part in script_name.replace('-', '_').split('_'):
|
||||
part = ''.join(ch for ch in raw_part if ch.isalnum())
|
||||
if part:
|
||||
normalized_parts.append(part.capitalize())
|
||||
|
||||
class_name = ''.join(normalized_parts) or "GeneratedScript"
|
||||
if class_name[0].isdigit():
|
||||
class_name = f"Script{class_name}"
|
||||
return class_name
|
||||
|
||||
def _get_basic_script_template(self, script_name: str) -> str:
|
||||
"""获取基础脚本模板"""
|
||||
class_name = ''.join(word.capitalize() for word in script_name.split('_'))
|
||||
class_name = self._build_script_class_name(script_name)
|
||||
|
||||
return f'''#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@ -590,7 +617,7 @@ class {class_name}(ScriptBase):
|
||||
|
||||
def _get_movement_script_template(self, script_name: str) -> str:
|
||||
"""获取移动脚本模板"""
|
||||
class_name = ''.join(word.capitalize() for word in script_name.split('_'))
|
||||
class_name = self._build_script_class_name(script_name)
|
||||
|
||||
return f'''#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@ -675,7 +702,7 @@ class {class_name}(ScriptBase):
|
||||
script_instance = script_class()
|
||||
|
||||
# 创建脚本组件
|
||||
script_component = ScriptComponent(script_instance, game_object, self)
|
||||
script_component = ScriptComponent(script_instance, game_object, self, script_key=script_name)
|
||||
|
||||
# 添加到对象的脚本列表
|
||||
if game_object not in self.object_scripts:
|
||||
@ -702,7 +729,7 @@ class {class_name}(ScriptBase):
|
||||
removed = False
|
||||
|
||||
for component in script_components[:]: # 复制列表以避免修改时出错
|
||||
if component.script_instance.__class__.__name__ == script_name:
|
||||
if self._script_matches(component, script_name):
|
||||
# 从引擎移除
|
||||
self.engine.remove_script_component(component)
|
||||
# 从对象脚本列表移除
|
||||
@ -721,6 +748,13 @@ class {class_name}(ScriptBase):
|
||||
|
||||
return removed
|
||||
|
||||
def _script_matches(self, component: ScriptComponent, script_identifier: str) -> bool:
|
||||
return script_identifier in {
|
||||
getattr(component, "script_key", None),
|
||||
getattr(component, "script_name", None),
|
||||
component.script_instance.__class__.__name__,
|
||||
}
|
||||
|
||||
def _update_node_script_tags_after_removal(self, game_object, removed_script_name):
|
||||
"""在移除脚本后更新节点标签"""
|
||||
try:
|
||||
@ -763,7 +797,7 @@ class {class_name}(ScriptBase):
|
||||
"""获取对象上的特定脚本"""
|
||||
scripts = self.get_scripts_on_object(game_object)
|
||||
for script in scripts:
|
||||
if script.script_instance.__class__.__name__ == script_name:
|
||||
if self._script_matches(script, script_name):
|
||||
return script
|
||||
return None
|
||||
|
||||
@ -783,7 +817,7 @@ class {class_name}(ScriptBase):
|
||||
"name": script_name,
|
||||
"class": script_class,
|
||||
"doc": script_class.__doc__,
|
||||
"file": inspect.getfile(script_class) if hasattr(script_class, '__file__') else None,
|
||||
"file": self.loader.find_script_file(script_name) or inspect.getsourcefile(script_class),
|
||||
"methods": [method for method in dir(script_class) if not method.startswith('_')]
|
||||
}
|
||||
|
||||
@ -793,6 +827,18 @@ class {class_name}(ScriptBase):
|
||||
if script_info and script_info["file"]:
|
||||
return self.loader.reload_script(script_info["file"]) is not None
|
||||
return False
|
||||
|
||||
def set_hot_reload_enabled(self, enabled: bool):
|
||||
"""切换热重载并同步后台监控任务。"""
|
||||
enabled = bool(enabled)
|
||||
if self.hot_reload_enabled == enabled:
|
||||
return
|
||||
|
||||
self.hot_reload_enabled = enabled
|
||||
if enabled:
|
||||
self.start_hot_reload()
|
||||
else:
|
||||
self.stop_hot_reload()
|
||||
|
||||
# ==================== 调试功能 ====================
|
||||
|
||||
@ -858,4 +904,4 @@ def get_script_api():
|
||||
__all__ = [
|
||||
'ScriptBase', 'ScriptComponent', 'ScriptEngine',
|
||||
'ScriptLoader', 'ScriptAPI', 'ScriptManager'
|
||||
]
|
||||
]
|
||||
|
||||
@ -127,6 +127,77 @@ class SelectionSystem:
|
||||
tg = getattr(self.world, "newTransform", None)
|
||||
return tg is not None
|
||||
|
||||
def _has_attached_transform_gizmo(self, nodePath=None):
|
||||
"""Return whether a transform gizmo is currently attached in either legacy or new mode."""
|
||||
if self._has_new_transform_gizmo():
|
||||
try:
|
||||
target = getattr(self.world.newTransform, "target_node", None)
|
||||
if nodePath is not None:
|
||||
return target == nodePath
|
||||
return target is not None and not target.isEmpty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if not self.gizmo:
|
||||
return False
|
||||
try:
|
||||
if self.gizmo.isEmpty():
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
if nodePath is None:
|
||||
return True
|
||||
return self.gizmoTarget == nodePath
|
||||
|
||||
def _has_legacy_gizmo_input(self):
|
||||
"""Return whether legacy gizmo hit-testing/dragging is active."""
|
||||
if self._has_new_transform_gizmo():
|
||||
return False
|
||||
if not self.gizmo or not self.gizmoTarget:
|
||||
return False
|
||||
try:
|
||||
return not self.gizmo.isEmpty() and not self.gizmoTarget.isEmpty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def hasLegacyGizmoInput(self):
|
||||
"""Compatibility helper for event routing."""
|
||||
return self._has_legacy_gizmo_input()
|
||||
|
||||
def isLegacyGizmoDragActive(self):
|
||||
"""Return whether the legacy gizmo drag loop should keep consuming mouse input."""
|
||||
return self.isDraggingGizmo and self._has_legacy_gizmo_input()
|
||||
|
||||
def _get_effective_selected_node(self):
|
||||
"""Resolve the current editor selection across legacy and SSBO selection sources."""
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection"):
|
||||
try:
|
||||
if ssbo_editor.has_active_selection():
|
||||
node = ssbo_editor.get_selection_scene_node()
|
||||
if node and not node.isEmpty():
|
||||
return node
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
resolver = getattr(self.world, "_get_selection_node", None)
|
||||
if callable(resolver):
|
||||
try:
|
||||
node = resolver()
|
||||
if node and not node.isEmpty():
|
||||
return node
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
node = self.selectedNode
|
||||
if not node:
|
||||
return None
|
||||
try:
|
||||
return None if node.isEmpty() else node
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _sync_rp_light_position(self, light_node, light_object=None):
|
||||
"""同步灯光包装节点与 RenderPipeline 灯光对象位置。"""
|
||||
try:
|
||||
@ -1220,7 +1291,7 @@ class SelectionSystem:
|
||||
|
||||
def checkGizmoClick(self, mouseX, mouseY):
|
||||
"""使用屏幕空间检测是否点击了坐标轴"""
|
||||
if not self.gizmo or not self.gizmoTarget:
|
||||
if not self._has_legacy_gizmo_input():
|
||||
return None
|
||||
|
||||
# 基本参数验证
|
||||
@ -1381,7 +1452,7 @@ class SelectionSystem:
|
||||
|
||||
def detectGizmoAxisAtMouse(self, mouseX, mouseY):
|
||||
"""统一的坐标轴检测方法 - 同时用于高亮和点击检测"""
|
||||
if not self.gizmo or not self.gizmoTarget:
|
||||
if not self._has_legacy_gizmo_input():
|
||||
return None
|
||||
|
||||
try:
|
||||
@ -1497,7 +1568,7 @@ class SelectionSystem:
|
||||
|
||||
def updateGizmoHighlight(self, mouseX, mouseY):
|
||||
"""更新坐标轴高亮状态"""
|
||||
if not self.gizmo or self.isDraggingGizmo:
|
||||
if not self._has_legacy_gizmo_input() or self.isDraggingGizmo:
|
||||
self._resetCursor()
|
||||
return
|
||||
|
||||
@ -1557,6 +1628,8 @@ class SelectionSystem:
|
||||
def startGizmoDrag(self, axis, mouseX, mouseY):
|
||||
"""开始坐标轴拖拽"""
|
||||
try:
|
||||
if not self._has_legacy_gizmo_input():
|
||||
return
|
||||
# 确保状态正确初始化
|
||||
if not self.gizmoTarget:
|
||||
print("开始拖拽失败: 没有拖拽目标")
|
||||
@ -1928,6 +2001,21 @@ class SelectionSystem:
|
||||
|
||||
def stopGizmoDrag(self):
|
||||
"""停止坐标轴拖拽并创建撤销命令"""
|
||||
if not self.isDraggingGizmo:
|
||||
return
|
||||
|
||||
if not self._has_legacy_gizmo_input():
|
||||
self.isDraggingGizmo = False
|
||||
self.dragGizmoAxis = None
|
||||
self.dragStartMousePos = None
|
||||
self.gizmoTargetStartPos = None
|
||||
self.gizmoTargetStartScale = None
|
||||
self.gizmoTargetStartHpr = None
|
||||
self.gizmoStartPos = None
|
||||
self.gizmoHighlightAxis = None
|
||||
self._resetCursor()
|
||||
return
|
||||
|
||||
print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}")
|
||||
|
||||
# 移除拖拽更新任务
|
||||
@ -2066,6 +2154,13 @@ class SelectionSystem:
|
||||
node_name = nodePath.getName()
|
||||
#print(f"新选择的节点: {node_name}")
|
||||
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
if ssbo_editor:
|
||||
try:
|
||||
ssbo_editor.sync_scene_selection(nodePath)
|
||||
except Exception as e:
|
||||
print(f"同步 SSBO 选择状态失败: {e}")
|
||||
|
||||
self.selectedNode = nodePath
|
||||
# 添加兼容性属性
|
||||
self.selectedObject = nodePath
|
||||
@ -2093,9 +2188,11 @@ class SelectionSystem:
|
||||
#print("创建坐标轴...")
|
||||
self._updateSelectionOutline(nodePath)
|
||||
self.createGizmo(nodePath)
|
||||
if self.gizmo:
|
||||
if self._has_attached_transform_gizmo(nodePath):
|
||||
gizmo_name = "Unknown"
|
||||
if self.gizmo and not self.gizmo.isEmpty():
|
||||
if self._has_new_transform_gizmo():
|
||||
gizmo_name = getattr(nodePath, "getName", lambda: "TransformGizmo")()
|
||||
elif self.gizmo and not self.gizmo.isEmpty():
|
||||
gizmo_name = self.gizmo.getName()
|
||||
#print(f"✓ 坐标轴创建成功: {gizmo_name}")
|
||||
else:
|
||||
@ -2177,11 +2274,11 @@ class SelectionSystem:
|
||||
|
||||
def getSelectedNode(self):
|
||||
"""获取当前选中的节点"""
|
||||
return self.selectedNode
|
||||
return self._get_effective_selected_node()
|
||||
|
||||
def deleteSelectedNode(self):
|
||||
"""兼容旧接口:删除当前选中节点。"""
|
||||
node = self.selectedNode
|
||||
node = self._get_effective_selected_node()
|
||||
if not node or node.isEmpty():
|
||||
return False
|
||||
|
||||
@ -2215,7 +2312,7 @@ class SelectionSystem:
|
||||
|
||||
def hasSelection(self):
|
||||
"""检查是否有选中的节点"""
|
||||
return self.selectedNode is not None
|
||||
return self._get_effective_selected_node() is not None
|
||||
|
||||
def checkAndClearIfTargetDeleted(self):
|
||||
if (self.gizmoTarget and self.gizmoTarget.isEmpty()):
|
||||
@ -2391,7 +2488,8 @@ class SelectionSystem:
|
||||
def focusCameraOnSelectedNodeAdvanced(self):
|
||||
"""高级版的摄像机聚焦功能,包含平滑动画效果"""
|
||||
try:
|
||||
if not self.selectedNode or self.selectedNode.isEmpty():
|
||||
selected_node = self._get_effective_selected_node()
|
||||
if not selected_node or selected_node.isEmpty():
|
||||
print("没有选中的节点,无法聚焦")
|
||||
return False
|
||||
|
||||
@ -2399,9 +2497,9 @@ class SelectionSystem:
|
||||
minPoint = Point3()
|
||||
maxPoint = Point3()
|
||||
|
||||
if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||||
if not selected_node.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||||
print("无法计算选中节点的边界框,使用节点为位置作为替代方案")
|
||||
node_pos = self.selectedNode.getPos(self.world.render)
|
||||
node_pos = selected_node.getPos(self.world.render)
|
||||
optimal_distance = 10.0
|
||||
current_cam_pos = self.world.cam.getPos()
|
||||
view_direction = node_pos - current_cam_pos
|
||||
@ -2423,7 +2521,7 @@ class SelectionSystem:
|
||||
currrent_cam_pos = Point3(self.world.cam.getPos())
|
||||
current_cam_hpr = Vec3(self.world.cam.getHpr())
|
||||
self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr)
|
||||
print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}")
|
||||
print(f"开始聚焦到节点(使用位置): {selected_node.getName()}")
|
||||
return True
|
||||
|
||||
# 计算节点中心点和大小
|
||||
@ -2476,7 +2574,7 @@ class SelectionSystem:
|
||||
self._startCameraFocusAnimation(current_cam_pos, target_cam_pos,
|
||||
current_cam_hpr, target_cam_hpr)
|
||||
|
||||
print(f"开始聚焦到节点: {self.selectedNode.getName()}")
|
||||
print(f"开始聚焦到节点: {selected_node.getName()}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@ -2679,8 +2777,9 @@ class SelectionSystem:
|
||||
(hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))):
|
||||
is_gizmo_click = True
|
||||
# 如果有选中的模型,使用选中的模型作为聚焦目标
|
||||
if self.selectedNode and not self.selectedNode.isEmpty():
|
||||
target_node = self.selectedNode
|
||||
selected_node = self._get_effective_selected_node()
|
||||
if selected_node and not selected_node.isEmpty():
|
||||
target_node = selected_node
|
||||
print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}")
|
||||
|
||||
# 检查是否为双击(同一节点且在时间阈值内)
|
||||
@ -2696,7 +2795,7 @@ class SelectionSystem:
|
||||
# 无论是点击模型还是坐标轴,都执行聚焦
|
||||
if target_node and not target_node.isEmpty():
|
||||
print(f"双击聚焦到节点: {target_node.getName()}")
|
||||
if self.selectedNode != target_node:
|
||||
if self._get_effective_selected_node() != target_node:
|
||||
self.updateSelection(target_node)
|
||||
else:
|
||||
self.focusCameraOnSelectedNodeAdvanced()
|
||||
@ -2730,8 +2829,9 @@ class SelectionSystem:
|
||||
# 如果是坐标轴,确保使用关联的模型作为目标
|
||||
if (nodePath and hasattr(nodePath, 'hasTag') and
|
||||
nodePath.hasTag("is_gizmo")):
|
||||
if self.selectedNode and not self.selectedNode.isEmpty():
|
||||
target_node = self.selectedNode
|
||||
selected_node = self._get_effective_selected_node()
|
||||
if selected_node and not selected_node.isEmpty():
|
||||
target_node = selected_node
|
||||
print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}")
|
||||
else:
|
||||
print("坐标轴双击,但没有关联的选中模型")
|
||||
@ -2740,7 +2840,7 @@ class SelectionSystem:
|
||||
if target_node and not target_node.isEmpty():
|
||||
print(f"双击聚焦到节点: {target_node.getName()}")
|
||||
# 更新选择(如果需要)
|
||||
if self.selectedNode != target_node:
|
||||
if self._get_effective_selected_node() != target_node:
|
||||
self.updateSelection(target_node)
|
||||
|
||||
# 执行聚焦
|
||||
@ -2923,10 +3023,10 @@ class SelectionSystem:
|
||||
def triggerDoubleClickFocus(self, nodePath=None):
|
||||
"""手动触发双击聚焦"""
|
||||
try:
|
||||
target_node = nodePath if nodePath else self.selectedNode
|
||||
target_node = nodePath if nodePath else self._get_effective_selected_node()
|
||||
if target_node and not target_node.isEmpty():
|
||||
print(f"手动触发聚焦到节点: {target_node.getName()}")
|
||||
if self.selectedNode != target_node:
|
||||
if self._get_effective_selected_node() != target_node:
|
||||
self.updateSelection(target_node)
|
||||
self.focusCameraOnSelectedNodeAdvanced()
|
||||
return True
|
||||
@ -2951,6 +3051,12 @@ class SelectionSystem:
|
||||
def clearSelection(self):
|
||||
"""清除当前选择"""
|
||||
try:
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
if ssbo_editor:
|
||||
try:
|
||||
ssbo_editor.clear_selection(sync_world_selection=False)
|
||||
except Exception as e:
|
||||
print(f"清除 SSBO 选择失败: {e}")
|
||||
self.selectedNode = None
|
||||
self.selectedObject = None
|
||||
self.clearSelectionBox()
|
||||
|
||||
@ -9,11 +9,12 @@ class ToolManager:
|
||||
|
||||
def setCurrentTool(self, tool):
|
||||
"""设置当前工具"""
|
||||
self.currentTool = tool
|
||||
|
||||
print(f"\n=== 工具切换 ===")
|
||||
print(f"当前工具: {tool}")
|
||||
print(f"选中节点: {self.world.selection.selectedNode.getName() if self.world.selection.selectedNode else '无'}")
|
||||
self.currentTool = tool
|
||||
|
||||
print(f"\n=== 工具切换 ===")
|
||||
print(f"当前工具: {tool}")
|
||||
selected_node = self.world._get_selection_node() if hasattr(self.world, "_get_selection_node") else self.world.selection.selectedNode
|
||||
print(f"选中节点: {selected_node.getName() if selected_node else '无'}")
|
||||
|
||||
# 根据工具类型启用对应的方法
|
||||
if tool == "选择":
|
||||
|
||||
@ -9,7 +9,8 @@ from direct.actor.Actor import Actor
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
|
||||
from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight,
|
||||
Point3, WindowProperties, Material, LColor, loadPrcFileData)
|
||||
Point3, WindowProperties, Material, LColor, loadPrcFileData,
|
||||
GraphicsPipeSelection)
|
||||
from direct.showbase.ShowBase import ShowBase
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from scene.scene_manager import SceneManager
|
||||
@ -41,6 +42,10 @@ class CoreWorld(ShowBase):
|
||||
|
||||
# 初始化基础属性
|
||||
self.host_widget = None # 外部宿主窗口引用(用于获取准确渲染区域尺寸)
|
||||
desktop_size = self._get_desktop_window_size()
|
||||
if not is_fullscreen and (width, height) == (1380, 750) and desktop_size:
|
||||
width, height = desktop_size
|
||||
print(f"✓ 使用桌面分辨率启动窗口: {width} x {height}")
|
||||
|
||||
# 设置基本配置
|
||||
loadPrcFileData("", "show-frame-rate-meter 0")
|
||||
@ -112,6 +117,50 @@ class CoreWorld(ShowBase):
|
||||
self._setupGround()
|
||||
self._loadFont()
|
||||
|
||||
@staticmethod
|
||||
def _get_desktop_window_size():
|
||||
"""Query the current desktop display mode for a better default window size."""
|
||||
if os.name == "nt":
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
class RECT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("left", wintypes.LONG),
|
||||
("top", wintypes.LONG),
|
||||
("right", wintypes.LONG),
|
||||
("bottom", wintypes.LONG),
|
||||
]
|
||||
|
||||
work_area = RECT()
|
||||
spi_get_work_area = 0x0030
|
||||
if ctypes.windll.user32.SystemParametersInfoW(
|
||||
spi_get_work_area, 0, ctypes.byref(work_area), 0
|
||||
):
|
||||
width = int(work_area.right - work_area.left)
|
||||
height = int(work_area.bottom - work_area.top)
|
||||
if width > 0 and height > 0:
|
||||
return width, height
|
||||
except Exception as e:
|
||||
print(f"⚠ 获取 Windows 工作区尺寸失败,回退到显示模式尺寸: {e}")
|
||||
|
||||
try:
|
||||
pipe = GraphicsPipeSelection.getGlobalPtr().makeDefaultPipe()
|
||||
if not pipe:
|
||||
return None
|
||||
info = pipe.getDisplayInformation()
|
||||
current_mode = info.getCurrentDisplayModeIndex()
|
||||
if current_mode < 0:
|
||||
return None
|
||||
width = int(info.getDisplayModeWidth(current_mode))
|
||||
height = int(info.getDisplayModeHeight(current_mode))
|
||||
if width > 0 and height > 0:
|
||||
return width, height
|
||||
except Exception as e:
|
||||
print(f"⚠ 获取桌面分辨率失败,继续使用默认窗口尺寸: {e}")
|
||||
return None
|
||||
|
||||
def _handle_transform_error(self):
|
||||
"""处理TransformState相关的错误"""
|
||||
try:
|
||||
@ -184,6 +233,7 @@ class CoreWorld(ShowBase):
|
||||
print(f"✓ 创建并添加子目录: {subdir}")
|
||||
|
||||
# 设置纹理搜索路径
|
||||
texture_path = None
|
||||
try:
|
||||
from panda3d.core import getTexturePath
|
||||
texture_path = getTexturePath()
|
||||
@ -193,12 +243,13 @@ class CoreWorld(ShowBase):
|
||||
# 新版本 Panda3D 中 getTexturePath 可能不可用
|
||||
print(" 注意: getTexturePath 不可用,使用默认纹理路径")
|
||||
|
||||
for subdir in ['textures', 'materials', 'icons']:
|
||||
subdir_path = os.path.join(resources_dir, subdir)
|
||||
if os.path.exists(subdir_path):
|
||||
subdir_filename = Filename.from_os_specific(subdir_path)
|
||||
if not texture_path.findFile(subdir_filename):
|
||||
texture_path.appendDirectory(subdir_filename)
|
||||
if texture_path is not None:
|
||||
for subdir in ['textures', 'materials', 'icons']:
|
||||
subdir_path = os.path.join(resources_dir, subdir)
|
||||
if os.path.exists(subdir_path):
|
||||
subdir_filename = Filename.from_os_specific(subdir_path)
|
||||
if not texture_path.findFile(subdir_filename):
|
||||
texture_path.appendDirectory(subdir_filename)
|
||||
|
||||
print(f"✓ 资源路径设置完成")
|
||||
print(f" 项目根目录: {project_root}")
|
||||
|
||||
52
imgui.ini
52
imgui.ini
@ -24,28 +24,28 @@ Size=832,45
|
||||
Collapsed=0
|
||||
|
||||
[Window][工具栏]
|
||||
Pos=278,20
|
||||
Size=2013,32
|
||||
Pos=453,20
|
||||
Size=1326,32
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=276,854
|
||||
Size=451,748
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=2293,20
|
||||
Size=267,1331
|
||||
Pos=1781,20
|
||||
Size=267,748
|
||||
Collapsed=0
|
||||
DockId=0x00000003,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=0,876
|
||||
Size=276,475
|
||||
Pos=0,770
|
||||
Size=2048,334
|
||||
Collapsed=0
|
||||
DockId=0x00000008,0
|
||||
DockId=0x0000000A,1
|
||||
|
||||
[Window][脚本管理]
|
||||
Pos=1653,20
|
||||
@ -60,7 +60,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=2560,1331
|
||||
Size=2048,1084
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -99,10 +99,10 @@ Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=278,1017
|
||||
Size=2013,334
|
||||
Pos=0,770
|
||||
Size=2048,334
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
DockId=0x0000000A,0
|
||||
|
||||
[Window][创建3D文本]
|
||||
Pos=60,60
|
||||
@ -150,8 +150,8 @@ Size=101,226
|
||||
Collapsed=0
|
||||
|
||||
[Window][LUI编辑器]
|
||||
Pos=1653,412
|
||||
Size=267,597
|
||||
Pos=1113,310
|
||||
Size=267,440
|
||||
Collapsed=0
|
||||
DockId=0x00000004,0
|
||||
|
||||
@ -201,17 +201,15 @@ Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1331 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1651,989 Split=X
|
||||
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=276,989 Split=Y Selected=0xE0015051
|
||||
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753
|
||||
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1373,989 Split=Y
|
||||
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,937 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,601 CentralNode=1
|
||||
DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,334 Selected=0x3A2E05C3
|
||||
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=267,989 Split=Y Selected=0x3188AB8D
|
||||
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x08BD597D SizeRef=2560,748 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=451,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=1595,1084 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1651,989 Split=Y
|
||||
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1
|
||||
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=267,989 Split=Y Selected=0x3188AB8D
|
||||
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7
|
||||
DockNode ID=0x0000000A Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3
|
||||
|
||||
|
||||
159
main.py
159
main.py
@ -5,27 +5,54 @@ import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
if __name__ == "__main__" and os.name == "nt" and sys.version_info < (3, 11):
|
||||
|
||||
def _maybe_relaunch_with_py311():
|
||||
"""On Windows, relaunch with Python 3.11 so ui/lui.pyd ABI matches."""
|
||||
if __name__ != "__main__":
|
||||
return
|
||||
if os.name != "nt" or sys.version_info >= (3, 11):
|
||||
return
|
||||
if os.environ.get("EG_RELAUNCHED_PY311") == "1":
|
||||
return
|
||||
|
||||
py_launcher = shutil.which("py")
|
||||
if py_launcher and os.environ.get("EG_RELAUNCHED_PY311") != "1":
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
[py_launcher, "-3.11", "-c", "import sys;print(sys.executable)"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if probe.returncode == 0:
|
||||
os.environ["EG_RELAUNCHED_PY311"] = "1"
|
||||
os.execv(py_launcher, [py_launcher, "-3.11", os.path.abspath(__file__), *sys.argv[1:]])
|
||||
else:
|
||||
print("⚠ 未检测到可用的 Python 3.11,LUI 可能不可用。")
|
||||
except Exception as relaunch_error:
|
||||
print(f"⚠ 自动切换 Python 3.11 失败: {relaunch_error}")
|
||||
if not py_launcher:
|
||||
print(f"⚠ 当前解释器是 Python {sys.version.split()[0]},未找到 py launcher,无法自动切换到 3.11。")
|
||||
return
|
||||
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
[py_launcher, "-3.11", "-c", "import sys;print(sys.executable)"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except Exception as relaunch_error:
|
||||
print(f"⚠ 自动切换 Python 3.11 失败: {relaunch_error}")
|
||||
return
|
||||
|
||||
if probe.returncode != 0:
|
||||
print("⚠ 未检测到可用的 Python 3.11,LUI 可能不可用。")
|
||||
return
|
||||
|
||||
target_python = (probe.stdout or "").strip().splitlines()
|
||||
target_python = target_python[-1].strip() if target_python else ""
|
||||
if not target_python or not os.path.exists(target_python):
|
||||
print("⚠ 检测到 Python 3.11,但无法解析解释器路径,继续使用当前解释器。")
|
||||
return
|
||||
|
||||
os.environ["EG_RELAUNCHED_PY311"] = "1"
|
||||
# Exec directly into the real Python 3.11 executable to avoid shell prompt flicker.
|
||||
os.execv(target_python, [target_python, os.path.abspath(__file__), *sys.argv[1:]])
|
||||
|
||||
|
||||
_maybe_relaunch_with_py311()
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
THIRD_PARTY_DIR = PROJECT_ROOT / "third_party"
|
||||
if str(THIRD_PARTY_DIR) not in sys.path:
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
if THIRD_PARTY_DIR.is_dir() and str(THIRD_PARTY_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(THIRD_PARTY_DIR))
|
||||
|
||||
from panda3d.core import loadPrcFileData, WindowProperties, Point3
|
||||
@ -38,9 +65,12 @@ from direct.interval.IntervalGlobal import Sequence
|
||||
from panda3d.core import NodePath
|
||||
|
||||
import p3dimgui
|
||||
from direct.showbase.MessengerGlobal import messenger
|
||||
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
|
||||
imgui_internal = getattr(imgui, "internal", None)
|
||||
|
||||
# 导入MyWorld类和必要的模块
|
||||
from core.world import CoreWorld
|
||||
from core.selection import SelectionSystem
|
||||
@ -87,12 +117,15 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
class MyWorld(PanelDelegates, CoreWorld):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._shutdown_in_progress = False
|
||||
|
||||
# 设置窗口为最大化模式
|
||||
# 启动后强制同步一次真实窗口尺寸,避免首次渲染仍停留在旧默认分辨率。
|
||||
props = WindowProperties()
|
||||
#props.set_maximized(True)
|
||||
self.win.request_properties(props)
|
||||
print("✓ 窗口已设置为最大化模式")
|
||||
self._last_forced_window_sync_size = None
|
||||
self._remaining_window_sync_attempts = 6
|
||||
self.taskMgr.doMethodLater(0.05, self._sync_window_metrics_task, "initial-window-metrics-sync")
|
||||
print(f"✓ 窗口初始化完成: {self.win.getXSize()} x {self.win.getYSize()}")
|
||||
|
||||
# Legacy compatibility fields used by scene/project modules.
|
||||
self.gui_elements = []
|
||||
@ -149,6 +182,9 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self.project_manager = ProjectManager(self)
|
||||
# print(f"[DEBUG] 项目管理系统初始化完成")
|
||||
|
||||
# Legacy manager removed in ImGui migration; keep attribute for compatibility.
|
||||
self.info_panel_manager = None
|
||||
|
||||
self.command_manager = CommandManager()
|
||||
|
||||
# 初始化碰撞管理器
|
||||
@ -412,7 +448,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self.dialog_params = {} # 存储各种对话框的参数
|
||||
|
||||
# 脚本系统状态
|
||||
self.hotReloadEnabled = False
|
||||
self.hotReloadEnabled = self.script_manager.hot_reload_enabled
|
||||
|
||||
# 初始化高度图浏览器
|
||||
self._refresh_heightmap_browser()
|
||||
@ -445,6 +481,8 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
# 滚轮事件
|
||||
self.accept('wheel_up', self._on_wheel_up)
|
||||
self.accept('wheel_down', self._on_wheel_down)
|
||||
# 监听窗口关闭,确保退出主循环并结束进程。
|
||||
self.accept('window-event', self._on_window_event)
|
||||
|
||||
self.testTexture = None
|
||||
self.icons = {} # 初始化图标字典
|
||||
@ -460,6 +498,75 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
|
||||
print("✓ MyWorld 初始化完成")
|
||||
|
||||
def _on_window_event(self, window):
|
||||
"""窗口事件处理:窗口被关闭时退出应用。"""
|
||||
if self._shutdown_in_progress:
|
||||
return
|
||||
try:
|
||||
target_window = window if window is not None else getattr(self, "win", None)
|
||||
if not target_window:
|
||||
return
|
||||
self.windowEvent(target_window)
|
||||
if not target_window.getProperties().getOpen():
|
||||
self.userExit()
|
||||
except Exception as e:
|
||||
print(f"处理窗口事件失败: {e}")
|
||||
|
||||
def _sync_window_metrics_task(self, task):
|
||||
"""Force early window-size synchronization so RP/ImGui match the real client area."""
|
||||
if self._shutdown_in_progress:
|
||||
return task.done
|
||||
|
||||
try:
|
||||
if not getattr(self, "win", None):
|
||||
return task.done
|
||||
|
||||
width = int(self.win.getXSize())
|
||||
height = int(self.win.getYSize())
|
||||
if width > 0 and height > 0:
|
||||
current_size = (width, height)
|
||||
if current_size != self._last_forced_window_sync_size:
|
||||
self._last_forced_window_sync_size = current_size
|
||||
messenger.send("window-event", [self.win])
|
||||
print(f"✓ 同步窗口分辨率: {width} x {height}")
|
||||
except Exception as e:
|
||||
print(f"同步窗口分辨率失败: {e}")
|
||||
|
||||
self._remaining_window_sync_attempts -= 1
|
||||
if self._remaining_window_sync_attempts > 0:
|
||||
task.delayTime = 0.15
|
||||
return task.again
|
||||
return task.done
|
||||
|
||||
def userExit(self):
|
||||
"""统一退出流程,避免关闭窗口后进程残留。"""
|
||||
if self._shutdown_in_progress:
|
||||
return
|
||||
self._shutdown_in_progress = True
|
||||
try:
|
||||
monitor = getattr(self, "drag_drop_monitor", None)
|
||||
if monitor:
|
||||
try:
|
||||
monitor.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
webview = getattr(self, "_imgui_webview", None)
|
||||
if webview:
|
||||
try:
|
||||
webview.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
script_manager = getattr(self, "script_manager", None)
|
||||
if script_manager:
|
||||
try:
|
||||
script_manager.stop_system()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
super().userExit()
|
||||
|
||||
# ==================== 兼容性属性 ====================
|
||||
|
||||
# 保留models属性以兼容现有代码
|
||||
@ -499,8 +606,11 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
def onFocusKeyPressed(self):
|
||||
"""处理 F 键按下事件"""
|
||||
try:
|
||||
if hasattr(self, 'selection') and self.selection.selectedNode:
|
||||
selected_node = self._get_selection_node()
|
||||
if selected_node:
|
||||
self.selection.focusCameraOnSelectedNodeAdvanced()
|
||||
elif getattr(getattr(self, "ssbo_editor", None), "has_active_selection", lambda: False)():
|
||||
return
|
||||
else:
|
||||
print("当前没有选中任何节点")
|
||||
except Exception as e:
|
||||
@ -618,7 +728,10 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
# 创建全屏DockSpace(在第一帧之后创建)
|
||||
if imgui.get_frame_count() > 0:
|
||||
viewport = imgui.get_main_viewport()
|
||||
imgui.dock_space_over_viewport(0, viewport, imgui.DockNodeFlags_.passthru_central_node)
|
||||
dock_flags = imgui.DockNodeFlags_.passthru_central_node
|
||||
if imgui_internal is not None:
|
||||
dock_flags |= imgui_internal.DockNodeFlagsPrivate_.no_window_menu_button
|
||||
imgui.dock_space_over_viewport(0, viewport, dock_flags)
|
||||
|
||||
# 在第一帧应用样式
|
||||
if imgui.get_frame_count() == 0:
|
||||
|
||||
@ -260,20 +260,12 @@ class ProjectManager:
|
||||
|
||||
# 固定的场景文件名
|
||||
scene_file = os.path.join(scenes_path, "scene.bam")
|
||||
|
||||
# 如果存在旧文件,先删除
|
||||
if os.path.exists(scene_file):
|
||||
try:
|
||||
os.remove(scene_file)
|
||||
print(f"已删除旧场景文件: {scene_file}")
|
||||
except Exception as e:
|
||||
print(f"删除旧场景文件失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 保存场景
|
||||
if self.world.scene_manager.saveScene(scene_file, project_path):
|
||||
|
||||
# 先写临时文件,成功后再替换,避免保存失败时丢失旧场景。
|
||||
if self._save_scene_atomically(scene_file, project_path):
|
||||
# 更新项目配置文件
|
||||
config_file = os.path.join(project_path, "project.json")
|
||||
project_config = self.project_config or {}
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
project_config = json.load(f)
|
||||
@ -298,6 +290,43 @@ class ProjectManager:
|
||||
except Exception as e:
|
||||
print(f"保存项目时发生错误:{str(e)}")
|
||||
return False
|
||||
|
||||
def _save_scene_atomically(self, scene_file, project_path):
|
||||
"""先保存到临时文件,成功后再原子替换正式场景文件。"""
|
||||
scene_file = os.path.normpath(scene_file)
|
||||
scene_dir = os.path.dirname(scene_file)
|
||||
scene_name, scene_ext = os.path.splitext(os.path.basename(scene_file))
|
||||
temp_scene_file = os.path.join(scene_dir, f"{scene_name}.tmp{scene_ext}")
|
||||
final_gui_info_file = scene_file.replace('.bam', '_gui.json')
|
||||
temp_gui_info_file = temp_scene_file.replace('.bam', '_gui.json')
|
||||
|
||||
for temp_path in (temp_scene_file, temp_gui_info_file):
|
||||
if os.path.exists(temp_path):
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if not self.world.scene_manager.saveScene(temp_scene_file, project_path):
|
||||
return False
|
||||
|
||||
os.replace(temp_scene_file, scene_file)
|
||||
|
||||
if os.path.exists(temp_gui_info_file):
|
||||
os.replace(temp_gui_info_file, final_gui_info_file)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"原子保存场景失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
for temp_path in (temp_scene_file, temp_gui_info_file):
|
||||
if os.path.exists(temp_path):
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# ==================== 项目打包功能 ====================
|
||||
|
||||
@ -839,4 +868,4 @@ setup(
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行打包命令失败: {str(e)}")
|
||||
return False
|
||||
return False
|
||||
|
||||
@ -12,7 +12,7 @@ from pathlib import Path
|
||||
from panda3d.core import (
|
||||
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
|
||||
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox,
|
||||
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib
|
||||
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib, ShaderAttrib
|
||||
)
|
||||
from panda3d.egg import EggData, EggVertexPool
|
||||
from direct.actor.Actor import Actor
|
||||
@ -20,6 +20,48 @@ from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
|
||||
from scene import util
|
||||
|
||||
class SceneManagerIOMixin:
|
||||
def _cleanup_untracked_render_children(self):
|
||||
"""Remove direct render children left behind by previous scene/runtime state."""
|
||||
render = getattr(self.world, "render", None)
|
||||
if not render or render.isEmpty():
|
||||
return
|
||||
|
||||
keep_exact = {
|
||||
"camera",
|
||||
"ForwardShadingCam",
|
||||
"EnvmapCamRig",
|
||||
"PSSMCameraRig",
|
||||
"PSSMDistShadowsESM",
|
||||
"PSSMSceneSunShadowCam",
|
||||
"SkyAOCaptureCam",
|
||||
"SceneRoot",
|
||||
"alight",
|
||||
"dlight",
|
||||
"ground",
|
||||
}
|
||||
keep_prefixes = (
|
||||
"ShadowCam-",
|
||||
)
|
||||
|
||||
stale_children = []
|
||||
for child in render.getChildren():
|
||||
try:
|
||||
name = child.getName()
|
||||
except Exception:
|
||||
continue
|
||||
if name in keep_exact or any(name.startswith(prefix) for prefix in keep_prefixes):
|
||||
continue
|
||||
stale_children.append(child)
|
||||
|
||||
for child in stale_children:
|
||||
try:
|
||||
child_name = child.getName()
|
||||
if not child.isEmpty():
|
||||
child.removeNode()
|
||||
print(f"清理残留场景节点: {child_name}")
|
||||
except Exception as e:
|
||||
print(f"清理残留场景节点失败: {e}")
|
||||
|
||||
def _collectGUIElementInfo(self, gui_node):
|
||||
"""收集GUI元素的信息用于保存"""
|
||||
try:
|
||||
@ -219,6 +261,45 @@ class SceneManagerIOMixin:
|
||||
all_nodes.extend(self.Spotlight)
|
||||
all_nodes.extend(self.Pointlight)
|
||||
|
||||
def expand_scene_package_wrappers(nodes):
|
||||
expanded_nodes = []
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
source_scene_model = getattr(ssbo_editor, "source_model", None) if ssbo_editor else None
|
||||
for node in nodes:
|
||||
if not node or node.isEmpty():
|
||||
continue
|
||||
|
||||
is_scene_wrapper = (
|
||||
node.hasTag("scene_import_source") and
|
||||
node.getTag("scene_import_source") == "project_scene_bam"
|
||||
)
|
||||
if not is_scene_wrapper:
|
||||
expanded_nodes.append(node)
|
||||
continue
|
||||
|
||||
source_children = []
|
||||
if source_scene_model and not source_scene_model.isEmpty():
|
||||
for child in source_scene_model.getChildren():
|
||||
try:
|
||||
if not child or child.isEmpty():
|
||||
continue
|
||||
if child.getName() in {"render", "render2d", "aspect2d"}:
|
||||
continue
|
||||
source_children.append(child)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if source_children:
|
||||
print(f"展开场景包节点 {node.getName()} -> 使用原始场景树的 {len(source_children)} 个顶层子节点")
|
||||
expanded_nodes.extend(source_children)
|
||||
continue
|
||||
|
||||
print(f"场景包节点 {node.getName()} 缺少原始场景树,回退为包装根节点保存")
|
||||
expanded_nodes.append(node)
|
||||
return expanded_nodes
|
||||
|
||||
all_nodes = expand_scene_package_wrappers(all_nodes)
|
||||
|
||||
|
||||
# 创建用于保存GUI信息的JSON文件路径
|
||||
gui_info_file = filename.replace('.bam', '_gui.json')
|
||||
@ -341,13 +422,42 @@ class SceneManagerIOMixin:
|
||||
print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本")
|
||||
|
||||
try:
|
||||
print("--- 打印当前场景图 (render) ---")
|
||||
self.world.render.ls()
|
||||
print("---------------------------------")
|
||||
unique_nodes = []
|
||||
for node in all_nodes:
|
||||
if not node or node.isEmpty():
|
||||
continue
|
||||
if any(existing == node for existing in unique_nodes):
|
||||
continue
|
||||
unique_nodes.append(node)
|
||||
|
||||
save_root = NodePath(ModelRoot("SavedSceneRoot"))
|
||||
|
||||
def has_saved_parent(node):
|
||||
parent = node.getParent()
|
||||
if not parent or parent.isEmpty() or parent == self.world.render:
|
||||
return False
|
||||
return any(candidate == parent for candidate in unique_nodes)
|
||||
|
||||
def strip_runtime_render_state(root_np):
|
||||
for current in [root_np] + list(root_np.findAllMatches("**")):
|
||||
try:
|
||||
current.clearShader()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
current.clearAttrib(ShaderAttrib.getClassType())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for node in unique_nodes:
|
||||
if has_saved_parent(node):
|
||||
continue
|
||||
clone = node.copyTo(save_root)
|
||||
strip_runtime_render_state(clone)
|
||||
|
||||
self.take_screenshot(project_path)
|
||||
# 保存场景
|
||||
success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename))
|
||||
success = save_root.writeBamFile(Filename.fromOsSpecific(filename))
|
||||
|
||||
if success:
|
||||
print(f"✓ 场景保存成功: {filename}")
|
||||
@ -357,6 +467,11 @@ class SceneManagerIOMixin:
|
||||
return success
|
||||
|
||||
finally:
|
||||
try:
|
||||
if 'save_root' in locals() and save_root and not save_root.isEmpty():
|
||||
save_root.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
# 恢复之前隐藏的节点
|
||||
for item in nodes_to_restore:
|
||||
node, was_visible = item
|
||||
@ -439,6 +554,28 @@ class SceneManagerIOMixin:
|
||||
if os.path.getsize(filename) == 0:
|
||||
# print(f"[DEBUG] 错误: 场景文件为空")
|
||||
return False
|
||||
|
||||
if retry_count == 0:
|
||||
try:
|
||||
selection = getattr(self.world, "selection", None)
|
||||
if selection:
|
||||
selection.clearSelection()
|
||||
except Exception as e:
|
||||
print(f"清理选择状态失败: {e}")
|
||||
|
||||
try:
|
||||
script_manager = getattr(self.world, "script_manager", None)
|
||||
if script_manager and hasattr(script_manager, "reset_scene_state"):
|
||||
script_manager.reset_scene_state()
|
||||
except Exception as e:
|
||||
print(f"清理脚本场景状态失败: {e}")
|
||||
|
||||
try:
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "reset_scene_state"):
|
||||
ssbo_editor.reset_scene_state()
|
||||
except Exception as e:
|
||||
print(f"清理 SSBO 场景状态失败: {e}")
|
||||
|
||||
# 预防性清理:确保Panda3D处于稳定状态
|
||||
if retry_count > 0:
|
||||
@ -526,6 +663,7 @@ class SceneManagerIOMixin:
|
||||
self.Pointlight.clear()
|
||||
# 清理可能存在的辅助节点
|
||||
self._cleanupAuxiliaryNodes()
|
||||
self._cleanup_untracked_render_children()
|
||||
|
||||
# 加载场景
|
||||
# print(f"[DEBUG] 开始加载BAM文件...")
|
||||
@ -649,7 +787,10 @@ class SceneManagerIOMixin:
|
||||
if use_ssbo_scene_import:
|
||||
try:
|
||||
print(f"[SSBO] 打开项目使用统一导入链路: {filename}")
|
||||
ssbo_scene_model = self.world._import_model_for_runtime(filename)
|
||||
ssbo_scene_model = self.world._import_model_for_runtime(
|
||||
filename,
|
||||
scene_package_import=True,
|
||||
)
|
||||
if ssbo_scene_model and not ssbo_scene_model.isEmpty():
|
||||
if ssbo_scene_model not in self.models:
|
||||
self.models.append(ssbo_scene_model)
|
||||
|
||||
@ -15,7 +15,6 @@ from panda3d.core import (
|
||||
BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib
|
||||
)
|
||||
from panda3d.egg import EggData, EggVertexPool
|
||||
from direct.actor.Actor import Actor
|
||||
from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq
|
||||
from scene import util
|
||||
from scene.tree_roles import TREE_USER_ROLE
|
||||
@ -971,58 +970,6 @@ class SceneManagerModelMixin:
|
||||
|
||||
has_animations = (character_nodes.getNumPaths() > 0 or
|
||||
anim_bundle_nodes.getNumPaths() > 0)
|
||||
|
||||
# 如果模型树中没检测到,再尝试通过 Actor 从文件路径检测
|
||||
if not has_animations:
|
||||
model_path = model_node.getTag("model_path") if model_node.hasTag("model_path") else ""
|
||||
if model_path:
|
||||
try:
|
||||
from direct.actor.Actor import Actor
|
||||
from panda3d.core import Filename
|
||||
|
||||
candidate_paths = []
|
||||
# Always prefer normalized Panda paths; avoid raw Windows path fallback,
|
||||
# which triggers noisy loader(error) logs for some absolute/Unicode paths.
|
||||
try:
|
||||
normalized = util.normalize_model_path(model_path)
|
||||
if normalized and normalized != model_path:
|
||||
candidate_paths.append(normalized)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for ctor_name in ("fromOsSpecificW", "from_os_specific_w", "fromOsSpecific", "from_os_specific"):
|
||||
ctor = getattr(Filename, ctor_name, None)
|
||||
if not ctor:
|
||||
continue
|
||||
try:
|
||||
panda_path = ctor(model_path).get_fullpath()
|
||||
if panda_path:
|
||||
candidate_paths.append(panda_path)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
seen = set()
|
||||
unique_paths = []
|
||||
for p in candidate_paths:
|
||||
if not p or p in seen:
|
||||
continue
|
||||
seen.add(p)
|
||||
unique_paths.append(p)
|
||||
|
||||
for candidate in unique_paths:
|
||||
try:
|
||||
actor = Actor(candidate)
|
||||
anim_names = actor.getAnimNames()
|
||||
actor.cleanup()
|
||||
actor.removeNode()
|
||||
if anim_names:
|
||||
print(f"通过 Actor 路径检测到动画: {candidate} -> {anim_names}")
|
||||
has_animations = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if has_animations:
|
||||
print(f"检测到模型 {model_node.getName()} 包含动画:")
|
||||
|
||||
@ -64,6 +64,8 @@ class SSBOEditor:
|
||||
self.rp = render_pipeline
|
||||
self.controller = None
|
||||
self.model = None
|
||||
self.source_model = None
|
||||
self.source_model_root = None
|
||||
self.ssbo = None
|
||||
self.font_path = font_path
|
||||
self._transform_gizmo = None
|
||||
@ -275,9 +277,11 @@ class SSBOEditor:
|
||||
except Exception as e:
|
||||
print(f'修复黑色模型材质时出错: {e}')
|
||||
|
||||
def load_model(self, model_path):
|
||||
def load_model(self, model_path, keep_source_model=False):
|
||||
"""Load and process a model using hybrid static/dynamic chunks."""
|
||||
print(f"[SSBOEditor] Loading model: {model_path}")
|
||||
self.source_model = None
|
||||
self.source_model_root = None
|
||||
source_model = None
|
||||
last_error = None
|
||||
for fn in self._build_filename_candidates(model_path):
|
||||
@ -297,7 +301,11 @@ class SSBOEditor:
|
||||
model_name = os.path.basename(model_path)
|
||||
if model_name:
|
||||
source_model.set_name(model_name)
|
||||
|
||||
|
||||
if keep_source_model:
|
||||
self.source_model_root = NodePath(f"{model_name or 'scene'}__source_snapshot_root")
|
||||
self.source_model = source_model.copyTo(self.source_model_root)
|
||||
|
||||
self.controller = ObjectController()
|
||||
count = self.controller.bake_ids_and_collect(source_model)
|
||||
self.model = self.controller.model
|
||||
@ -1219,7 +1227,100 @@ class SSBOEditor:
|
||||
|
||||
self._outline_manager.set_targets(targets)
|
||||
|
||||
def clear_selection(self):
|
||||
def _node_is_valid(self, node):
|
||||
if not node:
|
||||
return False
|
||||
try:
|
||||
return not node.is_empty()
|
||||
except Exception:
|
||||
try:
|
||||
return not node.isEmpty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def has_active_selection(self):
|
||||
return bool(self.controller and self.selected_name is not None)
|
||||
|
||||
def _is_root_selection(self):
|
||||
return bool(
|
||||
self.controller and
|
||||
self.selected_name == getattr(self.controller, "tree_root_key", None)
|
||||
)
|
||||
|
||||
def get_selection_scene_node(self):
|
||||
"""Return a stable scene node for editor features that need one."""
|
||||
if not self.controller or self.selected_name is None:
|
||||
return None
|
||||
|
||||
if self._is_root_selection():
|
||||
return self.model if self._node_is_valid(self.model) else None
|
||||
|
||||
if len(self.selected_ids) == 1:
|
||||
obj_np = self.controller.id_to_object_np.get(self.selected_ids[0])
|
||||
if self._node_is_valid(obj_np):
|
||||
return obj_np
|
||||
|
||||
return None
|
||||
|
||||
def get_selection_summary(self):
|
||||
if not self.controller or self.selected_name is None:
|
||||
return None
|
||||
return {
|
||||
"key": self.selected_name,
|
||||
"display_name": self.controller.display_names.get(self.selected_name, self.selected_name),
|
||||
"object_count": len(self.selected_ids),
|
||||
"is_root": self._is_root_selection(),
|
||||
"is_group": len(self.selected_ids) > 1 and not self._is_root_selection(),
|
||||
}
|
||||
|
||||
def _sync_editor_selection_reference(self, node):
|
||||
selection = getattr(self.base, "selection", None)
|
||||
if not selection:
|
||||
return
|
||||
selection.selectedNode = node
|
||||
selection.selectedObject = node
|
||||
|
||||
def _clear_editor_selection_visuals(self):
|
||||
selection = getattr(self.base, "selection", None)
|
||||
if not selection:
|
||||
return
|
||||
try:
|
||||
selection.clearSelectionBox()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
selection._updateSelectionOutline(None)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
selection.clearGizmo()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _find_tree_key_for_scene_node(self, node):
|
||||
if not self.controller or not self._node_is_valid(node):
|
||||
return None
|
||||
if self.model and node == self.model:
|
||||
return getattr(self.controller, "tree_root_key", None)
|
||||
for gid, obj_np in self.controller.id_to_object_np.items():
|
||||
if obj_np == node:
|
||||
return self.controller.id_to_name.get(gid)
|
||||
return None
|
||||
|
||||
def sync_scene_selection(self, node):
|
||||
"""Mirror scene-tree selection back into SSBO state, or clear stale SSBO state."""
|
||||
if not self.controller:
|
||||
return
|
||||
|
||||
target_key = self._find_tree_key_for_scene_node(node)
|
||||
if target_key:
|
||||
self.select_node(target_key, sync_world_selection=False)
|
||||
return
|
||||
|
||||
if self.has_active_selection():
|
||||
self.clear_selection(sync_world_selection=False)
|
||||
|
||||
def clear_selection(self, sync_world_selection=True):
|
||||
self._stop_pick_sync_task()
|
||||
self._reset_pick_sync_cache()
|
||||
self._cleanup_group_proxy()
|
||||
@ -1231,6 +1332,9 @@ class SSBOEditor:
|
||||
self.controller.set_active_ids([])
|
||||
if self._transform_gizmo:
|
||||
self._transform_gizmo.detach()
|
||||
if sync_world_selection:
|
||||
self._clear_editor_selection_visuals()
|
||||
self._sync_editor_selection_reference(None)
|
||||
|
||||
def on_model_deleted(self, deleted_node):
|
||||
"""Called by app deletion flow when SSBO root model is deleted."""
|
||||
@ -1241,6 +1345,39 @@ class SSBOEditor:
|
||||
self.clear_selection()
|
||||
self._sync_pick_scene_binding()
|
||||
|
||||
def reset_scene_state(self):
|
||||
"""Remove the current SSBO model/controller state before loading another scene."""
|
||||
self.clear_selection()
|
||||
self._cleanup_group_proxy()
|
||||
self._reset_pick_sync_cache()
|
||||
|
||||
controller = self.controller
|
||||
pick_model = getattr(controller, "pick_model", None) if controller else None
|
||||
model = self.model
|
||||
source_model = self.source_model
|
||||
source_model_root = self.source_model_root
|
||||
|
||||
for node in (pick_model, model, source_model, source_model_root):
|
||||
if not node:
|
||||
continue
|
||||
try:
|
||||
if not node.is_empty():
|
||||
node.remove_node()
|
||||
except Exception:
|
||||
try:
|
||||
if not node.isEmpty():
|
||||
node.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.controller = None
|
||||
self.model = None
|
||||
self.source_model = None
|
||||
self.source_model_root = None
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
self._sync_pick_scene_binding()
|
||||
|
||||
def _cleanup_group_proxy(self):
|
||||
"""Reparent objects back to their chunk and remove the group proxy."""
|
||||
proxy = getattr(self, '_group_proxy', None)
|
||||
@ -1269,7 +1406,7 @@ class SSBOEditor:
|
||||
def update_selection_mask(self):
|
||||
pass # No selection mask texture needed without custom shader
|
||||
|
||||
def select_node(self, key):
|
||||
def select_node(self, key, sync_world_selection=True):
|
||||
# Clean up previous group proxy before changing selection
|
||||
self._cleanup_group_proxy()
|
||||
self._reset_pick_sync_cache()
|
||||
@ -1280,6 +1417,9 @@ class SSBOEditor:
|
||||
self.controller and
|
||||
key == getattr(self.controller, "tree_root_key", None)
|
||||
)
|
||||
if sync_world_selection:
|
||||
self._clear_editor_selection_visuals()
|
||||
self._sync_editor_selection_reference(self.get_selection_scene_node())
|
||||
|
||||
# Root selection should stay lightweight:
|
||||
# keep static chunks active and transform the model root directly.
|
||||
@ -1315,6 +1455,12 @@ class SSBOEditor:
|
||||
# follow the gizmo transform together.
|
||||
from panda3d.core import Vec3
|
||||
proxy = self.base.render.attach_new_node("ssbo_group_proxy")
|
||||
try:
|
||||
proxy.set_name(self.controller.display_names.get(key, "ssbo_group_proxy"))
|
||||
proxy.setTag("is_ssbo_proxy", "1")
|
||||
proxy.setTag("ssbo_selection_key", str(key))
|
||||
except Exception:
|
||||
pass
|
||||
center = Vec3(0, 0, 0)
|
||||
valid = []
|
||||
for gid in self.selected_ids:
|
||||
|
||||
14
third_party/p3dimgui/backend.py
vendored
14
third_party/p3dimgui/backend.py
vendored
@ -331,9 +331,18 @@ class ImGuiBackend(DirectObject):
|
||||
self.notify.debug("__setupEvent")
|
||||
self.accept("window-event", self.__windowEvent)
|
||||
|
||||
def __refreshDisplayMetrics(self):
|
||||
if not self.window:
|
||||
return
|
||||
width = self.window.getXSize()
|
||||
height = self.window.getYSize()
|
||||
if width <= 0 or height <= 0:
|
||||
return
|
||||
self.io.display_size = (width, height)
|
||||
self.io.display_framebuffer_scale = (1.0, 1.0)
|
||||
|
||||
def __windowEvent(self, _ = None):
|
||||
if self.window:
|
||||
self.io.display_size = (self.window.getXSize(), self.window.getYSize())
|
||||
self.__refreshDisplayMetrics()
|
||||
|
||||
def __setupButton(self):
|
||||
self.notify.debug("__setupButton")
|
||||
@ -384,6 +393,7 @@ class ImGuiBackend(DirectObject):
|
||||
if self.root.isHidden():
|
||||
return task.cont
|
||||
|
||||
self.__refreshDisplayMetrics()
|
||||
self.io.delta_time = base.clock.getDt()
|
||||
if self.window:
|
||||
mouse = self.window.getPointer(0)
|
||||
|
||||
@ -905,9 +905,7 @@ class LUIManagerEditorMixin:
|
||||
self.selected_index = idx
|
||||
# Clear 3D scene selection
|
||||
if hasattr(self.world, 'selection') and self.world.selection:
|
||||
self.world.selection.selectedNode = None
|
||||
self.world.selection.clearSelectionBox()
|
||||
self.world.selection.clearGizmo()
|
||||
self.world.selection.clearSelection()
|
||||
|
||||
# Right-click menu
|
||||
if imgui.begin_popup_context_item(f"##comp_ctx_{idx}"):
|
||||
|
||||
@ -159,9 +159,14 @@ class LUIManager(LUIManagerInteractionMixin, LUIManagerEditorMixin):
|
||||
for fpath in candidate_fonts:
|
||||
if os.path.exists(fpath):
|
||||
try:
|
||||
font_default = self.world.loader.loadFont(fpath)
|
||||
font_label = self.world.loader.loadFont(fpath)
|
||||
font_header = self.world.loader.loadFont(fpath)
|
||||
panda_path = fpath
|
||||
filename_from_os = getattr(p3d.Filename, "from_os_specific", None) or getattr(p3d.Filename, "fromOsSpecific", None)
|
||||
if filename_from_os:
|
||||
panda_path = filename_from_os(fpath).get_fullpath()
|
||||
|
||||
font_default = self.world.loader.loadFont(panda_path)
|
||||
font_label = self.world.loader.loadFont(panda_path)
|
||||
font_header = self.world.loader.loadFont(panda_path)
|
||||
|
||||
if font_default and font_label and font_header:
|
||||
if hasattr(font_label, "setPixelsPerUnit"):
|
||||
@ -173,7 +178,7 @@ class LUIManager(LUIManagerInteractionMixin, LUIManagerEditorMixin):
|
||||
font_pool.register_font("default", font_default)
|
||||
font_pool.register_font("label", font_label)
|
||||
font_pool.register_font("header", font_header)
|
||||
print(f"✓ LUI 成功注册中文字体: {fpath}")
|
||||
print(f"✓ LUI 成功注册中文字体: {panda_path}")
|
||||
font_registered = True
|
||||
break
|
||||
except Exception as e:
|
||||
|
||||
@ -42,7 +42,7 @@ class AppActions:
|
||||
if hasattr(self, 'script_manager') and self.script_manager:
|
||||
try:
|
||||
current_state = getattr(self.script_manager, 'hot_reload_enabled', False)
|
||||
self.script_manager.hot_reload_enabled = not current_state
|
||||
self._set_hot_reload_enabled(not current_state)
|
||||
|
||||
new_state = "启用" if not current_state else "禁用"
|
||||
self.add_success_message(f"热重载已{new_state}")
|
||||
@ -50,6 +50,13 @@ class AppActions:
|
||||
except Exception as e:
|
||||
self.add_error_message(f"切换热重载失败: {str(e)}")
|
||||
print(f"[脚本系统] 切换热重载失败: {e}")
|
||||
|
||||
def _set_hot_reload_enabled(self, enabled):
|
||||
"""统一切换脚本热重载状态并同步 UI 状态。"""
|
||||
if not hasattr(self, 'script_manager') or not self.script_manager:
|
||||
raise RuntimeError("脚本管理器未初始化")
|
||||
self.script_manager.set_hot_reload_enabled(enabled)
|
||||
self.hotReloadEnabled = self.script_manager.hot_reload_enabled
|
||||
|
||||
|
||||
def _create_new_script(self):
|
||||
@ -168,9 +175,7 @@ class AppActions:
|
||||
|
||||
def _mount_script_to_selected(self, script_name):
|
||||
"""挂载脚本到选中对象"""
|
||||
selected_node = None
|
||||
if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'):
|
||||
selected_node = self.selection.selectedNode
|
||||
selected_node = self._get_selection_node()
|
||||
|
||||
if not selected_node or selected_node.isEmpty():
|
||||
self.add_error_message("请先选择一个对象")
|
||||
@ -193,9 +198,7 @@ class AppActions:
|
||||
|
||||
def _unmount_script_from_selected(self, script_name):
|
||||
"""从选中对象卸载脚本"""
|
||||
selected_node = None
|
||||
if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'):
|
||||
selected_node = self.selection.selectedNode
|
||||
selected_node = self._get_selection_node()
|
||||
|
||||
if not selected_node or selected_node.isEmpty():
|
||||
self.add_error_message("请先选择一个对象")
|
||||
@ -322,10 +325,8 @@ class AppActions:
|
||||
|
||||
# 2. 取消 3D 场景选择
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
if self.selection.selectedNode:
|
||||
self.selection.selectedNode = None
|
||||
self.selection.clearSelectionBox()
|
||||
self.selection.clearGizmo()
|
||||
if self._get_selection_node() or self._get_ssbo_selection_summary():
|
||||
self.selection.clearSelection()
|
||||
print("✓ 已取消场景节点选中")
|
||||
|
||||
|
||||
@ -523,7 +524,7 @@ class AppActions:
|
||||
return
|
||||
|
||||
# 获取当前选中的节点
|
||||
selected_node = self._resolve_cut_copy_node(self.selection.selectedNode)
|
||||
selected_node = self._resolve_cut_copy_node(self._get_selection_node())
|
||||
if not selected_node:
|
||||
self.add_warning_message("没有选中的节点")
|
||||
return
|
||||
@ -558,7 +559,7 @@ class AppActions:
|
||||
return
|
||||
|
||||
# 获取当前选中的节点
|
||||
selected_node = self._resolve_cut_copy_node(self.selection.selectedNode)
|
||||
selected_node = self._resolve_cut_copy_node(self._get_selection_node())
|
||||
if not selected_node:
|
||||
self.add_warning_message("没有选中的节点")
|
||||
return
|
||||
@ -606,7 +607,7 @@ class AppActions:
|
||||
# 确定粘贴目标父节点
|
||||
parent_node = None
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
selected_node = self.selection.selectedNode
|
||||
selected_node = self._get_selection_node()
|
||||
if selected_node and not selected_node.isEmpty():
|
||||
# Paste as sibling by default (not as child of selected node),
|
||||
# which matches editor expectations and avoids "pasted but invisible".
|
||||
@ -705,7 +706,7 @@ class AppActions:
|
||||
return
|
||||
|
||||
# 获取当前选中的节点
|
||||
selected_node = self.selection.selectedNode
|
||||
selected_node = self._get_selection_node()
|
||||
if not selected_node:
|
||||
self.add_warning_message("没有选中的节点")
|
||||
return
|
||||
@ -851,45 +852,9 @@ class AppActions:
|
||||
|
||||
def _save_project_impl(self):
|
||||
"""保存项目的具体实现(不依赖Qt)"""
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
|
||||
project_path = self.project_manager.current_project_path
|
||||
scenes_path = os.path.join(project_path, "scenes")
|
||||
|
||||
# 固定的场景文件名
|
||||
scene_file = os.path.join(scenes_path, "scene.bam")
|
||||
|
||||
# 如果存在旧文件,先删除
|
||||
if os.path.exists(scene_file):
|
||||
try:
|
||||
os.remove(scene_file)
|
||||
print(f"已删除旧场景文件: {scene_file}")
|
||||
except Exception as e:
|
||||
print(f"删除旧场景文件失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 保存场景
|
||||
if self.scene_manager.saveScene(scene_file, project_path):
|
||||
# 更新项目配置文件
|
||||
config_file = os.path.join(project_path, "project.json")
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
project_config = json.load(f)
|
||||
|
||||
# 更新最后修改时间
|
||||
project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# 记录场景文件路径
|
||||
project_config["scene_file"] = os.path.relpath(scene_file, project_path)
|
||||
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||||
|
||||
# 更新项目配置
|
||||
self.project_manager.project_config = project_config
|
||||
return True
|
||||
return False
|
||||
if not hasattr(self, 'project_manager') or not self.project_manager:
|
||||
return False
|
||||
return self.project_manager.saveProject()
|
||||
|
||||
|
||||
def _open_project_impl(self, project_path):
|
||||
@ -1036,7 +1001,7 @@ class AppActions:
|
||||
# ==================== 路径浏览器辅助方法 ====================
|
||||
|
||||
|
||||
def _import_model_for_runtime(self, file_path, prefer_scene_manager=False):
|
||||
def _import_model_for_runtime(self, file_path, prefer_scene_manager=False, scene_package_import=False):
|
||||
"""Import model through the active runtime path.
|
||||
SSBO mode: load via SSBOEditor always (regardless of prefer_scene_manager).
|
||||
Legacy mode: load via SceneManager.
|
||||
@ -1072,15 +1037,20 @@ class AppActions:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.ssbo_editor.load_model(file_path)
|
||||
self.ssbo_editor.load_model(
|
||||
file_path,
|
||||
keep_source_model=scene_package_import,
|
||||
)
|
||||
model_np = getattr(self.ssbo_editor, 'model', None)
|
||||
# Keep legacy ray-pick fallback usable by adding a collision body.
|
||||
if model_np:
|
||||
try:
|
||||
from scene import util as scene_util
|
||||
normalized_model_path = scene_util.normalize_model_path(file_path)
|
||||
except Exception:
|
||||
normalized_model_path = file_path
|
||||
normalized_model_path = file_path
|
||||
if not scene_package_import:
|
||||
try:
|
||||
from scene import util as scene_util
|
||||
normalized_model_path = scene_util.normalize_model_path(file_path)
|
||||
except Exception:
|
||||
normalized_model_path = file_path
|
||||
# Apply vital tags manually since SSBO overrides SceneManager loader
|
||||
model_np.setTag("model_path", normalized_model_path)
|
||||
model_np.setTag("original_path", file_path)
|
||||
@ -1093,7 +1063,12 @@ class AppActions:
|
||||
if hasattr(self, 'scene_manager') and self.scene_manager:
|
||||
try:
|
||||
self.scene_manager.setupCollision(model_np)
|
||||
self.scene_manager._processModelAnimations(model_np)
|
||||
if scene_package_import:
|
||||
model_np.setTag("has_animations", "false")
|
||||
model_np.setTag("has_animations_checked", "true")
|
||||
model_np.setTag("scene_import_source", "project_scene_bam")
|
||||
else:
|
||||
self.scene_manager._processModelAnimations(model_np)
|
||||
except Exception as e:
|
||||
print(f"[SSBO] setup components failed: {e}")
|
||||
return model_np
|
||||
|
||||
@ -34,14 +34,9 @@ class DialogPanels:
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
# 获取屏幕尺寸,居中显示对话框
|
||||
display_size = imgui.get_io().display_size
|
||||
dialog_width = 400
|
||||
dialog_height = 300
|
||||
imgui.set_next_window_size((dialog_width, dialog_height))
|
||||
imgui.set_next_window_pos(
|
||||
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
|
||||
)
|
||||
self.style_manager.prepare_centered_dialog(dialog_width, dialog_height)
|
||||
|
||||
with imgui_ctx.begin("新建项目", True, flags) as window:
|
||||
if not window.opened:
|
||||
@ -95,14 +90,9 @@ class DialogPanels:
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
# 获取屏幕尺寸,居中显示对话框
|
||||
display_size = imgui.get_io().display_size
|
||||
dialog_width = 500
|
||||
dialog_height = 400
|
||||
imgui.set_next_window_size((dialog_width, dialog_height))
|
||||
imgui.set_next_window_pos(
|
||||
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
|
||||
)
|
||||
self.style_manager.prepare_centered_dialog(dialog_width, dialog_height)
|
||||
|
||||
with imgui_ctx.begin("打开项目", True, flags) as window:
|
||||
if not window.opened:
|
||||
@ -147,14 +137,9 @@ class DialogPanels:
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
# 获取屏幕尺寸,居中显示对话框
|
||||
display_size = imgui.get_io().display_size
|
||||
dialog_width = 600
|
||||
dialog_height = 500
|
||||
imgui.set_next_window_size((dialog_width, dialog_height))
|
||||
imgui.set_next_window_pos(
|
||||
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
|
||||
)
|
||||
self.style_manager.prepare_centered_dialog(dialog_width, dialog_height)
|
||||
|
||||
with imgui_ctx.begin("选择路径", True, flags) as window:
|
||||
if not window.opened:
|
||||
@ -414,6 +399,7 @@ class DialogPanels:
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
self.style_manager.prepare_centered_dialog(420, 320)
|
||||
|
||||
with imgui_ctx.begin("创建聚光灯", self.show_spot_light_dialog, flags) as window:
|
||||
if not window:
|
||||
@ -495,6 +481,7 @@ class DialogPanels:
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
self.style_manager.prepare_centered_dialog(420, 360)
|
||||
|
||||
with imgui_ctx.begin("创建点光源", self.show_point_light_dialog, flags) as window:
|
||||
if not window:
|
||||
@ -587,6 +574,7 @@ class DialogPanels:
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
self.style_manager.prepare_centered_dialog(420, 300)
|
||||
|
||||
with imgui_ctx.begin("创建平面地形", self.show_terrain_dialog, flags) as window:
|
||||
if not window:
|
||||
@ -659,6 +647,7 @@ class DialogPanels:
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
self.style_manager.prepare_centered_dialog(420, 240)
|
||||
|
||||
with imgui_ctx.begin("创建脚本", self.show_script_dialog, flags) as window:
|
||||
if not window:
|
||||
@ -732,6 +721,7 @@ class DialogPanels:
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
self.style_manager.prepare_centered_dialog(640, 480)
|
||||
|
||||
with imgui_ctx.begin("选择脚本文件", self.show_script_browser, flags) as window:
|
||||
if not window:
|
||||
@ -880,6 +870,7 @@ class DialogPanels:
|
||||
flags = (imgui.WindowFlags_.no_resize |
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
self.style_manager.prepare_centered_dialog(640, 480)
|
||||
|
||||
with imgui_ctx.begin("选择高度图文件", self.show_heightmap_browser, flags) as window:
|
||||
if not window:
|
||||
|
||||
@ -9,8 +9,12 @@ class EditorPanelsLeftMixin:
|
||||
# 使用更少的限制性标志,允许docking
|
||||
flags = (imgui.WindowFlags_.no_collapse)
|
||||
|
||||
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags):
|
||||
self.app.showSceneTree = True # 确保窗口保持打开
|
||||
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showSceneTree = False
|
||||
return
|
||||
|
||||
self.app.showSceneTree = opened
|
||||
window_pos = imgui.get_window_pos()
|
||||
window_size = imgui.get_window_size()
|
||||
self.app._scene_tree_window_rect = (
|
||||
@ -119,9 +123,7 @@ class EditorPanelsLeftMixin:
|
||||
return
|
||||
|
||||
# 检查是否被选中
|
||||
is_selected = (hasattr(self.app, 'selection') and self.app.selection and
|
||||
hasattr(self.app.selection, 'selectedNode') and
|
||||
self.app.selection.selectedNode == node)
|
||||
is_selected = (self.app._get_selection_node() == node)
|
||||
|
||||
# 节点可见性
|
||||
is_visible = node.is_hidden() == False
|
||||
@ -279,8 +281,12 @@ class EditorPanelsLeftMixin:
|
||||
"""绘制资源管理器面板"""
|
||||
flags = self.app.style_manager.get_window_flags("panel")
|
||||
|
||||
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags):
|
||||
self.app.showResourceManager = True
|
||||
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showResourceManager = False
|
||||
return
|
||||
|
||||
self.app.showResourceManager = opened
|
||||
rm = self.app.resource_manager
|
||||
|
||||
self._update_resource_manager_window_rect(rm)
|
||||
|
||||
@ -17,8 +17,12 @@ class EditorPanelsRightMixin(
|
||||
# 使用面板类型的窗口标志,支持docking
|
||||
flags = self.app.style_manager.get_window_flags("panel")
|
||||
|
||||
with self.app.style_manager.begin_styled_window("属性面板", self.app.showPropertyPanel, flags):
|
||||
self.app.showPropertyPanel = True # 确保窗口保持打开
|
||||
with self.app.style_manager.begin_styled_window("属性面板", self.app.showPropertyPanel, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showPropertyPanel = False
|
||||
return
|
||||
|
||||
self.app.showPropertyPanel = opened
|
||||
|
||||
# --- LUI Component Properties ---
|
||||
# 优先检查 LUI 组件选择
|
||||
@ -32,35 +36,15 @@ class EditorPanelsRightMixin(
|
||||
return
|
||||
|
||||
# --- Scene Node Properties ---
|
||||
# 获取当前选中的节点
|
||||
selected_node = None
|
||||
if hasattr(self.app, 'selection') and self.app.selection and hasattr(self.app.selection, 'selectedNode'):
|
||||
selected_node = self.app.selection.selectedNode
|
||||
|
||||
# SSBO mode may select a proxy node for gizmo operations.
|
||||
# Resolve proxy back to a real scene node so property panel stays meaningful.
|
||||
try:
|
||||
if (selected_node and not selected_node.isEmpty() and
|
||||
selected_node.hasTag("is_ssbo_proxy")):
|
||||
ssbo_editor = getattr(self.app, "ssbo_editor", None)
|
||||
controller = getattr(ssbo_editor, "controller", None) if ssbo_editor else None
|
||||
if ssbo_editor and controller:
|
||||
resolved = None
|
||||
if getattr(ssbo_editor, "selected_ids", None):
|
||||
first_gid = ssbo_editor.selected_ids[0]
|
||||
key = controller.id_to_name.get(first_gid)
|
||||
if key:
|
||||
resolved = controller.key_to_node.get(key)
|
||||
if (resolved is None or resolved.isEmpty()) and getattr(ssbo_editor, "selected_name", None):
|
||||
resolved = controller.key_to_node.get(ssbo_editor.selected_name)
|
||||
if resolved and not resolved.isEmpty():
|
||||
selected_node = resolved
|
||||
except Exception:
|
||||
pass
|
||||
selected_node = self.app._get_selection_node()
|
||||
|
||||
if selected_node and not selected_node.isEmpty():
|
||||
self._draw_node_properties(selected_node)
|
||||
else:
|
||||
ssbo_summary = self.app._get_ssbo_selection_summary()
|
||||
if ssbo_summary:
|
||||
self._draw_ssbo_selection_summary(ssbo_summary)
|
||||
return
|
||||
# 无选中对象时显示提示(模仿Qt版本的空状态样式)
|
||||
imgui.spacing()
|
||||
imgui.spacing()
|
||||
@ -91,6 +75,25 @@ class EditorPanelsRightMixin(
|
||||
imgui.bullet_text("使用 F 键快速聚焦到选中对象")
|
||||
imgui.bullet_text("使用 Delete 键删除选中对象")
|
||||
|
||||
def _draw_ssbo_selection_summary(self, summary):
|
||||
"""Render a safe summary for SSBO group selections without exposing wrong node properties."""
|
||||
imgui.spacing()
|
||||
imgui.text("SSBO 选择摘要")
|
||||
imgui.separator()
|
||||
imgui.text(f"名称: {summary.get('display_name') or '未命名'}")
|
||||
imgui.text(f"对象数量: {summary.get('object_count', 0)}")
|
||||
if summary.get("is_root"):
|
||||
imgui.text_colored((0.5, 0.8, 1.0, 1.0), "当前选中的是 SSBO 根模型")
|
||||
elif summary.get("is_group"):
|
||||
imgui.text_colored((1.0, 0.8, 0.3, 1.0), "当前选中的是 SSBO 组合节点")
|
||||
imgui.text_wrapped("这个选择对应多个动态对象。为避免误改错误节点,这里先显示摘要信息。")
|
||||
imgui.separator()
|
||||
imgui.text("建议:")
|
||||
imgui.bullet_text("在左侧展开到叶子节点后查看单对象属性")
|
||||
imgui.bullet_text("继续使用场景中的 Gizmo 做组合移动")
|
||||
else:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "当前 SSBO 选择没有可直接映射的单个场景节点")
|
||||
|
||||
def _draw_node_properties(self, node):
|
||||
"""绘制节点属性"""
|
||||
if not node or node.isEmpty():
|
||||
@ -222,9 +225,6 @@ class EditorPanelsRightMixin(
|
||||
# 动画徽章(优化检测逻辑,避免重复创建Actor)
|
||||
has_animation = False
|
||||
if node_type == "模型": # 只对模型类型进行动画检测
|
||||
model_path = node.getTag("model_path") if node.hasTag("model_path") else ""
|
||||
likely_anim_format = bool(model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx', '.bam', '.egg')))
|
||||
|
||||
# 优先使用场景标签(导入/加载时会写入)
|
||||
if node.hasTag("has_animations"):
|
||||
has_animation = node.getTag("has_animations").lower() == "true"
|
||||
@ -241,21 +241,11 @@ class EditorPanelsRightMixin(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 最后才尝试 Actor 检测(只缓存“有动画”,避免把失败结果永久缓存)
|
||||
# 只读取已有缓存,避免属性面板在普通模型上触发高噪音 Actor 探测
|
||||
cached_result = node.getPythonTag('animation')
|
||||
if cached_result is True:
|
||||
has_animation = True
|
||||
elif not has_animation and likely_anim_format:
|
||||
try:
|
||||
actor = self._getActor(node)
|
||||
if actor and actor.getAnimNames():
|
||||
has_animation = True
|
||||
node.setTag("has_animations", "true")
|
||||
node.setPythonTag('animation', True)
|
||||
print(f"[动画检测] {node.getName()}: 有动画")
|
||||
except Exception as e:
|
||||
print(f"动画检测失败: {e}")
|
||||
elif cached_result is False and not likely_anim_format:
|
||||
elif cached_result is False:
|
||||
has_animation = False
|
||||
else:
|
||||
# 对于非模型类型,检查已有的动画标签
|
||||
@ -605,14 +595,30 @@ class EditorPanelsRightMixin(
|
||||
anim_node.setPythonTag("cached_anim_info", None)
|
||||
anim_node.setPythonTag("cached_processed_names", None)
|
||||
anim_node.setPythonTag("animation", None)
|
||||
|
||||
should_force_probe = False
|
||||
has_cached_animation = anim_node.getPythonTag("animation") is True
|
||||
if not (has_animation_tag or has_animation_nodes or has_cached_animation):
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型未检测到动画结构")
|
||||
if imgui.button("尝试强制检测##force_detect_animation"):
|
||||
should_force_probe = True
|
||||
anim_node.setPythonTag("cached_anim_info", None)
|
||||
anim_node.setPythonTag("cached_processed_names", None)
|
||||
else:
|
||||
return
|
||||
|
||||
# 只有在没有缓存时才进行完整的动画检测和处理
|
||||
if cached_anim_info is None or cached_processed_names is None:
|
||||
# 获取Actor
|
||||
actor = self._getActor(anim_node)
|
||||
actor = self._getActor(anim_node) if (should_force_probe or has_animation_tag or has_animation_nodes or has_cached_animation) else None
|
||||
if not actor:
|
||||
if has_animation_tag or has_animation_nodes:
|
||||
imgui.text_colored((1.0, 0.7, 0.3, 1.0), "检测到动画结构,但当前未成功绑定Actor")
|
||||
elif should_force_probe:
|
||||
imgui.text_colored((0.9, 0.6, 0.3, 1.0), "强制检测未发现可播放动画")
|
||||
anim_node.setPythonTag("animation", False)
|
||||
anim_node.setPythonTag("cached_processed_names", [])
|
||||
anim_node.setPythonTag("cached_anim_info", "无动画")
|
||||
else:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画")
|
||||
return
|
||||
|
||||
@ -112,6 +112,9 @@ class EditorPanelsTopMixin:
|
||||
# 脚本子菜单
|
||||
with imgui_ctx.begin_menu("脚本") as script_menu:
|
||||
if script_menu:
|
||||
hot_reload_enabled = False
|
||||
if getattr(self.app, "script_manager", None):
|
||||
hot_reload_enabled = bool(self.app.script_manager.hot_reload_enabled)
|
||||
if imgui.menu_item("创建脚本...", "", False, True)[1]:
|
||||
self.app._on_create_script()
|
||||
if imgui.menu_item("加载脚本文件...", "", False, True)[1]:
|
||||
@ -119,7 +122,9 @@ class EditorPanelsTopMixin:
|
||||
imgui.separator()
|
||||
if imgui.menu_item("重载所有脚本", "", False, True)[1]:
|
||||
self.app._on_reload_all_scripts()
|
||||
_, self.app.hotReloadEnabled = imgui.menu_item("启用热重载", "", self.app.hotReloadEnabled, True)
|
||||
changed, new_hot_reload = imgui.menu_item("启用热重载", "", hot_reload_enabled, True)
|
||||
if changed:
|
||||
self.app._set_hot_reload_enabled(new_hot_reload)
|
||||
if imgui.menu_item("脚本管理器", "", False, True)[1]:
|
||||
self.app._on_open_scripts_manager()
|
||||
|
||||
@ -238,8 +243,12 @@ class EditorPanelsTopMixin:
|
||||
# 工具栏可以保持无标题栏,但允许移动和调整大小
|
||||
flags = self.app.style_manager.get_window_flags("toolbar")
|
||||
|
||||
with self.app.style_manager.begin_styled_window("工具栏", self.app.showToolbar, flags):
|
||||
self.app.showToolbar = True # 确保窗口保持打开
|
||||
with self.app.style_manager.begin_styled_window("工具栏", self.app.showToolbar, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showToolbar = False
|
||||
return
|
||||
|
||||
self.app.showToolbar = opened
|
||||
|
||||
# 选择工具按钮
|
||||
select_active = self.app.tool_manager.isSelectionTool()
|
||||
|
||||
@ -125,7 +125,8 @@ class InteractionPanels:
|
||||
|
||||
# 清除选择
|
||||
if hasattr(self, 'selection') and self.selection:
|
||||
if self.selection.selectedNode == node:
|
||||
current_node = self.selection.getSelectedNode() if hasattr(self.selection, "getSelectedNode") else self.selection.selectedNode
|
||||
if current_node == node:
|
||||
self.selection.clearSelection()
|
||||
|
||||
# 添加成功消息
|
||||
|
||||
@ -2,6 +2,39 @@
|
||||
|
||||
|
||||
class PanelDelegates:
|
||||
def _node_is_valid(self, node):
|
||||
if not node:
|
||||
return False
|
||||
try:
|
||||
return not node.is_empty()
|
||||
except Exception:
|
||||
try:
|
||||
return not node.isEmpty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _get_selection_node(self):
|
||||
"""Return the current editor selection, preferring the active SSBO state."""
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
|
||||
ssbo_node = ssbo_editor.get_selection_scene_node()
|
||||
return ssbo_node if self._node_is_valid(ssbo_node) else None
|
||||
|
||||
selection = getattr(self, "selection", None)
|
||||
if selection and hasattr(selection, "getSelectedNode"):
|
||||
node = selection.getSelectedNode()
|
||||
else:
|
||||
node = getattr(selection, "selectedNode", None) if selection else None
|
||||
return node if self._node_is_valid(node) else None
|
||||
|
||||
def _get_ssbo_selection_summary(self):
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if not ssbo_editor or not hasattr(ssbo_editor, "has_active_selection"):
|
||||
return None
|
||||
if not ssbo_editor.has_active_selection():
|
||||
return None
|
||||
return ssbo_editor.get_selection_summary()
|
||||
|
||||
def _draw_menu_bar(self):
|
||||
self.editor_panels.draw_menu_bar()
|
||||
|
||||
@ -319,6 +352,9 @@ class PanelDelegates:
|
||||
def _toggle_hot_reload(self, *args, **kwargs):
|
||||
return self.app_actions._toggle_hot_reload(*args, **kwargs)
|
||||
|
||||
def _set_hot_reload_enabled(self, *args, **kwargs):
|
||||
return self.app_actions._set_hot_reload_enabled(*args, **kwargs)
|
||||
|
||||
def _create_new_script(self, *args, **kwargs):
|
||||
return self.app_actions._create_new_script(*args, **kwargs)
|
||||
|
||||
|
||||
@ -1427,14 +1427,9 @@ class PropertyHelpers:
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
# 获取屏幕尺寸,居中显示对话框
|
||||
display_size = imgui.get_io().display_size
|
||||
dialog_width = 300
|
||||
dialog_height = 400
|
||||
imgui.set_next_window_size((dialog_width, dialog_height))
|
||||
imgui.set_next_window_pos(
|
||||
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
|
||||
)
|
||||
self.style_manager.prepare_centered_dialog(dialog_width, dialog_height)
|
||||
|
||||
with imgui_ctx.begin("颜色选择器", True, flags) as window:
|
||||
if not window.opened:
|
||||
@ -1620,14 +1615,9 @@ class PropertyHelpers:
|
||||
imgui.WindowFlags_.no_collapse |
|
||||
imgui.WindowFlags_.modal)
|
||||
|
||||
# 获取屏幕尺寸,居中显示对话框
|
||||
display_size = imgui.get_io().display_size
|
||||
dialog_width = 400
|
||||
dialog_height = 500
|
||||
imgui.set_next_window_size((dialog_width, dialog_height))
|
||||
imgui.set_next_window_pos(
|
||||
((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
|
||||
)
|
||||
self.style_manager.prepare_centered_dialog(dialog_width, dialog_height)
|
||||
|
||||
with imgui_ctx.begin("字体选择器", True, flags) as window:
|
||||
if not window.opened:
|
||||
|
||||
@ -22,8 +22,12 @@ class ScriptPanels:
|
||||
# 使用面板类型的窗口标志,支持docking
|
||||
flags = self.style_manager.get_window_flags("panel")
|
||||
|
||||
with self.style_manager.begin_styled_window("控制台", self.app.showConsole, flags):
|
||||
self.app.showConsole = True # 确保窗口保持打开
|
||||
with self.style_manager.begin_styled_window("控制台", self.app.showConsole, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showConsole = False
|
||||
return
|
||||
|
||||
self.app.showConsole = opened
|
||||
|
||||
imgui.text("控制台输出")
|
||||
imgui.separator()
|
||||
@ -94,8 +98,12 @@ class ScriptPanels:
|
||||
# 使用面板类型的窗口标志,支持docking
|
||||
flags = self.style_manager.get_window_flags("panel")
|
||||
|
||||
with self.style_manager.begin_styled_window("脚本管理", self.app.showScriptPanel, flags):
|
||||
self.app.showScriptPanel = True # 确保窗口保持打开
|
||||
with self.style_manager.begin_styled_window("脚本管理", self.app.showScriptPanel, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showScriptPanel = False
|
||||
return
|
||||
|
||||
self.app.showScriptPanel = opened
|
||||
|
||||
# 1. 脚本系统状态组
|
||||
self._draw_script_status_group()
|
||||
@ -225,9 +233,7 @@ class ScriptPanels:
|
||||
"""绘制脚本挂载组"""
|
||||
if imgui.collapsing_header("脚本挂载"):
|
||||
# 显示当前选中对象
|
||||
selected_node = None
|
||||
if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'):
|
||||
selected_node = self.selection.selectedNode
|
||||
selected_node = self._get_selection_node()
|
||||
|
||||
if selected_node and not selected_node.isEmpty():
|
||||
imgui.text("选中对象:")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user