458 lines
18 KiB
Python
458 lines
18 KiB
Python
import os
|
||
|
||
import panda3d.core as p3d
|
||
from imgui_bundle import imgui, imgui_ctx
|
||
|
||
from ui.LUI.lui_manager_editor import LUIManagerEditorMixin
|
||
from ui.LUI.lui_manager_interaction import LUIManagerInteractionMixin
|
||
try:
|
||
from ui.LUI.lui_world_space import WorldSpaceLUIRuntime
|
||
except Exception as exc:
|
||
WorldSpaceLUIRuntime = None
|
||
LUI_WORLD_SPACE_IMPORT_ERROR = exc
|
||
else:
|
||
LUI_WORLD_SPACE_IMPORT_ERROR = None
|
||
from ui.LUI.lui_shared import (
|
||
CardMaker,
|
||
LUIButton,
|
||
LUIDefaultSkin,
|
||
LUIFrame,
|
||
LUIInputField,
|
||
LUIInputHandler,
|
||
LUILabel,
|
||
LUIObject,
|
||
LUIRegion,
|
||
LUISlider,
|
||
LUISprite,
|
||
LUI_IMPORT_DETAILS,
|
||
NodePath,
|
||
Path,
|
||
lui,
|
||
)
|
||
from ui.lui_function import luiFunction
|
||
|
||
|
||
class LUIManager(LUIManagerInteractionMixin, LUIManagerEditorMixin):
|
||
"""Manager for LUI system integration"""
|
||
def __init__(self, world):
|
||
self.world = world
|
||
self.lui_enabled = (lui is not None)
|
||
# Always initialize selection fields, even when LUI is unavailable.
|
||
self.selected_index = -1
|
||
self._last_selected_index = -1
|
||
|
||
self.luiFunction = luiFunction
|
||
|
||
# Editor State
|
||
self.show_editor = True
|
||
self.play_mode = False
|
||
self._selected_type_index = 0
|
||
|
||
if not self.lui_enabled:
|
||
details = str(LUI_IMPORT_DETAILS or "").strip()
|
||
if details:
|
||
print(f"LUI is not available, LUIManager will be disabled. {details}")
|
||
else:
|
||
print("LUI is not available, LUIManager will be disabled.")
|
||
return
|
||
|
||
# 1. 优先注册中文字体
|
||
self._register_chinese_font()
|
||
|
||
# 2. 初始化核心 LUI 区域
|
||
self._init_ui_regions()
|
||
|
||
# 3. 加载皮肤资源
|
||
self.lui_skin = LUIDefaultSkin()
|
||
self.lui_skin.load()
|
||
|
||
# 3.1 皮肤会覆盖 default/label/header 字体,这里再次注册中文字体
|
||
# 以避免 LUIText 输出中文时出现 “Font does not support character ...”
|
||
self._register_chinese_font()
|
||
|
||
# 4. 状态管理 - 与engine保持一致
|
||
self.canvases = []
|
||
self.current_canvas_index = -1
|
||
self.canvas_counter = 0
|
||
self.components = []
|
||
self.root_order = []
|
||
self._tree_drag_src = None
|
||
self.selected_index = -1
|
||
self._last_selected_index = -1
|
||
|
||
# 拖拽和选择状态
|
||
self.dragging_comp = None
|
||
self.pending_drag_comp = None
|
||
self.pending_drag_index = -1
|
||
self.pending_drag_start_mouse = None
|
||
self.pending_drag_start_abs = None
|
||
self.pending_drag_start_parent = None
|
||
self.dragging_index = -1
|
||
self.drag_start_threshold = 6.0
|
||
self.drag_offset = (0, 0)
|
||
|
||
# 锚点功能相关变量
|
||
self.anchor_popup_open = False
|
||
self.pending_child_creation = False
|
||
self.pending_parent_index = -1
|
||
|
||
# 子父对齐功能
|
||
self.child_parent_alignment_enabled = True
|
||
|
||
# Resize handles 相关变量
|
||
self.resize_handles = []
|
||
self.selection_box = []
|
||
self.debug_outlines = {} # comp_index -> [border sprites]
|
||
self.resizing_handle = None
|
||
self.resize_start_pos = (0, 0)
|
||
self.resize_start_bounds = {}
|
||
self.min_size = 20
|
||
|
||
# 使用外部字典管理状态,避免修改 C++ 对象属性导致 AttributeError
|
||
self.drag_states = {}
|
||
self._last_interaction_frame = -1 # 记录最后一次交互的帧数,用于防止事件穿透
|
||
self._input_enabled = True
|
||
self.debug_world_lui = str(os.environ.get("EG_LUI_WORLD_DEBUG", "1")).lower() not in ("0", "false", "off", "no")
|
||
self._debug_lui_visibility_state = {}
|
||
self.world_space_runtime = None
|
||
self._init_world_space_runtime()
|
||
|
||
# 创建resize handles和selection box(延迟创建,确保在Canvas之后)
|
||
if hasattr(self.world, 'taskMgr'):
|
||
self.world.taskMgr.doMethodLater(0.1, self._delayed_create_resize_handles, "delayed_create_resize_handles")
|
||
else:
|
||
self._create_resize_handles()
|
||
|
||
# 添加任务来处理拖动和resize handles更新
|
||
if hasattr(self.world, 'taskMgr'):
|
||
self.world.taskMgr.add(self._update_drag, "update_lui_drag")
|
||
self.world.taskMgr.add(self._update_resize_handles, "update_resize_handles")
|
||
self.world.taskMgr.add(self._update_component_outlines, "update_lui_outlines")
|
||
self.world.taskMgr.add(self._update_http_components_task, "update_http_components")
|
||
|
||
print("✓ LUIManager initialized with complete functionality")
|
||
|
||
def _init_world_space_runtime(self):
|
||
"""Attach the optional Panda3D scene-space LUI mirror."""
|
||
if WorldSpaceLUIRuntime is None:
|
||
print(f"⚠ LUI 3D 场景运行时不可用: {LUI_WORLD_SPACE_IMPORT_ERROR}")
|
||
return
|
||
try:
|
||
self.world_space_runtime = WorldSpaceLUIRuntime(self)
|
||
print("✓ LUI 3D 场景运行时已启用")
|
||
except Exception as exc:
|
||
self.world_space_runtime = None
|
||
print(f"⚠ LUI 3D 场景运行时初始化失败: {exc}")
|
||
|
||
def _canvas_render_mode(self, canvas_data):
|
||
mode = str((canvas_data or {}).get("render_mode") or "screen").lower().strip()
|
||
return "world" if mode == "world" else "screen"
|
||
|
||
def _debug_world_lui_enabled(self):
|
||
return bool(getattr(self, "debug_world_lui", False))
|
||
|
||
def _debug_lui_parent_label(self, obj):
|
||
if obj is None:
|
||
return "None"
|
||
try:
|
||
get_name = getattr(obj, "getName", None) or getattr(obj, "get_name", None)
|
||
if callable(get_name):
|
||
name = get_name()
|
||
if name:
|
||
return f"{obj.__class__.__name__}:{name}"
|
||
except Exception:
|
||
pass
|
||
try:
|
||
name = getattr(obj, "name", None)
|
||
if name:
|
||
return f"{obj.__class__.__name__}:{name}"
|
||
except Exception:
|
||
pass
|
||
return f"{obj.__class__.__name__}@{id(obj):x}"
|
||
|
||
def _debug_lui_visible_value(self, obj):
|
||
if obj is None:
|
||
return None
|
||
try:
|
||
return getattr(obj, "visible", None)
|
||
except Exception:
|
||
return "?"
|
||
|
||
def _debug_lui_screen_visibility(self, canvas_data, requested_visible):
|
||
if not self._debug_world_lui_enabled():
|
||
return
|
||
panel = (canvas_data or {}).get("panel")
|
||
panel_parent = getattr(panel, "parent", None)
|
||
parent_state = "None" if panel_parent is None else "parented"
|
||
state = (
|
||
bool(requested_visible),
|
||
parent_state,
|
||
self._debug_lui_visible_value(panel),
|
||
self._debug_lui_visible_value((canvas_data or {}).get("background")),
|
||
self._debug_lui_visible_value((canvas_data or {}).get("title_label")),
|
||
self._canvas_render_mode(canvas_data),
|
||
)
|
||
key = id(canvas_data)
|
||
if self._debug_lui_visibility_state.get(key) == state:
|
||
return
|
||
self._debug_lui_visibility_state[key] = state
|
||
print(
|
||
"[LUI3D Debug] screen overlay visibility "
|
||
f"canvas='{(canvas_data or {}).get('name')}' "
|
||
f"mode={state[5]} requested={state[0]} "
|
||
f"panel_parent={self._debug_lui_parent_label(panel_parent)} "
|
||
f"panel_parent_state={state[1]} panel_visible={state[2]} "
|
||
f"bg_visible={state[3]} title_visible={state[4]}"
|
||
)
|
||
|
||
def _set_lui_object_visible_deep(self, obj, visible, visited=None):
|
||
if obj is None:
|
||
return
|
||
if visited is None:
|
||
visited = set()
|
||
obj_id = id(obj)
|
||
if obj_id in visited:
|
||
return
|
||
visited.add(obj_id)
|
||
|
||
try:
|
||
if visible and hasattr(obj, "show"):
|
||
obj.show()
|
||
elif (not visible) and hasattr(obj, "hide"):
|
||
obj.hide()
|
||
elif hasattr(obj, "visible"):
|
||
obj.visible = bool(visible)
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
if hasattr(obj, "visible"):
|
||
obj.visible = bool(visible)
|
||
except Exception:
|
||
pass
|
||
|
||
child_attrs = (
|
||
"_layout", "_label", "_text", "_shadow_text", "_knob",
|
||
"_slider_bg", "_slider_fill", "_sprite_left", "_sprite_mid",
|
||
"_sprite_right", "content_node", "root_layout", "header_bar",
|
||
"main_frame", "label", "text_label", "sprite", "widget",
|
||
"layout_obj", "object",
|
||
)
|
||
for attr_name in child_attrs:
|
||
try:
|
||
child = getattr(obj, attr_name, None)
|
||
except Exception:
|
||
child = None
|
||
if child is not None:
|
||
self._set_lui_object_visible_deep(child, visible, visited)
|
||
|
||
try:
|
||
obj_dict = getattr(obj, "__dict__", {}) or {}
|
||
except Exception:
|
||
obj_dict = {}
|
||
for value in obj_dict.values():
|
||
if value is obj:
|
||
continue
|
||
if isinstance(value, dict):
|
||
for nested in value.values():
|
||
self._set_lui_object_visible_deep(nested, visible, visited)
|
||
elif isinstance(value, (list, tuple, set)):
|
||
for nested in value:
|
||
self._set_lui_object_visible_deep(nested, visible, visited)
|
||
elif value is not None and value.__class__.__name__.lower().startswith("lui"):
|
||
self._set_lui_object_visible_deep(value, visible, visited)
|
||
|
||
def _set_lui_canvas_screen_visible(self, canvas_data, visible):
|
||
panel = (canvas_data or {}).get("panel")
|
||
if panel is None:
|
||
return
|
||
visible = bool(visible)
|
||
try:
|
||
if visible:
|
||
parent = canvas_data.get("_screen_parent")
|
||
if parent is None:
|
||
parent = getattr(self, "overlay_root", None)
|
||
if parent is not None and getattr(panel, "parent", None) is None:
|
||
panel.parent = parent
|
||
else:
|
||
current_parent = getattr(panel, "parent", None)
|
||
if current_parent is not None:
|
||
canvas_data["_screen_parent"] = current_parent
|
||
panel.parent = None
|
||
self._set_lui_object_visible_deep(panel, visible)
|
||
self._set_lui_object_visible_deep(canvas_data.get("background"), visible)
|
||
self._set_lui_object_visible_deep(canvas_data.get("title_label"), visible)
|
||
except Exception:
|
||
pass
|
||
self._debug_lui_screen_visibility(canvas_data, visible)
|
||
|
||
def _set_lui_component_screen_visible(self, comp_data, visible):
|
||
comp_obj = (comp_data or {}).get("object")
|
||
if comp_obj is None:
|
||
return
|
||
visible = bool(visible)
|
||
try:
|
||
if visible:
|
||
parent = comp_data.get("_screen_parent")
|
||
if parent is None:
|
||
canvas_index = comp_data.get("canvas_index")
|
||
if canvas_index is not None and 0 <= int(canvas_index) < len(getattr(self, "canvases", [])):
|
||
parent = self.canvases[int(canvas_index)].get("panel")
|
||
if parent is not None and getattr(comp_obj, "parent", None) is None:
|
||
comp_obj.parent = parent
|
||
else:
|
||
current_parent = getattr(comp_obj, "parent", None)
|
||
if current_parent is not None:
|
||
comp_data["_screen_parent"] = current_parent
|
||
comp_obj.parent = None
|
||
except Exception:
|
||
pass
|
||
|
||
self._set_lui_object_visible_deep(comp_obj, visible)
|
||
for key in (
|
||
"widget", "sprite", "text_label", "layout_obj",
|
||
"keep_alive", "keep_alive_node",
|
||
):
|
||
self._set_lui_object_visible_deep(comp_data.get(key), visible)
|
||
|
||
def is_world_space_pointer_over_ui(self):
|
||
runtime = getattr(self, "world_space_runtime", None)
|
||
if runtime is None:
|
||
return False
|
||
try:
|
||
return bool(runtime.is_pointer_over_ui())
|
||
except Exception:
|
||
return False
|
||
|
||
def has_world_space_pointer_capture(self):
|
||
runtime = getattr(self, "world_space_runtime", None)
|
||
if runtime is None:
|
||
return False
|
||
return bool(getattr(runtime, "_active_hit", None))
|
||
|
||
def handle_world_space_pointer_press(self):
|
||
runtime = getattr(self, "world_space_runtime", None)
|
||
if runtime is None:
|
||
return False
|
||
try:
|
||
return bool(runtime.handle_pointer_press_from_event())
|
||
except Exception as exc:
|
||
print(f"LUI 3D UI pointer handling failed: {exc}")
|
||
return False
|
||
|
||
def invalidate_world_space_canvas(self, canvas_index=None):
|
||
runtime = getattr(self, "world_space_runtime", None)
|
||
if runtime is None:
|
||
return
|
||
try:
|
||
if canvas_index is None:
|
||
runtime._signatures.clear()
|
||
else:
|
||
runtime._signatures.pop(int(canvas_index), None)
|
||
except Exception:
|
||
pass
|
||
|
||
def set_input_enabled(self, enabled):
|
||
"""Enable/disable LUI input handling (used to avoid ImGui click-through)."""
|
||
if not self.lui_enabled:
|
||
return
|
||
|
||
if enabled == self._input_enabled:
|
||
return
|
||
|
||
self._input_enabled = enabled
|
||
|
||
try:
|
||
if enabled:
|
||
if hasattr(self, "overlay_handler_node") and self.overlay_handler_node:
|
||
self.overlay_handler_node.reparent_to(self.world.mouseWatcher)
|
||
if hasattr(self, "overlay_region") and self.overlay_region:
|
||
self.overlay_region.set_input_handler(self.overlay_handler)
|
||
else:
|
||
if hasattr(self, "overlay_region") and self.overlay_region:
|
||
self.overlay_region.set_input_handler(None)
|
||
if hasattr(self, "overlay_handler_node") and self.overlay_handler_node:
|
||
self.overlay_handler_node.detach_node()
|
||
except Exception as e:
|
||
print(f"Warning: Failed to toggle LUI input state: {e}")
|
||
|
||
def _register_chinese_font(self):
|
||
"""注册支持中文的字体"""
|
||
from panda3d.lui import LUIFontPool
|
||
import os
|
||
|
||
# 尝试常见的系统路径,确保使用完整的 OS 路径
|
||
candidate_fonts = [
|
||
"C:/Windows/Fonts/msyh.ttc",
|
||
"C:/Windows/Fonts/msyhbd.ttc",
|
||
"C:/Windows/Fonts/simhei.ttf",
|
||
"C:/Windows/Fonts/simsun.ttc",
|
||
"C:/Windows/Fonts/arialuni.ttf",
|
||
"C:/Windows/Fonts/arial.ttf"
|
||
]
|
||
|
||
font_registered = False
|
||
for fpath in candidate_fonts:
|
||
if os.path.exists(fpath):
|
||
try:
|
||
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"):
|
||
font_label.setPixelsPerUnit(32)
|
||
if hasattr(font_header, "setPixelsPerUnit"):
|
||
font_header.setPixelsPerUnit(80)
|
||
|
||
font_pool = LUIFontPool.get_global_ptr()
|
||
font_pool.register_font("default", font_default)
|
||
font_pool.register_font("label", font_label)
|
||
font_pool.register_font("header", font_header)
|
||
print(f"✓ LUI 成功注册中文字体: {panda_path}")
|
||
font_registered = True
|
||
break
|
||
except Exception as e:
|
||
print(f"LUI 字体注册失败 {fpath}: {e}")
|
||
|
||
if not font_registered:
|
||
# 如果没有系统字体,至少注册一个默认的避免崩溃
|
||
print("⚠️ 警告: 未能找到合适的 LUI 中文字体")
|
||
|
||
def _init_ui_regions(self):
|
||
"""初始化 LUI 区域 (Overlay, Camera, WorldSpace)"""
|
||
# 渲染层级说明(参考engine/main.py的设置):
|
||
# - 3D场景(render)默认sort=0
|
||
# - LUI组件设置sort=5,在3D场景之上
|
||
# - pixel2d (ImGui) 默认sort=20,在LUI之上
|
||
# 这样:3D场景 < LUI组件 < ImGui面板
|
||
|
||
self.overlay_region = LUIRegion.make("OverlayUI", self.world.win)
|
||
self.overlay_handler = LUIInputHandler()
|
||
self.overlay_handler_node = self.world.mouseWatcher.attach_new_node(self.overlay_handler)
|
||
self.overlay_region.set_input_handler(self.overlay_handler)
|
||
self.overlay_region.set_sort(5) # 在3D场景之上,在ImGui之下(与engine保持一致)
|
||
self.overlay_root = self.overlay_region.root
|
||
|
||
# 调试输出:确认层级设置
|
||
actual_sort = self.overlay_region.getSort()
|
||
print(f"✓ LUI Overlay Region 层级设置: {actual_sort}")
|
||
|
||
# 显示所有DisplayRegion的层级信息
|
||
num_regions = self.world.win.getNumDisplayRegions()
|
||
print(f"✓ 窗口总共有 {num_regions} 个DisplayRegion:")
|
||
for i in range(num_regions):
|
||
dr = self.world.win.getDisplayRegion(i)
|
||
sort_value = dr.getSort()
|
||
camera = dr.getCamera()
|
||
camera_name = camera.getName() if not camera.isEmpty() else "无摄像机"
|
||
print(f" Region {i}: sort={sort_value}, camera={camera_name}")
|
||
|
||
# 当前操作的根节点
|
||
self.root = self.overlay_root
|