imgui优化

This commit is contained in:
ayuan9957 2026-03-12 15:13:05 +08:00
parent c5dbc6be6f
commit 780536203e
25 changed files with 959 additions and 339 deletions

BIN
Resources/models/box1.glb Normal file

Binary file not shown.

View File

@ -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)
# 减少高亮调试输出,只在需要时输出

View File

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

View File

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

View File

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

View File

@ -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 == "选择":

View File

@ -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}")

View File

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

@ -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.11LUI 可能不可用。")
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.11LUI 可能不可用。")
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:

View File

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

View File

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

View File

@ -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()} 包含动画:")

View File

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

View File

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

View File

@ -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}"):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
# 添加成功消息

View File

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

View File

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

View File

@ -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("选中对象:")