LUI添加3D

This commit is contained in:
Rowland 2026-05-25 14:07:58 +08:00
parent 1223746dee
commit 1019179b8d
9 changed files with 2231 additions and 23 deletions

View File

@ -0,0 +1,28 @@
# Scene-space effect for world-space LUI canvas textures.
fragment:
defines: |
#define DONT_FETCH_DEFAULT_TEXTURES 1
#define DONT_SET_MATERIAL_PROPERTIES 1
inout: |
uniform sampler2D p3d_Texture0;
material: |
vec4 lui_color = texture(p3d_Texture0, texcoord);
if (lui_color.a <= 0.001) {
discard;
}
#if IN_FORWARD_SHADER
color_result = lui_color;
return;
#else
m.shading_model = SHADING_MODEL_DEFAULT;
m.basecolor = lui_color.rgb;
m.normal = normalize(vOutput.normal);
m.roughness = 0.65;
m.specular_ior = 1.45;
m.metallic = 0.0;
m.shading_model_param0 = 0.0;
#endif

View File

@ -31,25 +31,25 @@ DockId=0x0000000D,0
[Window][场景树]
Pos=0,20
Size=339,996
Size=339,1023
Collapsed=0
DockId=0x00000007,0
[Window][属性面板]
Pos=1504,20
Size=346,498
Pos=1574,20
Size=346,512
Collapsed=0
DockId=0x00000003,0
[Window][控制台]
Pos=341,617
Size=1161,399
Pos=341,644
Size=1231,399
Collapsed=0
DockId=0x00000006,1
[Window][脚本管理]
Pos=1504,520
Size=346,496
Pos=1574,534
Size=346,509
Collapsed=0
DockId=0x00000004,0
@ -60,7 +60,7 @@ Collapsed=0
[Window][WindowOverViewport_11111111]
Pos=0,20
Size=1850,996
Size=1920,1023
Collapsed=0
[Window][测试窗口1]
@ -99,8 +99,8 @@ Size=600,500
Collapsed=0
[Window][资源管理器]
Pos=341,617
Size=1161,399
Pos=341,644
Size=1231,399
Collapsed=0
DockId=0x00000006,0
@ -150,7 +150,7 @@ Size=101,226
Collapsed=0
[Window][LUI编辑器]
Pos=160,44
Pos=20,40
Size=346,1331
Collapsed=0
@ -225,7 +225,7 @@ Size=460,260
Collapsed=0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,1023 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=2212,1012 Split=X
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=339,1084 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1871,1084 Split=Y

10
main.py
View File

@ -836,6 +836,10 @@ class MyWorld(PanelDelegates, CoreWorld):
if imgui_captured:
print("❌ ImGui处理了该事件跳过3D场景选择")
return
if hasattr(self, 'lui_manager') and self.lui_manager.handle_world_space_pointer_press():
print("✓ LUI 3D UI 处理了该事件跳过3D场景选择")
return
# 调用事件处理器进行射线检测和选择
if hasattr(self, 'event_handler'):
@ -867,6 +871,12 @@ class MyWorld(PanelDelegates, CoreWorld):
winWidth, winHeight = self.win.getSize()
window_x = (mouse_x + 1) * 0.5 * winWidth
window_y = (1 - mouse_y) * 0.5 * winHeight
if hasattr(self, 'lui_manager') and (
self.lui_manager.is_world_space_pointer_over_ui()
or self.lui_manager.has_world_space_pointer_capture()
):
return
# 调用事件处理器
if hasattr(self, 'event_handler'):

View File

@ -2584,6 +2584,10 @@ class SSBOEditor:
process_imgui_click = getattr(self.base, "processImGuiMouseClick", None)
if callable(process_imgui_click) and process_imgui_click(window_x, window_y):
return
lui_manager = getattr(self.base, "lui_manager", None)
handle_lui_world = getattr(lui_manager, "handle_world_space_pointer_press", None)
if callable(handle_lui_world) and handle_lui_world():
return
except Exception:
pass
self._sync_pick_model_transform()

View File

@ -81,6 +81,14 @@ class LUIFunctionComponentsMixin:
'panel': canvas_panel,
'visible': True,
'background': bg,
'render_mode': 'screen',
'world_position': (0.0, 8.0, 2.0),
'world_hpr': (25.0, 0.0, 0.0),
'world_scale': (1.0, 1.0, 1.0),
'world_pixels_per_unit': 280.0,
'world_depth_test': True,
'world_interactive': True,
'world_two_sided': False,
'fill_mode': True, # 默认开启填充模式
'margins': {
'left': 240,

View File

@ -102,9 +102,74 @@ class LUIManagerEditorMixin:
self._lui_structure_edit_sessions = {}
return self._lui_structure_edit_sessions
def _triplet_or_default(self, value, fallback):
if not isinstance(value, (list, tuple)) or len(value) < 3:
return tuple(fallback)
out = []
for idx, default in enumerate(fallback):
try:
out.append(float(value[idx]))
except Exception:
out.append(float(default))
return tuple(out)
def _ensure_canvas_world_defaults(self, canvas_data):
if not canvas_data:
return
canvas_data["render_mode"] = str(canvas_data.get("render_mode") or "screen").lower().strip()
if canvas_data["render_mode"] != "world":
canvas_data["render_mode"] = "screen"
canvas_data.setdefault("world_position", (0.0, 8.0, 2.0))
canvas_data.setdefault("world_hpr", (25.0, 0.0, 0.0))
canvas_data.setdefault("world_scale", (1.0, 1.0, 1.0))
canvas_data.setdefault("world_pixels_per_unit", 280.0)
canvas_data.setdefault("world_depth_test", True)
canvas_data.setdefault("world_interactive", True)
canvas_data.setdefault("world_two_sided", False)
def _copy_canvas_node_transform_to_canvas(self, canvas_data):
node = (canvas_data or {}).get("node")
if node is None:
return
try:
if hasattr(node, "isEmpty") and node.isEmpty():
return
pos = node.getPos()
hpr = node.getHpr()
scale = node.getScale()
canvas_data["world_position"] = (float(pos.x), float(pos.y), float(pos.z))
canvas_data["world_hpr"] = (float(hpr.x), float(hpr.y), float(hpr.z))
canvas_data["world_scale"] = (float(scale.x), float(scale.y), float(scale.z))
except Exception:
pass
def _apply_canvas_world_transform(self, canvas_data):
node = (canvas_data or {}).get("node")
if node is None:
return
try:
if hasattr(node, "isEmpty") and node.isEmpty():
return
node.setPos(*self._triplet_or_default(canvas_data.get("world_position"), (0.0, 8.0, 2.0)))
node.setHpr(*self._triplet_or_default(canvas_data.get("world_hpr"), (25.0, 0.0, 0.0)))
node.setScale(*self._triplet_or_default(canvas_data.get("world_scale"), (1.0, 1.0, 1.0)))
if canvas_data.get("render_mode") == "world":
node.setTag("is_lui_canvas", "1")
node.setTag("is_scene_element", "1")
node.setTag("tree_item_type", "SCENE_NODE")
else:
for tag_name in ("is_lui_canvas", "is_scene_element", "tree_item_type"):
if node.hasTag(tag_name):
node.clearTag(tag_name)
except Exception:
pass
def _capture_canvas_snapshot(self, canvas_data):
if not canvas_data:
return None
self._ensure_canvas_world_defaults(canvas_data)
if canvas_data.get("render_mode") == "world":
self._copy_canvas_node_transform_to_canvas(canvas_data)
panel = canvas_data.get("panel")
background = canvas_data.get("background")
@ -121,6 +186,10 @@ class LUIManagerEditorMixin:
top = float(pos.y)
except Exception:
pass
try:
world_pixels_per_unit = float(canvas_data.get("world_pixels_per_unit", 280.0) or 280.0)
except Exception:
world_pixels_per_unit = 280.0
return {
"name": canvas_data.get("name"),
@ -128,6 +197,14 @@ class LUIManagerEditorMixin:
"fill_mode": bool(canvas_data.get("fill_mode", False)),
"fixed": bool(canvas_data.get("fixed", False)),
"margins": copy.deepcopy(canvas_data.get("margins", {})),
"render_mode": canvas_data.get("render_mode", "screen"),
"world_position": self._triplet_or_default(canvas_data.get("world_position"), (0.0, 8.0, 2.0)),
"world_hpr": self._triplet_or_default(canvas_data.get("world_hpr"), (25.0, 0.0, 0.0)),
"world_scale": self._triplet_or_default(canvas_data.get("world_scale"), (1.0, 1.0, 1.0)),
"world_pixels_per_unit": world_pixels_per_unit,
"world_depth_test": bool(canvas_data.get("world_depth_test", True)),
"world_interactive": bool(canvas_data.get("world_interactive", True)),
"world_two_sided": bool(canvas_data.get("world_two_sided", False)),
"left": left,
"top": top,
"width": width,
@ -229,6 +306,12 @@ class LUIManagerEditorMixin:
self._hide_resize_handles()
except Exception:
pass
runtime = getattr(self, "world_space_runtime", None)
if runtime is not None:
try:
runtime.clear()
except Exception:
pass
if hasattr(self, "debug_outlines") and isinstance(self.debug_outlines, dict):
for borders in list(self.debug_outlines.values()):
@ -315,11 +398,42 @@ class LUIManagerEditorMixin:
canvas_data["fixed"] = bool(snapshot.get("fixed", canvas_data.get("fixed", False)))
canvas_data["visible"] = bool(snapshot.get("visible", canvas_data.get("visible", True)))
canvas_data["margins"] = copy.deepcopy(snapshot.get("margins", canvas_data.get("margins", {})))
canvas_data["render_mode"] = str(snapshot.get("render_mode", canvas_data.get("render_mode", "screen")) or "screen").lower().strip()
if canvas_data["render_mode"] != "world":
canvas_data["render_mode"] = "screen"
canvas_data["world_position"] = self._triplet_or_default(
snapshot.get("world_position", canvas_data.get("world_position")),
(0.0, 8.0, 2.0),
)
canvas_data["world_hpr"] = self._triplet_or_default(
snapshot.get("world_hpr", canvas_data.get("world_hpr")),
(25.0, 0.0, 0.0),
)
canvas_data["world_scale"] = self._triplet_or_default(
snapshot.get("world_scale", canvas_data.get("world_scale")),
(1.0, 1.0, 1.0),
)
try:
canvas_data["world_pixels_per_unit"] = float(
snapshot.get("world_pixels_per_unit", canvas_data.get("world_pixels_per_unit", 280.0)) or 280.0
)
except Exception:
canvas_data["world_pixels_per_unit"] = 280.0
canvas_data["world_depth_test"] = bool(
snapshot.get("world_depth_test", canvas_data.get("world_depth_test", True))
)
canvas_data["world_interactive"] = bool(
snapshot.get("world_interactive", canvas_data.get("world_interactive", True))
)
canvas_data["world_two_sided"] = bool(
snapshot.get("world_two_sided", canvas_data.get("world_two_sided", False))
)
panel = canvas_data.get("panel")
background = canvas_data.get("background")
title_label = canvas_data.get("title_label")
canvas_node = canvas_data.get("node")
self._apply_canvas_world_transform(canvas_data)
if canvas_node is not None and hasattr(canvas_node, "setName"):
try:
@ -366,9 +480,12 @@ class LUIManagerEditorMixin:
pass
try:
if canvas_data["visible"] and hasattr(panel, "show"):
screen_visible = canvas_data["visible"] and canvas_data.get("render_mode") != "world"
if hasattr(self, "_set_lui_canvas_screen_visible"):
self._set_lui_canvas_screen_visible(canvas_data, screen_visible)
elif screen_visible and hasattr(panel, "show"):
panel.show()
elif not canvas_data["visible"] and hasattr(panel, "hide"):
elif (not screen_visible) and hasattr(panel, "hide"):
panel.hide()
except Exception:
pass
@ -402,6 +519,9 @@ class LUIManagerEditorMixin:
except Exception:
pass
if hasattr(self, "invalidate_world_space_canvas"):
self.invalidate_world_space_canvas(canvas_index)
def _compute_lui_structure_absolute_position(self, index, component_snapshots, memo=None):
if memo is None:
memo = {}
@ -1554,6 +1674,136 @@ class LUIManagerEditorMixin:
# Recurse for grandchildren
self._sync_canvas_children(child_idx)
def _set_canvas_render_mode(self, canvas, render_mode):
if not canvas:
return
mode = "world" if str(render_mode).lower().strip() == "world" else "screen"
self._ensure_canvas_world_defaults(canvas)
canvas["render_mode"] = mode
if mode == "world":
current_hpr = self._triplet_or_default(canvas.get("world_hpr"), (0.0, 0.0, 0.0))
if not canvas.get("_world_hpr_user_set") and all(abs(float(v)) < 1e-6 for v in current_hpr):
canvas["world_hpr"] = (25.0, 0.0, 0.0)
self._apply_canvas_world_transform(canvas)
if hasattr(self, "invalidate_world_space_canvas"):
try:
self.invalidate_world_space_canvas(self.canvases.index(canvas))
except Exception:
self.invalidate_world_space_canvas()
if 0 <= getattr(self, "current_canvas_index", -1) < len(getattr(self, "canvases", [])):
self.switch_canvas(self.current_canvas_index)
runtime = getattr(self, "world_space_runtime", None)
if runtime is not None:
try:
runtime.sync()
except Exception:
pass
if hasattr(self.world, "updateSceneTree"):
try:
self.world.updateSceneTree()
except Exception:
pass
def _draw_canvas_render_mode_controls(self, canvas):
self._ensure_canvas_world_defaults(canvas)
mode_labels = ["屏幕 (Screen)", "场景3D (World)"]
current_mode = canvas.get("render_mode", "screen")
current_mode_index = 1 if current_mode == "world" else 0
changed, new_mode_index = imgui.combo("渲染模式", current_mode_index, mode_labels)
if changed:
new_mode = "world" if new_mode_index == 1 else "screen"
self._apply_lui_structure_change_with_history(
lambda target_mode=new_mode: self._set_canvas_render_mode(canvas, target_mode)
)
if canvas.get("render_mode") == "world":
self._draw_world_canvas_controls(canvas)
def _draw_world_canvas_controls(self, canvas):
self._copy_canvas_node_transform_to_canvas(canvas)
imgui.separator()
imgui.text("场景3D设置")
pos = self._triplet_or_default(canvas.get("world_position"), (0.0, 8.0, 2.0))
changed, new_pos = imgui.drag_float3("3D位置", pos, 0.05, -10000.0, 10000.0)
if imgui.is_item_activated():
self._begin_lui_structure_snapshot_session("canvas_world_position")
if changed:
canvas["world_position"] = tuple(new_pos)
self._apply_canvas_world_transform(canvas)
if hasattr(self, "invalidate_world_space_canvas"):
self.invalidate_world_space_canvas(self.current_canvas_index)
self._finish_lui_structure_snapshot_session("canvas_world_position")
hpr = self._triplet_or_default(canvas.get("world_hpr"), (25.0, 0.0, 0.0))
changed, new_hpr = imgui.drag_float3("3D旋转(HPR)", hpr, 0.5, -360.0, 360.0)
if imgui.is_item_activated():
self._begin_lui_structure_snapshot_session("canvas_world_hpr")
if changed:
canvas["world_hpr"] = tuple(new_hpr)
canvas["_world_hpr_user_set"] = True
self._apply_canvas_world_transform(canvas)
if hasattr(self, "invalidate_world_space_canvas"):
self.invalidate_world_space_canvas(self.current_canvas_index)
self._finish_lui_structure_snapshot_session("canvas_world_hpr")
scale = self._triplet_or_default(canvas.get("world_scale"), (1.0, 1.0, 1.0))
changed, new_scale = imgui.drag_float3("3D缩放", scale, 0.01, 0.01, 100.0)
if imgui.is_item_activated():
self._begin_lui_structure_snapshot_session("canvas_world_scale")
if changed:
canvas["world_scale"] = tuple(max(0.01, float(v)) for v in new_scale)
self._apply_canvas_world_transform(canvas)
if hasattr(self, "invalidate_world_space_canvas"):
self.invalidate_world_space_canvas(self.current_canvas_index)
self._finish_lui_structure_snapshot_session("canvas_world_scale")
ppu = float(canvas.get("world_pixels_per_unit", 280.0) or 280.0)
changed, new_ppu = imgui.drag_float("像素/单位", ppu, 1.0, 10.0, 4000.0)
if imgui.is_item_activated():
self._begin_lui_structure_snapshot_session("canvas_world_ppu")
if changed:
canvas["world_pixels_per_unit"] = max(10.0, float(new_ppu))
if hasattr(self, "invalidate_world_space_canvas"):
self.invalidate_world_space_canvas(self.current_canvas_index)
self._finish_lui_structure_snapshot_session("canvas_world_ppu")
depth_test = bool(canvas.get("world_depth_test", True))
changed, new_depth_test = imgui.checkbox("启用深度测试", depth_test)
if changed:
self._apply_lui_structure_change_with_history(
lambda value=new_depth_test: canvas.__setitem__("world_depth_test", bool(value))
)
if hasattr(self, "invalidate_world_space_canvas"):
self.invalidate_world_space_canvas(self.current_canvas_index)
interactive = bool(canvas.get("world_interactive", True))
changed, new_interactive = imgui.checkbox("启用3D交互", interactive)
if changed:
self._apply_lui_structure_change_with_history(
lambda value=new_interactive: canvas.__setitem__("world_interactive", bool(value))
)
two_sided = bool(canvas.get("world_two_sided", False))
changed, new_two_sided = imgui.checkbox("双面显示", two_sided)
if changed:
self._apply_lui_structure_change_with_history(
lambda value=new_two_sided: canvas.__setitem__("world_two_sided", bool(value))
)
if hasattr(self, "invalidate_world_space_canvas"):
self.invalidate_world_space_canvas(self.current_canvas_index)
debug_world_lui = bool(getattr(self, "debug_world_lui", False))
changed, new_debug_world_lui = imgui.checkbox("输出3D LUI调试日志", debug_world_lui)
if changed:
self.debug_world_lui = bool(new_debug_world_lui)
runtime = getattr(self, "world_space_runtime", None)
if runtime is not None:
try:
runtime._debug_force_next = True
except Exception:
pass
def draw_editor(self):
"""Draw the LUI Editor Window"""
if hasattr(self, "world") and hasattr(self.world, "showLUIEditor"):
@ -1651,10 +1901,15 @@ class LUIManagerEditorMixin:
self._apply_lui_structure_change_with_history(
lambda: (
canvas.__setitem__('visible', visible),
panel.show() if visible else panel.hide(),
self._set_lui_canvas_screen_visible(
canvas,
visible and self._canvas_render_mode(canvas) != "world",
) if hasattr(self, "_set_lui_canvas_screen_visible")
else (panel.show() if visible else panel.hide()),
)
)
self._draw_canvas_render_mode_controls(canvas)
# Fill Mode & Margins
is_fill_mode = canvas.get('fill_mode', False)

View File

@ -69,14 +69,29 @@ class LUIManagerInteractionMixin:
self.current_canvas_index = index
# 显示选中的Canvas隐藏其他Canvas
# 显示选中的Canvas隐藏其他Canvas。场景3D Canvas 只显示场景镜像,
# 原始 overlay 仍作为编辑数据源,但不直接绘制在屏幕上。
for i, canvas in enumerate(self.canvases):
render_mode = "screen"
try:
render_mode = self._canvas_render_mode(canvas)
except Exception:
render_mode = str(canvas.get("render_mode") or "screen").lower().strip()
screen_visible = (i == index and render_mode != "world")
if i == index:
canvas['panel'].show()
canvas['visible'] = True
else:
canvas['panel'].hide()
canvas['visible'] = False
if hasattr(self, "_set_lui_canvas_screen_visible"):
self._set_lui_canvas_screen_visible(canvas, screen_visible)
else:
try:
if screen_visible:
canvas['panel'].show()
else:
canvas['panel'].hide()
except Exception:
pass
# 显示/隐藏属于不同Canvas的组件包括子组件
visible_count = 0
@ -87,24 +102,43 @@ class LUIManagerInteractionMixin:
comp_obj = comp['object']
comp_name = comp.get('name', 'Unknown')
should_be_visible = (comp_canvas_index == index)
canvas_render_mode = "screen"
try:
if comp_canvas_index is not None and 0 <= comp_canvas_index < len(self.canvases):
canvas_render_mode = self._canvas_render_mode(self.canvases[comp_canvas_index])
except Exception:
canvas_render_mode = "screen"
should_be_visible = (comp_canvas_index == index and canvas_render_mode != "world")
if hasattr(self, "_set_lui_component_screen_visible"):
self._set_lui_component_screen_visible(comp, should_be_visible)
if should_be_visible:
visible_count += 1
else:
hidden_count += 1
if hasattr(self, "_set_lui_component_screen_visible"):
continue
if should_be_visible:
if hasattr(comp_obj, 'show'):
comp_obj.show()
elif hasattr(comp_obj, 'visible'):
comp_obj.visible = True
visible_count += 1
else:
if hasattr(comp_obj, 'hide'):
comp_obj.hide()
elif hasattr(comp_obj, 'visible'):
comp_obj.visible = False
hidden_count += 1
# 更新当前根节点
if index >= 0:
self.root = self.canvases[index]['panel']
try:
if self._canvas_render_mode(self.canvases[index]) == "world" and hasattr(self, "_hide_resize_handles"):
self._hide_resize_handles()
except Exception:
pass
# 如果选中的组件不属于当前Canvas取消选中
if self.selected_index >= 0 and self.selected_index < len(self.components):
@ -118,6 +152,24 @@ class LUIManagerInteractionMixin:
print(f"✓ Switched to Canvas: {self.canvases[index]['name']}")
print(f" 显示组件: {visible_count}, 隐藏组件: {hidden_count}")
try:
if getattr(self, "debug_world_lui", False):
active_canvas = self.canvases[index]
active_mode = self._canvas_render_mode(active_canvas)
active_panel = active_canvas.get("panel")
parent_label = (
self._debug_lui_parent_label(getattr(active_panel, "parent", None))
if hasattr(self, "_debug_lui_parent_label")
else str(getattr(active_panel, "parent", None))
)
print(
"[LUI3D Debug] switch_canvas "
f"idx={index} name='{active_canvas.get('name')}' mode={active_mode} "
f"screen_visible={active_mode != 'world'} panel_parent={parent_label} "
f"visible_components={visible_count} hidden_components={hidden_count}"
)
except Exception:
pass
def _update_canvas_geometry(self, canvas_data):
"""根据填充模式和边距更新Canvas几何信息"""
@ -831,6 +883,14 @@ class LUIManagerInteractionMixin:
comp_data = self.components[self.selected_index]
comp_type = comp_data['type']
comp_canvas_index = comp_data.get('canvas_index')
try:
if comp_canvas_index is not None and 0 <= comp_canvas_index < len(self.canvases):
if self._canvas_render_mode(self.canvases[comp_canvas_index]) == "world":
self._hide_resize_handles()
return task.cont
except Exception:
pass
# 只对Frame、Button、Slider、InputField、Plane、Image显示resize handles
if comp_type not in ['Frame', 'Button', 'Slider', 'InputField', 'Plane', 'Image', 'Checkbox', 'Text', 'Label', 'Video', 'Progressbar', 'Selectbox', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout', 'HttpText']:
@ -908,8 +968,12 @@ class LUIManagerInteractionMixin:
continue
canvas = self.canvases[canvas_index]
if not canvas.get('visible', True):
# Hide outlines for components on hidden canvases
try:
is_world_canvas = self._canvas_render_mode(canvas) == "world"
except Exception:
is_world_canvas = str(canvas.get("render_mode") or "screen").lower().strip() == "world"
if not canvas.get('visible', True) or is_world_canvas:
# Hide screen-space editor outlines for hidden or scene-space canvases.
borders = self.debug_outlines.get(idx)
if borders:
for b in borders:

1608
ui/LUI/lui_world_space.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,13 @@ 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,
@ -105,6 +112,10 @@ class LUIManager(LUIManagerInteractionMixin, LUIManagerEditorMixin):
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'):
@ -121,6 +132,226 @@ class LUIManager(LUIManagerInteractionMixin, LUIManagerEditorMixin):
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: