1150 lines
44 KiB
Python
1150 lines
44 KiB
Python
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import warnings
|
||
from pathlib import Path
|
||
|
||
|
||
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 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(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
|
||
from math import pi, sin, cos
|
||
|
||
from direct.showbase.ShowBase import ShowBase
|
||
from direct.task import Task
|
||
from direct.actor.Actor import Actor
|
||
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
|
||
from core.event_handler import EventHandler
|
||
from core.tool_manager import ToolManager
|
||
from core.script_system import ScriptManager
|
||
from core.Command_System import CommandManager
|
||
from core.terrain_manager import TerrainManager
|
||
from scene.scene_manager import SceneManager
|
||
from project.project_manager import ProjectManager
|
||
from core.collision_manager import CollisionManager
|
||
from core.CustomMouseController import CustomMouseController
|
||
from core.resource_manager import ResourceManager
|
||
from ui.lui_manager import LUIManager
|
||
from ui.panels.editor_panels import EditorPanels
|
||
from ui.panels.script_panels import ScriptPanels
|
||
from ui.panels.dialog_panels import DialogPanels
|
||
from ui.panels.interaction_panels import InteractionPanels
|
||
from ui.panels.create_actions import CreateActions
|
||
from ui.panels.runtime_actions import RuntimeActions
|
||
from ui.panels.object_factory import ObjectFactory
|
||
from ui.panels.animation_tools import AnimationTools
|
||
from ui.panels.property_helpers import PropertyHelpers
|
||
from ui.panels.app_actions import AppActions
|
||
from ui.panels.panel_delegates import PanelDelegates
|
||
from core.model_drag_drop import ModelDragDropService
|
||
from ssbo_component.ssbo_editor import SSBOEditor
|
||
from TransformGizmo.transform_gizmo import TransformGizmo
|
||
|
||
try:
|
||
# 尝试导入视频管理器,避免循环导入
|
||
import importlib.util
|
||
spec = importlib.util.spec_from_file_location("video_integration", "demo/video_integration.py")
|
||
video_module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(video_module)
|
||
VideoManager = video_module.VideoManager
|
||
except:
|
||
# 如果video_integration模块不存在,则跳过
|
||
VideoManager = None
|
||
print("⚠ 视频管理器模块未找到,视频功能将不可用")
|
||
|
||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||
|
||
class MyWorld(PanelDelegates, CoreWorld):
|
||
PANEL_VISIBILITY_ATTRS = {
|
||
"scene_tree": "showSceneTree",
|
||
"property": "showPropertyPanel",
|
||
"console": "showConsole",
|
||
"script": "showScriptPanel",
|
||
"toolbar": "showToolbar",
|
||
"resources": "showResourceManager",
|
||
"web": "showWebPanel",
|
||
"lui_editor": "showLUIEditor",
|
||
}
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self._shutdown_in_progress = False
|
||
|
||
# 启动后强制同步一次真实窗口尺寸,避免首次渲染仍停留在旧默认分辨率。
|
||
props = WindowProperties()
|
||
self.win.request_properties(props)
|
||
self._last_forced_window_sync_size = None
|
||
self._remaining_window_sync_attempts = 6
|
||
self.taskMgr.doMethodLater(0.05, self._sync_window_metrics_task, "window-metrics-sync")
|
||
print(f"✓ 窗口初始化完成: {self.win.getXSize()} x {self.win.getYSize()}")
|
||
|
||
self.gui_elements = []
|
||
|
||
# 初始化选择和变换系统
|
||
self.selection = SelectionSystem(self)
|
||
|
||
# 绑定F键用于聚焦选中节点
|
||
self.accept("f", self.onFocusKeyPressed)
|
||
self.accept("F", self.onFocusKeyPressed) # 大写F
|
||
|
||
self.use_ssbo_mouse_picking = True
|
||
self.use_ssbo_scene_import = True
|
||
if not self.use_ssbo_mouse_picking:
|
||
self.accept("mouse1", self.onMouseClick)
|
||
# Keep release/move bindings even in SSBO mode so gizmo drag can work.
|
||
self.accept("mouse1-up", self.onMouseRelease)
|
||
self.accept("mouse-move", self.onMouseMove)
|
||
self.accept("drag", self.onMouseMove)
|
||
|
||
# 初始化事件处理系统
|
||
self.event_handler = EventHandler(self)
|
||
|
||
# 初始化工具管理系统
|
||
self.tool_manager = ToolManager(self)
|
||
|
||
# 初始化脚本管理系统
|
||
self.script_manager = ScriptManager(self)
|
||
|
||
# 初始化LUI管理系统
|
||
self.lui_manager = LUIManager(self)
|
||
|
||
# 新的坐标系
|
||
self.newTransform = TransformGizmo(self)
|
||
self._setup_transform_gizmo_light_sync()
|
||
|
||
# 初始化视频管理
|
||
if VideoManager is not None:
|
||
self.video_manager = VideoManager(self)
|
||
else:
|
||
self.video_manager = None
|
||
|
||
# 初始化场景管理系统
|
||
# print(f"[DEBUG] 初始化场景管理系统...")
|
||
self.scene_manager = SceneManager(self)
|
||
# print(f"[DEBUG] 场景管理系统初始化完成")
|
||
|
||
# 初始化项目管理系统
|
||
# print(f"[DEBUG] 初始化项目管理系统...")
|
||
self.project_manager = ProjectManager(self)
|
||
# print(f"[DEBUG] 项目管理系统初始化完成")
|
||
|
||
self.info_panel_manager = None
|
||
|
||
self.command_manager = CommandManager()
|
||
|
||
# 初始化碰撞管理器
|
||
self.collision_manager = CollisionManager(self)
|
||
|
||
# 初始化资源管理器
|
||
self.resource_manager = ResourceManager(self)
|
||
|
||
# 初始化Actor缓存系统(用于动画控制)
|
||
self._actor_cache = {}
|
||
print("✓ Actor缓存系统初始化完成")
|
||
|
||
# 初始化自定义鼠标控制器(视角移动)
|
||
self.mouse_controller = CustomMouseController(self)
|
||
self.mouse_controller.setUp(mouse_speed=25, move_speed=20)
|
||
print("✓ 自定义鼠标控制器初始化完成")
|
||
|
||
# 初始化VR管理器
|
||
try:
|
||
from core.vr import VRManager
|
||
self.vr_manager = VRManager(self)
|
||
print("✓ VR管理器初始化完成")
|
||
except Exception as e:
|
||
print(f"⚠ VR管理器初始化失败: {e}")
|
||
self.vr_manager = None
|
||
|
||
# VR调试相关变量
|
||
self.vr_debug_enabled = False
|
||
self.vr_detailed_mode = True
|
||
self.vr_performance_monitor = False
|
||
|
||
# 调试选项
|
||
self.debug_collision = True # 是否显示碰撞体
|
||
|
||
# 默认启用模型间碰撞检测(可选)
|
||
if self.use_ssbo_mouse_picking:
|
||
self.enableModelCollisionDetection(enable=False, frequency=0.1, threshold=0.5)
|
||
else:
|
||
self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
|
||
|
||
# 碰撞检测UI相关变量
|
||
self._selected_collision_shape = "球形 (Sphere)" # 默认选择的碰撞形状
|
||
|
||
# 启动脚本系统
|
||
self.script_manager.start_system()
|
||
|
||
self.terrain_manager = TerrainManager(self)
|
||
|
||
self.terrain_edit_radius = 3.0
|
||
self.terrain_edit_strength=0.3
|
||
self.terrain_edit_operation = "add"
|
||
|
||
# Install Dear ImGui
|
||
p3dimgui.init(
|
||
wantPlaceManager=False,
|
||
wantExplorerManager=False,
|
||
wantTimeSliderManager=False,
|
||
)
|
||
self._imgui_ini_path = PROJECT_ROOT / "imgui.ini"
|
||
self._imgui_default_layout_path = PROJECT_ROOT / "config" / "default_imgui_layout.ini"
|
||
imgui.get_io().set_ini_filename(self._imgui_ini_path.as_posix())
|
||
# Let ssbo_component reuse the existing imgui backend instance.
|
||
self.imgui_backend = self.imgui
|
||
# Initialize SSBO editor and let it own mouse1 picking.
|
||
# Do not auto-load a default model here; models are loaded from import flow.
|
||
self.ssbo_editor = None
|
||
if self.use_ssbo_mouse_picking:
|
||
self.ssbo_editor = SSBOEditor(
|
||
base_app=self,
|
||
render_pipeline=self.render_pipeline,
|
||
model_path=None,
|
||
font_path=None,
|
||
)
|
||
self.ssbo_editor.bind_transform_gizmo(self.newTransform)
|
||
print("SSBOEditor mouse picking enabled (waiting for imported model)")
|
||
|
||
imgui.get_io().config_flags |= imgui.ConfigFlags_.docking_enable
|
||
print("✓ ImGui Docking功能已启用")
|
||
|
||
# 初始化样式管理器
|
||
from core.imgui_style_manager import ImGuiStyleManager
|
||
self.style_manager = ImGuiStyleManager(self.imgui, self)
|
||
self.editor_panels = EditorPanels(self)
|
||
self.script_panels = ScriptPanels(self)
|
||
self.dialog_panels = DialogPanels(self)
|
||
self.interaction_panels = InteractionPanels(self)
|
||
self.create_actions = CreateActions(self)
|
||
self.runtime_actions = RuntimeActions(self)
|
||
self.object_factory = ObjectFactory(self)
|
||
self.animation_tools = AnimationTools(self)
|
||
self.property_helpers = PropertyHelpers(self)
|
||
self.app_actions = AppActions(self)
|
||
self._ensure_default_imgui_layout()
|
||
|
||
# 简化的初始化字体设置(只使用中文字体)
|
||
try:
|
||
# 先清除默认字体
|
||
self.imgui.io.fonts.clear()
|
||
|
||
# 尝试加载中文字体
|
||
import platform
|
||
system = platform.system().lower()
|
||
if system == "linux":
|
||
font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
|
||
elif system == "windows":
|
||
font_path = "C:/Windows/Fonts/msyh.ttc"
|
||
elif system == "darwin":
|
||
font_path = "/System/Library/Fonts/PingFang.ttc"
|
||
else:
|
||
font_path = None
|
||
|
||
if font_path and Path(font_path).exists():
|
||
self.imgui.io.fonts.add_font_from_file_ttf(font_path, 14.0)
|
||
print(f"✓ 初始化中文字体成功: {font_path}")
|
||
else:
|
||
# 回退到原来的字体
|
||
fallback_font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
|
||
if fallback_font_path and Path(fallback_font_path).exists():
|
||
self.imgui.io.fonts.add_font_from_file_ttf(fallback_font_path, 14.0)
|
||
print(f"✓ 初始化回退字体成功: {fallback_font_path}")
|
||
else:
|
||
self.imgui.io.fonts.add_font_default()
|
||
print("✓ 初始化使用默认字体")
|
||
|
||
except Exception as e:
|
||
print(f"⚠ 初始化字体系统失败: {e}")
|
||
# 备用方案:使用默认字体
|
||
try:
|
||
self.imgui.io.fonts.add_font_default()
|
||
print("✓ 使用备用默认字体")
|
||
except:
|
||
pass
|
||
|
||
# Disable the camera trackball controls.
|
||
self.disableMouse()
|
||
|
||
self.showDemoWindow = False
|
||
|
||
# UI状态管理
|
||
self._default_panel_visibility = {
|
||
"scene_tree": True,
|
||
"property": True,
|
||
"console": True,
|
||
"script": not self.use_ssbo_mouse_picking,
|
||
"toolbar": True,
|
||
"resources": True,
|
||
"web": False,
|
||
"lui_editor": not self.use_ssbo_mouse_picking,
|
||
}
|
||
for attr_name in self.PANEL_VISIBILITY_ATTRS.values():
|
||
setattr(self, attr_name, False)
|
||
self.reset_panel_visibility_to_defaults()
|
||
self.webPanelUrl = "https://www.baidu.com"
|
||
|
||
# 脚本系统状态变量
|
||
self.hotReloadEnabled = True
|
||
self._new_script_name = "new_script"
|
||
self._selected_template = 0
|
||
self._mount_script_index = 0
|
||
self.console_command_input = ""
|
||
self._scene_tree_search_text = ""
|
||
self._resource_path_input = str(self.resource_manager.current_path)
|
||
self._resource_path_source = self._resource_path_input
|
||
|
||
# 变换监控相关
|
||
self._transform_monitoring = False
|
||
self._monitored_node = None
|
||
self._last_transform_values = {}
|
||
self._transform_update_timer = 0
|
||
self._transform_update_interval = 0.05 # 50ms检查一次
|
||
self._clipboard_pos = None # 位置剪贴板
|
||
self._transform_clipboard = {}
|
||
self._transform_scale_locked = True
|
||
|
||
# 颜色选择器相关
|
||
self._color_picker_active = False
|
||
self._color_picker_target = None # (target_object, property_name)
|
||
self._color_picker_current_color = (1.0, 1.0, 1.0, 1.0)
|
||
self._color_picker_callback = None
|
||
|
||
# 字体选择器相关
|
||
self._font_selector_active = False
|
||
self._font_selector_target = None # (target_object, property_name)
|
||
self._font_selector_current_font = ""
|
||
self._font_selector_search_text = ""
|
||
self._font_selector_callback = None
|
||
self._available_fonts = [] # 可用字体列表
|
||
self._refresh_available_fonts()
|
||
|
||
# 菜单状态管理
|
||
self.show_new_project_dialog = False
|
||
self.show_open_project_dialog = False
|
||
self.show_save_as_dialog = False
|
||
self.show_about_dialog = False
|
||
self.save_as_project_name = ""
|
||
self.save_as_project_path = ""
|
||
|
||
# 路径选择对话框状态
|
||
self.show_path_browser = False
|
||
self.path_browser_mode = "" # "new_project" 或 "open_project"
|
||
self.path_browser_current_path = os.getcwd()
|
||
self.path_browser_selected_path = ""
|
||
self.path_browser_items = []
|
||
|
||
# 快捷键映射
|
||
self.shortcut_keys = {
|
||
'Ctrl+N': self._on_new_project,
|
||
'Ctrl+O': self._on_open_project,
|
||
'Ctrl+S': self._on_save_project,
|
||
'Alt+F4': self._on_exit
|
||
}
|
||
|
||
# 键盘状态跟踪
|
||
self.ctrl_pressed = False
|
||
self.alt_pressed = False
|
||
|
||
# 消息系统
|
||
self.messages = []
|
||
self.max_messages = 5 # 最多显示5条消息
|
||
|
||
# 剪切板系统
|
||
self.clipboard = []
|
||
self.clipboard_mode = "" # "copy" 或 "cut"
|
||
|
||
# 视角控制状态
|
||
self.camera_control_enabled = True
|
||
self.show_camera_info = False
|
||
|
||
# 拖拽导入状态
|
||
self.dragged_files = []
|
||
self.is_dragging = False
|
||
self.show_drag_overlay = False
|
||
self.drag_drop_monitor = None
|
||
self._resource_manager_window_rect = None
|
||
self._resource_drop_targets = []
|
||
self._scene_tree_window_rect = None
|
||
self._property_panel_window_rect = None
|
||
self._script_panel_window_rect = None
|
||
self._console_window_rect = None
|
||
self._toolbar_window_rect = None
|
||
self._drag_scene_tree_hover_node = None
|
||
self.model_drag_drop = ModelDragDropService(self)
|
||
# 导入功能状态
|
||
self.show_import_dialog = False
|
||
self.import_file_path = ""
|
||
self.supported_formats = [".gltf", ".glb", ".fbx", ".bam", ".egg", ".obj"]
|
||
|
||
# 创建功能状态
|
||
self.show_3d_text_dialog = False
|
||
self.show_3d_image_dialog = False
|
||
self.show_gui_button_dialog = False
|
||
self.show_gui_label_dialog = False
|
||
self.show_gui_entry_dialog = False
|
||
self.show_gui_image_dialog = False
|
||
self.show_video_screen_dialog = False
|
||
self.show_2d_video_screen_dialog = False
|
||
self.show_spherical_video_dialog = False
|
||
self.show_virtual_screen_dialog = False
|
||
self.show_spot_light_dialog = False
|
||
self.show_point_light_dialog = False
|
||
self.show_terrain_dialog = False
|
||
self.show_heightmap_browser = False
|
||
self.show_script_dialog = False
|
||
self.show_script_browser = False
|
||
|
||
# 高度图浏览器状态
|
||
self.heightmap_browser_current_path = os.getcwd()
|
||
self.heightmap_browser_selected_path = ""
|
||
self.heightmap_browser_items = []
|
||
self.heightmap_file_path = ""
|
||
self.supported_heightmap_formats = [".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".tif"]
|
||
|
||
# 脚本浏览器状态
|
||
self.script_browser_current_path = os.getcwd()
|
||
self.script_browser_selected_path = ""
|
||
self.script_browser_items = []
|
||
|
||
# 对话框参数存储
|
||
self.dialog_params = {} # 存储各种对话框的参数
|
||
|
||
# 脚本系统状态
|
||
self.hotReloadEnabled = self.script_manager.hot_reload_enabled
|
||
|
||
# 初始化高度图浏览器
|
||
self._refresh_heightmap_browser()
|
||
|
||
# 初始化脚本浏览器
|
||
self._refresh_script_browser()
|
||
|
||
self.accept('imgui-new-frame', self.__newFrame)
|
||
self.accept('`', self.__toggleImgui)
|
||
|
||
# 添加键盘事件监听用于快捷键
|
||
self.accept('control', self._on_ctrl_pressed)
|
||
self.accept('control-up', self._on_ctrl_released)
|
||
self.accept('alt', self._on_alt_pressed)
|
||
self.accept('alt-up', self._on_alt_released)
|
||
self.accept('n', self._on_n_pressed)
|
||
self.accept('o', self._on_o_pressed)
|
||
self.accept('control-s', self._on_save_project)
|
||
self.accept('f4', self._on_f4_pressed)
|
||
|
||
# 编辑功能快捷键
|
||
self.accept('control-z', self._on_undo)
|
||
self.accept('control-y', self._on_redo)
|
||
self.accept('control-x', self._on_cut)
|
||
self.accept('control-c', self._on_copy)
|
||
self.accept('control-v', self._on_paste)
|
||
self.accept('delete', self._on_delete_pressed)
|
||
self.accept('escape', self._on_escape_pressed)
|
||
|
||
# 滚轮事件
|
||
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 = {} # 初始化图标字典
|
||
|
||
# 添加初始化消息
|
||
self.add_success_message("MyWorld 初始化完成")
|
||
self.add_info_message("ImGui菜单系统已就绪")
|
||
self.add_info_message("快捷键已启用 (Ctrl+N, Ctrl+O, Ctrl+S, Alt+F4)")
|
||
|
||
# 启用拖拽导入功能
|
||
self.setup_drag_drop_support()
|
||
self.add_info_message("拖拽导入功能已启用 - 可将3D文件拖拽到窗口中导入")
|
||
|
||
print("✓ MyWorld 初始化完成")
|
||
|
||
def is_panel_visible(self, panel_key):
|
||
attr_name = self.PANEL_VISIBILITY_ATTRS.get(panel_key)
|
||
if not attr_name:
|
||
return False
|
||
return bool(getattr(self, attr_name, False))
|
||
|
||
def set_panel_visible(self, panel_key, visible):
|
||
attr_name = self.PANEL_VISIBILITY_ATTRS.get(panel_key)
|
||
if not attr_name:
|
||
return
|
||
|
||
visible = bool(visible)
|
||
previous = bool(getattr(self, attr_name, False))
|
||
setattr(self, attr_name, visible)
|
||
|
||
if panel_key == "web" and previous != visible:
|
||
if not visible:
|
||
try:
|
||
self.editor_panels._stop_imgui_webview()
|
||
except Exception:
|
||
pass
|
||
|
||
if panel_key == "lui_editor" and hasattr(self, "lui_manager"):
|
||
try:
|
||
self.lui_manager.show_editor = visible
|
||
except Exception:
|
||
pass
|
||
|
||
def reset_panel_visibility_to_defaults(self):
|
||
for panel_key, default_visible in self._default_panel_visibility.items():
|
||
self.set_panel_visible(panel_key, default_visible)
|
||
|
||
def _imgui_layout_looks_valid(self, ini_text):
|
||
"""Conservatively check whether imgui.ini already contains a usable dock layout."""
|
||
if not ini_text or "[Docking][Data]" not in ini_text or "DockSpace" not in ini_text:
|
||
return False
|
||
return True
|
||
|
||
def _ensure_default_imgui_layout(self):
|
||
"""Seed a default dock layout when imgui.ini is missing or obviously incomplete."""
|
||
template_path = getattr(self, "_imgui_default_layout_path", None)
|
||
ini_path = getattr(self, "_imgui_ini_path", None)
|
||
if not template_path or not ini_path or not template_path.exists():
|
||
return
|
||
|
||
current_text = ""
|
||
if ini_path.exists():
|
||
try:
|
||
current_text = ini_path.read_text(encoding="utf-8")
|
||
except UnicodeDecodeError:
|
||
current_text = ini_path.read_text(encoding="utf-8", errors="ignore")
|
||
except Exception as e:
|
||
print(f"⚠ 读取 ImGui 布局文件失败: {e}")
|
||
|
||
if self._imgui_layout_looks_valid(current_text):
|
||
return
|
||
|
||
try:
|
||
template_text = template_path.read_text(encoding="utf-8")
|
||
except Exception as e:
|
||
print(f"⚠ 读取默认 ImGui 布局模板失败: {e}")
|
||
return
|
||
|
||
if not self._imgui_layout_looks_valid(template_text):
|
||
print("⚠ 默认 ImGui 布局模板不完整,跳过自动回填")
|
||
return
|
||
|
||
try:
|
||
if ini_path.exists() and current_text.strip():
|
||
backup_path = ini_path.with_suffix(".invalid.bak")
|
||
if not backup_path.exists():
|
||
shutil.copyfile(ini_path, backup_path)
|
||
print(f"ℹ 已备份无效 ImGui 布局: {backup_path}")
|
||
|
||
imgui.load_ini_settings_from_memory(template_text)
|
||
ini_path.write_text(template_text, encoding="utf-8")
|
||
print(f"✓ 已加载默认 ImGui 布局: {template_path}")
|
||
except Exception as e:
|
||
print(f"⚠ 应用默认 ImGui 布局失败: {e}")
|
||
|
||
def _reset_imgui_layout(self):
|
||
"""Force-reset the current ImGui dock layout from the default template."""
|
||
template_path = getattr(self, "_imgui_default_layout_path", None)
|
||
ini_path = getattr(self, "_imgui_ini_path", None)
|
||
if not template_path or not ini_path or not template_path.exists():
|
||
self.add_error_message("默认 ImGui 布局模板不存在")
|
||
return
|
||
|
||
try:
|
||
template_text = template_path.read_text(encoding="utf-8")
|
||
imgui.load_ini_settings_from_memory(template_text)
|
||
ini_path.write_text(template_text, encoding="utf-8")
|
||
self.reset_panel_visibility_to_defaults()
|
||
self.add_success_message("ImGui 布局已重置为默认布局")
|
||
except Exception as e:
|
||
self.add_error_message(f"重置 ImGui 布局失败: {e}")
|
||
|
||
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)
|
||
self._sync_window_dependent_state(target_window)
|
||
if not target_window.getProperties().getOpen():
|
||
self.userExit()
|
||
except Exception as e:
|
||
print(f"处理窗口事件失败: {e}")
|
||
|
||
def _sync_window_dependent_state(self, window):
|
||
"""Keep camera and picking lenses aligned with the current client size."""
|
||
try:
|
||
width = int(window.getXSize())
|
||
height = int(window.getYSize())
|
||
if width <= 0 or height <= 0:
|
||
return
|
||
|
||
render_pipeline = getattr(self, "render_pipeline", None)
|
||
if render_pipeline:
|
||
try:
|
||
pipeline_window_dims = getattr(render_pipeline, "_last_window_dims", None)
|
||
pipeline_needs_resize = (
|
||
pipeline_window_dims is None
|
||
or int(pipeline_window_dims.x) != width
|
||
or int(pipeline_window_dims.y) != height
|
||
)
|
||
if pipeline_needs_resize and hasattr(render_pipeline, "_handle_window_event"):
|
||
render_pipeline._handle_window_event(window)
|
||
except Exception as e:
|
||
print(f"同步 RenderPipeline 窗口尺寸失败: {e}")
|
||
|
||
aspect_ratio = float(width) / float(height)
|
||
|
||
lens = getattr(self, "camLens", None)
|
||
if lens:
|
||
try:
|
||
lens.setAspectRatio(aspect_ratio)
|
||
except Exception:
|
||
pass
|
||
|
||
cam = getattr(self, "cam", None)
|
||
if cam:
|
||
try:
|
||
cam_lens = cam.node().getLens()
|
||
if cam_lens:
|
||
cam_lens.setAspectRatio(aspect_ratio)
|
||
except Exception:
|
||
pass
|
||
|
||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||
if ssbo_editor and getattr(ssbo_editor, "pick_lens", None):
|
||
try:
|
||
ssbo_editor.pick_lens.setAspectRatio(aspect_ratio)
|
||
except Exception:
|
||
pass
|
||
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}")
|
||
else:
|
||
self._sync_window_dependent_state(self.win)
|
||
except Exception as e:
|
||
print(f"同步窗口分辨率失败: {e}")
|
||
|
||
if self._remaining_window_sync_attempts > 0:
|
||
self._remaining_window_sync_attempts -= 1
|
||
task.delayTime = 0.15
|
||
else:
|
||
task.delayTime = 0.05
|
||
return task.again
|
||
|
||
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属性以兼容现有代码
|
||
@property
|
||
def models(self):
|
||
"""模型列表的兼容性属性"""
|
||
return self.scene_manager.models
|
||
|
||
@models.setter
|
||
def models(self, value):
|
||
"""模型列表的兼容性设置器"""
|
||
self.scene_manager.models = value
|
||
|
||
|
||
# 保留currentTool属性以兼容现有代码
|
||
@property
|
||
def currentTool(self):
|
||
"""当前工具的兼容性属性"""
|
||
return self.tool_manager.currentTool
|
||
|
||
@currentTool.setter
|
||
def currentTool(self, value):
|
||
"""当前工具的兼容性设置器"""
|
||
self.tool_manager.currentTool = value
|
||
|
||
# 保留terrains属性以兼容现有代码
|
||
@property
|
||
def terrains(self):
|
||
"""地形列表的兼容性属性"""
|
||
return self.terrain_manager.terrains
|
||
|
||
@terrains.setter
|
||
def terrains(self,value):
|
||
"""地形列表的兼容性设置器"""
|
||
self.terrain_manager.terrains = value
|
||
|
||
def onFocusKeyPressed(self):
|
||
"""处理 F 键按下事件"""
|
||
try:
|
||
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:
|
||
print(f"处理 F 键事件失败: {e}")
|
||
|
||
def onMouseClick(self):
|
||
"""处理鼠标点击事件"""
|
||
print("\n=== 鼠标点击事件触发 ===")
|
||
try:
|
||
# 检查鼠标是否有效
|
||
if not self.mouseWatcherNode.hasMouse():
|
||
print("❌ 鼠标无效或不在窗口内")
|
||
return
|
||
|
||
print("✓ 鼠标位置有效")
|
||
|
||
# 获取鼠标位置
|
||
mouse_x = self.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.mouseWatcherNode.getMouseY()
|
||
print(f"📍 鼠标标准化坐标: ({mouse_x:.3f}, {mouse_y:.3f})")
|
||
|
||
# 转换为窗口坐标
|
||
winWidth, winHeight = self.win.getSize()
|
||
window_x = (mouse_x + 1) * 0.5 * winWidth
|
||
window_y = (1 - mouse_y) * 0.5 * winHeight
|
||
print(f"📍 鼠标窗口坐标: ({window_x:.1f}, {window_y:.1f})")
|
||
print(f"📐 窗口尺寸: {winWidth} x {winHeight}")
|
||
|
||
# 检查ImGui是否捕获了鼠标
|
||
imgui_captured = self.processImGuiMouseClick(window_x, window_y)
|
||
print(f"🖱️ ImGui捕获状态: {imgui_captured}")
|
||
if imgui_captured:
|
||
print("❌ ImGui处理了该事件,跳过3D场景选择")
|
||
return
|
||
|
||
# 调用事件处理器进行射线检测和选择
|
||
if hasattr(self, 'event_handler'):
|
||
print("✓ 找到事件处理器,开始处理选择")
|
||
self.event_handler.mousePressEventLeft({
|
||
'x': window_x,
|
||
'y': window_y
|
||
})
|
||
else:
|
||
print("❌ 未找到事件处理器")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 处理鼠标点击事件失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def onMouseRelease(self):
|
||
"""处理鼠标释放事件"""
|
||
try:
|
||
# 检查鼠标是否有效
|
||
if not self.mouseWatcherNode.hasMouse():
|
||
return
|
||
|
||
# 获取鼠标位置
|
||
mouse_x = self.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.mouseWatcherNode.getMouseY()
|
||
|
||
# 转换为窗口坐标
|
||
winWidth, winHeight = self.win.getSize()
|
||
window_x = (mouse_x + 1) * 0.5 * winWidth
|
||
window_y = (1 - mouse_y) * 0.5 * winHeight
|
||
|
||
# 调用事件处理器
|
||
if hasattr(self, 'event_handler'):
|
||
self.event_handler.mouseReleaseEventLeft({
|
||
'x': window_x,
|
||
'y': window_y
|
||
})
|
||
|
||
except Exception as e:
|
||
print(f"处理鼠标释放事件失败: {e}")
|
||
|
||
def onMouseMove(self):
|
||
"""处理鼠标移动事件"""
|
||
try:
|
||
# 检查鼠标是否有效
|
||
if not self.mouseWatcherNode.hasMouse():
|
||
return
|
||
|
||
# 获取鼠标位置
|
||
mouse_x = self.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.mouseWatcherNode.getMouseY()
|
||
|
||
# 转换为窗口坐标
|
||
winWidth, winHeight = self.win.getSize()
|
||
window_x = (mouse_x + 1) * 0.5 * winWidth
|
||
window_y = (1 - mouse_y) * 0.5 * winHeight
|
||
|
||
# 调用事件处理器
|
||
if hasattr(self, 'event_handler'):
|
||
self.event_handler.mouseMoveEvent({
|
||
'x': window_x,
|
||
'y': window_y
|
||
})
|
||
|
||
except Exception as e:
|
||
print(f"处理鼠标移动事件失败: {e}")
|
||
|
||
|
||
def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
|
||
"""启用模型间碰撞检测"""
|
||
return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold)
|
||
|
||
def __toggleImgui(self):
|
||
if not self.imgui.isKeyboardCaptured():
|
||
self.imgui.toggle()
|
||
|
||
def __newFrame(self):
|
||
# Dear ImGui commands can be placed here.
|
||
|
||
# 创建全屏DockSpace(在第一帧之后创建)
|
||
if imgui.get_frame_count() > 0:
|
||
viewport = imgui.get_main_viewport()
|
||
dock_flags = imgui.DockNodeFlags_.passthru_central_node
|
||
if imgui_internal is not None:
|
||
# imgui_bundle不同版本下私有Dock标志位的枚举类型可能不兼容
|
||
# (直接位或会在Python 3.11的Flag枚举里触发异常)
|
||
try:
|
||
private_flag = imgui_internal.DockNodeFlagsPrivate_.no_window_menu_button
|
||
dock_flags = imgui.DockNodeFlags_(int(dock_flags) | int(private_flag))
|
||
except Exception:
|
||
# 回退:忽略私有标志,不影响DockSpace主流程
|
||
pass
|
||
imgui.dock_space_over_viewport(0, viewport, dock_flags)
|
||
|
||
# 在第一帧应用样式
|
||
if imgui.get_frame_count() == 0:
|
||
self.style_manager.apply_style()
|
||
# 加载图标
|
||
self.icons = {
|
||
'select': self.style_manager.load_icon('select_tool'),
|
||
'move': self.style_manager.load_icon('move_tool'),
|
||
'rotate': self.style_manager.load_icon('rotate_tool'),
|
||
'scale': self.style_manager.load_icon('scale_tool'),
|
||
'success': self.style_manager.load_icon('success_icon'),
|
||
'warning': self.style_manager.load_icon('warning_icon'),
|
||
'property_select_image': self.style_manager.load_icon('property_select_image'),
|
||
'delete_fail_icon': self.style_manager.load_icon('delete_fail_icon'),
|
||
}
|
||
|
||
# 简化字体加载(只使用中文字体)
|
||
try:
|
||
# 清除现有字体
|
||
self.imgui.io.fonts.clear()
|
||
|
||
# 尝试加载中文字体
|
||
import platform
|
||
from pathlib import Path
|
||
|
||
system = platform.system().lower()
|
||
if system == "linux":
|
||
font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
|
||
elif system == "windows":
|
||
font_path = "C:/Windows/Fonts/msyh.ttc"
|
||
elif system == "darwin":
|
||
font_path = "/System/Library/Fonts/PingFang.ttc"
|
||
else:
|
||
font_path = None
|
||
|
||
if font_path and Path(font_path).exists():
|
||
self.imgui.io.fonts.add_font_from_file_ttf(font_path, 14.0)
|
||
print(f"✓ 中文字体加载成功: {font_path}")
|
||
else:
|
||
# 回退到原来的字体
|
||
fallback_font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
|
||
if fallback_font_path and Path(fallback_font_path).exists():
|
||
self.imgui.io.fonts.add_font_from_file_ttf(fallback_font_path, 14.0)
|
||
print(f"✓ 回退字体加载成功: {fallback_font_path}")
|
||
else:
|
||
self.imgui.io.fonts.add_font_default()
|
||
print("✓ 使用默认字体")
|
||
|
||
except Exception as e:
|
||
print(f"⚠ 字体初始化失败: {e}")
|
||
try:
|
||
self.imgui.io.fonts.add_font_default()
|
||
print("✓ 使用备用默认字体")
|
||
except:
|
||
pass
|
||
|
||
# 获取窗口尺寸
|
||
display_size = imgui.get_io().display_size
|
||
window_width = display_size.x
|
||
window_height = display_size.y
|
||
|
||
# 每帧重置窗口命中区域,由各面板绘制时重新填充
|
||
self._resource_manager_window_rect = None
|
||
self._resource_drop_targets = []
|
||
self._scene_tree_window_rect = None
|
||
self._property_panel_window_rect = None
|
||
self._script_panel_window_rect = None
|
||
self._console_window_rect = None
|
||
self._toolbar_window_rect = None
|
||
self._drag_scene_tree_hover_node = None
|
||
|
||
# 绘制菜单栏
|
||
self._draw_menu_bar()
|
||
|
||
# 绘制停靠布局
|
||
self._draw_docked_layout(window_width, window_height)
|
||
|
||
# 处理系统层外部拖入
|
||
self._process_external_drop_events()
|
||
|
||
# 绘制纹理测试窗口
|
||
if self.testTexture:
|
||
with self.style_manager.begin_styled_window("Texture Test", True,
|
||
imgui.WindowFlags_.always_auto_resize) as (_, windowOpen):
|
||
if not windowOpen:
|
||
self.imgui.removeTexture(self.testTexture)
|
||
self.testTexture = None
|
||
return
|
||
imgui.image(self.testTexture, (256, 256))
|
||
|
||
# 绘制ImGui演示窗口
|
||
if self.showDemoWindow:
|
||
imgui.show_demo_window()
|
||
|
||
# 绘制对话框
|
||
self._draw_new_project_dialog()
|
||
self._draw_open_project_dialog()
|
||
self._draw_save_as_project_dialog()
|
||
self._draw_path_browser()
|
||
self._draw_import_dialog()
|
||
|
||
# 绘制颜色选择器
|
||
self._draw_color_picker()
|
||
|
||
# 绘制字体选择器
|
||
self._draw_font_selector()
|
||
|
||
self._draw_spot_light_dialog()
|
||
self._draw_point_light_dialog()
|
||
self._draw_terrain_dialog()
|
||
self._draw_heightmap_browser()
|
||
self._draw_script_dialog()
|
||
|
||
# 绘制纹理选择对话框
|
||
self._draw_texture_file_dialog()
|
||
self._draw_script_browser()
|
||
self._draw_about_dialog()
|
||
|
||
# 绘制右键菜单
|
||
self._draw_context_menus()
|
||
|
||
# 绘制拖拽界面
|
||
self._draw_drag_drop_interface()
|
||
|
||
# 绘制LUI编辑器
|
||
if self.is_panel_visible("lui_editor") and hasattr(self, 'lui_manager'):
|
||
self.lui_manager.draw_editor()
|
||
|
||
# 更新变换监控
|
||
dt = imgui.get_io().delta_time
|
||
self.update_transform_monitoring(dt)
|
||
|
||
def _draw_docked_layout(self, window_width, window_height):
|
||
"""绘制可停靠的布局(支持拖拽)"""
|
||
# 左侧场景树面板
|
||
if self.is_panel_visible("scene_tree"):
|
||
self._draw_scene_tree()
|
||
|
||
# 资源管理器面板
|
||
if self.is_panel_visible("resources"):
|
||
self._draw_resource_manager()
|
||
|
||
# 属性面板
|
||
if self.is_panel_visible("property"):
|
||
self._draw_property_panel()
|
||
|
||
# Web面板
|
||
if self.is_panel_visible("web"):
|
||
self._draw_web_panel()
|
||
|
||
# 脚本面板
|
||
if self.is_panel_visible("script"):
|
||
self._draw_script_panel()
|
||
|
||
# 底部控制台
|
||
if self.is_panel_visible("console"):
|
||
self._draw_console()
|
||
|
||
# 顶部工具栏
|
||
if self.is_panel_visible("toolbar"):
|
||
self._draw_toolbar()
|
||
|
||
def _sync_rp_light_from_node(self, node):
|
||
"""将灯光包装节点的位置同步到 RenderPipeline 灯光对象。"""
|
||
try:
|
||
if not node or node.isEmpty() or (not node.hasPythonTag("rp_light_object")):
|
||
# 兼容旧场景:仅有 light_type 标签时尝试补绑 rp_light_object
|
||
if not node or node.isEmpty() or (not node.hasTag("light_type")):
|
||
return False
|
||
scene_manager = getattr(self, "scene_manager", None)
|
||
if scene_manager:
|
||
try:
|
||
light_type = node.getTag("light_type")
|
||
if light_type == "spot_light" and hasattr(scene_manager, "_recreateSpotLight"):
|
||
scene_manager._recreateSpotLight(node)
|
||
elif light_type == "point_light" and hasattr(scene_manager, "_recreatePointLight"):
|
||
scene_manager._recreatePointLight(node)
|
||
except Exception:
|
||
pass
|
||
light_obj = node.getPythonTag("rp_light_object") if node.hasPythonTag("rp_light_object") else None
|
||
if not light_obj:
|
||
return False
|
||
|
||
world_pos = node.getPos(self.render)
|
||
try:
|
||
light_obj.setPos(world_pos)
|
||
except Exception:
|
||
try:
|
||
light_obj.setPos(world_pos.x, world_pos.y, world_pos.z)
|
||
except Exception:
|
||
try:
|
||
light_obj.pos = Point3(world_pos)
|
||
except Exception:
|
||
return False
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def _on_transform_gizmo_drag_event(self, payload):
|
||
"""TransformGizmo 拖拽事件回调:实时同步灯光位置。"""
|
||
try:
|
||
node = payload.get("target") if isinstance(payload, dict) else None
|
||
if node and (not node.isEmpty()):
|
||
self._sync_rp_light_from_node(node)
|
||
except Exception:
|
||
pass
|
||
|
||
def _setup_transform_gizmo_light_sync(self):
|
||
"""为 newTransform 注册灯光同步事件钩子。"""
|
||
tg = getattr(self, "newTransform", None)
|
||
if not tg:
|
||
return
|
||
try:
|
||
from TransformGizmo.events import GizmoEvent
|
||
hooks = {
|
||
"move": {
|
||
GizmoEvent.DRAG_MOVE: [self._on_transform_gizmo_drag_event],
|
||
GizmoEvent.DRAG_END: [self._on_transform_gizmo_drag_event],
|
||
},
|
||
"rotate": {
|
||
GizmoEvent.DRAG_MOVE: [self._on_transform_gizmo_drag_event],
|
||
GizmoEvent.DRAG_END: [self._on_transform_gizmo_drag_event],
|
||
},
|
||
"scale": {
|
||
GizmoEvent.DRAG_MOVE: [self._on_transform_gizmo_drag_event],
|
||
GizmoEvent.DRAG_END: [self._on_transform_gizmo_drag_event],
|
||
},
|
||
}
|
||
tg.set_event_hooks(hooks, replace=False)
|
||
except Exception as e:
|
||
print(f"绑定 TransformGizmo 灯光同步事件失败: {e}")
|
||
|
||
if __name__ == "__main__":
|
||
demo = MyWorld()
|
||
demo.run()
|