EG/ui/lui_function.py
2026-02-25 11:49:31 +08:00

2557 lines
116 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import sys
from pathlib import Path
import panda3d.core as p3d
from panda3d.core import NodePath, CardMaker
from imgui_bundle import imgui, imgui_ctx
# Ensure ui directory is in path and lui is correctly patched
UI_DIR = Path(__file__).resolve().parent
if str(UI_DIR) not in sys.path:
sys.path.insert(0, str(UI_DIR))
# Add Builtin to path
BUILTIN_DIR = UI_DIR / "Builtin"
if str(BUILTIN_DIR) not in sys.path:
sys.path.insert(0, str(BUILTIN_DIR))
try:
import lui
# Monkey patch for Builtin components
import panda3d
panda3d.lui = lui
sys.modules["panda3d.lui"] = lui
from Builtin.LUIRegion import LUIRegion
from Builtin.LUIInputHandler import LUIInputHandler
from Builtin.LUIButton import LUIButton
from Builtin.LUILabel import LUILabel
from Builtin.LUIInputField import LUIInputField
from Builtin.LUISlider import LUISlider
from Builtin.LUIFrame import LUIFrame
from Builtin.LUISkin import LUIDefaultSkin
from Builtin.LUISprite import LUISprite
from Builtin.LUIObject import LUIObject
from Builtin.LUICheckbox import LUICheckbox
from Builtin.LUIProgressbar import LUIProgressbar
from Builtin.LUISelectbox import LUISelectbox
from Builtin.LUIScrollableRegion import LUIScrollableRegion
from Builtin.LUITabbedFrame import LUITabbedFrame
from Builtin.LUIVerticalLayout import LUIVerticalLayout
from Builtin.LUIHorizontalLayout import LUIHorizontalLayout
except ImportError as e:
print(f"Error: Failed to import LUI: {e}")
lui = None
class luiFunction:
def create_canvas(manager, name=None):
"""创建一个可视化的 UI 面板作为画布"""
manager.canvas_counter += 1
canvas_name = name or f"{manager.canvas_counter}"
# 获取窗口尺寸
win_width = 800
win_height = 600
if hasattr(manager.world, 'win'):
win_width = manager.world.win.getXSize()
win_height = manager.world.win.getYSize()
# 计算场景窗口区域排除Hierarchy面板
left_offset = 0
if hasattr(manager.world, 'hierarchy_size'):
left_offset = manager.world.hierarchy_size.x
target_width = win_width - left_offset
target_height = win_height
canvas_panel = LUIObject(parent=manager.overlay_root, x=left_offset, y=0)
# 显式设置尺寸
canvas_panel.width = target_width
canvas_panel.height = target_height
canvas_panel.solid = True # 接收事件
# 创建背景 Sprite
bg = LUISprite(canvas_panel, "blank", "skin")
bg.width = target_width
bg.height = target_height
bg.color = (0.5, 0.5, 0.5, 1.0) # 半透明灰色背景可见但不遮挡ImGui
# Canvas背景在LUI内部的底层
if hasattr(bg, 'set_sort'):
bg.set_sort(-1) # LUI内部的底层
# Canvas面板使用默认层级继承自LUI Region的sort=5
# 不需要额外设置sort会自动继承LUI Region的层级
canvas_panel.solid = True # 接收事件用于Canvas交互
# 创建Canvas的NodePath节点用于层级树显示
canvas_node = NodePath(canvas_name)
canvas_node.reparent_to(manager.world.render)
# 存储Canvas数据
canvas_data = {
'name': canvas_name,
'node': canvas_node,
'panel': canvas_panel,
'visible': True,
'background': bg,
'fill_mode': True, # 默认开启填充模式
'margins': {
'left': 240,
'right': 480, # 默认右侧面板宽度
'top': 89,
'bottom': 220 # 默认底部面板高度
}
}
# 应用初始几何计算 (调用 manager 的方法)
# 注意:这里需要 manager 是 public 的或者我们访问 protected 方法
if hasattr(manager, '_update_canvas_geometry'):
manager._update_canvas_geometry(canvas_data)
# 添加面板标题
title_text = f"Canvas: {canvas_name}"
title = LUILabel(text=title_text, font_size=16)
title.set_parent(canvas_panel)
title.set_pos(10, 10) # 内部 padding
if hasattr(title, 'set_color'):
title.set_color((0.8, 0.8, 0.8, 1.0))
# 存储标题引用以便后续修改
canvas_data['title_label'] = title
# 启用Canvas拖动
if hasattr(manager, '_make_canvas_draggable'):
manager._make_canvas_draggable(canvas_panel)
manager.canvases.append(canvas_data)
manager.current_canvas_index = len(manager.canvases) - 1
manager.switch_canvas(manager.current_canvas_index)
# 调试输出确认Canvas层级
print(f"✓ Created Canvas: {canvas_name}")
print(f" Canvas panel sort: {getattr(canvas_panel, 'sort', 'N/A')}")
print(f" Background sprite sort: {getattr(bg, 'sort', 'N/A')}")
if hasattr(manager.world, 'updateSceneTree'):
manager.world.updateSceneTree()
return canvas_data
def create_button(manager, text="Button", x=100, y=100, parent=None):
"""Create a LUI Button"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
btn = LUIButton(parent=parent, text=text, x=x, y=y)
# Ensure size is initialized so selection bounds match the actual button
default_w = 100
default_h = 30
try:
if not hasattr(btn, 'width') or btn.width is None or btn.width <= 0:
btn.width = default_w
if not hasattr(btn, 'height') or btn.height is None or btn.height <= 0:
btn.height = default_h
except Exception:
pass
width = getattr(btn, 'width', default_w) or default_w
height = getattr(btn, 'height', default_h) or default_h
# 设置组件层级在Canvas背景之上
if hasattr(btn, 'set_sort'):
btn.set_sort(1) # 在Canvas背景之上
comp_data = {
'type': 'Button',
'object': btn,
'widget': btn,
'text': text,
'left': x,
'top': y,
'width': float(width),
'height': float(height),
'canvas_index': manager.current_canvas_index,
'name': f"Button_{len(manager.components)}",
'color': (1.0, 1.0, 1.0, 1.0),
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
if hasattr(manager, '_setup_component_drag'):
manager._setup_component_drag(comp_data, comp_index)
if hasattr(manager, '_add_to_scene_tree'):
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return btn
def create_label(manager,text="Text", x=100, y=100, font_size=14, parent=None):
"""Create a LUI Text"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
lbl = LUILabel(parent=parent, text=text, x=x, y=y, solid=True, font_size=font_size)
# 设置组件层级在Canvas背景之上
if hasattr(lbl, 'set_sort'):
lbl.set_sort(1) # 在Canvas背景之上
comp_data = {
'type': 'Text',
'object': lbl,
'text': text,
'left': x,
'top': y,
'width': 80,
'height': 20,
'canvas_index': manager.current_canvas_index,
'name': f"Text_{len(manager.components)}",
'color': (1, 1, 1, 1),
'font_size': font_size,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return lbl
def create_input_field(manager,text="", x=100, y=100, width=200, parent=None):
"""Create a LUI InputField"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
inf = LUIInputField(parent=parent, value=text, x=x, y=y, width=width)
comp_data = {
'type': 'InputField',
'object': inf,
'text': text,
'left': x,
'top': y,
'width': width,
'height': 24,
'canvas_index': manager.current_canvas_index,
'name': f"InputField_{len(manager.components)}",
'value': text,
'placeholder': "输入文本...",
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return inf
def create_slider(manager,x=100, y=100, width=200, parent=None):
"""Create a LUI Slider"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
sld = LUISlider(parent=parent, x=x, y=y, width=width)
if hasattr(sld, 'set_height'):
sld.set_height(16)
if hasattr(sld, 'height'):
sld.height = 16
comp_data = {
'type': 'Slider',
'object': sld,
'left': x,
'top': y,
'width': width,
'height': 16,
'canvas_index': manager.current_canvas_index,
'name': f"Slider_{len(manager.components)}",
'min_value': 0.0,
'max_value': 100.0,
'value': 50.0,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return sld
def create_frame(manager,x=100, y=100, width=300, height=200, parent=None):
"""Create a LUI Frame"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
frm = LUIFrame(parent=parent, x=x, y=y, width=width, height=height)
comp_data = {
'type': 'Frame',
'object': frm,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"Frame_{len(manager.components)}",
'color': (0.7, 0.7, 0.7, 0.8),
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return frm
def create_vertical_layout(manager, x=100, y=100, width=300, height=200, spacing=0.0, parent=None):
"""Create a LUI Vertical Layout"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
container = LUIObject(parent=parent, x=x, y=y)
if hasattr(container, 'width'):
container.width = width
if hasattr(container, 'height'):
container.height = height
if hasattr(container, 'solid'):
container.solid = True
layout = LUIVerticalLayout(parent=container, spacing=spacing)
padding_left = 0.0
padding_right = 0.0
padding_top = 0.0
padding_bottom = 0.0
inner_w = max(1.0, float(width) - (padding_left + padding_right))
inner_h = max(1.0, float(height) - (padding_top + padding_bottom))
if hasattr(layout, 'left'):
layout.left = padding_left
if hasattr(layout, 'top'):
layout.top = padding_top
if hasattr(layout, 'width'):
layout.width = inner_w
if hasattr(layout, 'height'):
layout.height = inner_h
comp_data = {
'type': 'VerticalLayout',
'object': container,
'layout_obj': layout,
'left': x,
'top': y,
'width': width,
'height': height,
'layout_spacing': spacing,
'layout_padding_left': padding_left,
'layout_padding_right': padding_right,
'layout_padding_top': padding_top,
'layout_padding_bottom': padding_bottom,
'layout_align': 'start',
'layout_wrap': True,
'layout_line_spacing': 0.0,
'canvas_index': manager.current_canvas_index,
'name': f"VerticalLayout_{len(manager.components)}",
'sort': 1,
'draggable': True
}
comp_index = len(manager.components)
if hasattr(manager, '_setup_component_drag'):
manager._setup_component_drag(comp_data, comp_index)
if hasattr(manager, '_add_to_scene_tree'):
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return container
def create_horizontal_layout(manager, x=100, y=100, width=300, height=200, spacing=0.0, parent=None):
"""Create a LUI Horizontal Layout"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
container = LUIObject(parent=parent, x=x, y=y)
if hasattr(container, 'width'):
container.width = width
if hasattr(container, 'height'):
container.height = height
if hasattr(container, 'solid'):
container.solid = True
layout = LUIHorizontalLayout(parent=container, spacing=spacing)
padding_left = 0.0
padding_right = 0.0
padding_top = 0.0
padding_bottom = 0.0
inner_w = max(1.0, float(width) - (padding_left + padding_right))
inner_h = max(1.0, float(height) - (padding_top + padding_bottom))
if hasattr(layout, 'left'):
layout.left = padding_left
if hasattr(layout, 'top'):
layout.top = padding_top
if hasattr(layout, 'width'):
layout.width = inner_w
if hasattr(layout, 'height'):
layout.height = inner_h
comp_data = {
'type': 'HorizontalLayout',
'object': container,
'layout_obj': layout,
'left': x,
'top': y,
'width': width,
'height': height,
'layout_spacing': spacing,
'layout_padding_left': padding_left,
'layout_padding_right': padding_right,
'layout_padding_top': padding_top,
'layout_padding_bottom': padding_bottom,
'layout_align': 'start',
'layout_wrap': True,
'layout_line_spacing': 0.0,
'canvas_index': manager.current_canvas_index,
'name': f"HorizontalLayout_{len(manager.components)}",
'sort': 1,
'draggable': True
}
comp_index = len(manager.components)
if hasattr(manager, '_setup_component_drag'):
manager._setup_component_drag(comp_data, comp_index)
if hasattr(manager, '_add_to_scene_tree'):
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return container
def create_checkbox(manager, label="Checkbox", x=100, y=100, parent=None):
"""Create a LUI Checkbox"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
chk = LUICheckbox(parent=parent, label=label, x=x, y=y)
comp_data = {
'type': 'Checkbox',
'object': chk,
'text': label,
'left': x,
'top': y,
'width': 120,
'height': 20,
'canvas_index': manager.current_canvas_index,
'name': f"Checkbox_{len(manager.components)}",
'checked': False,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return chk
def create_plane(manager, x=100, y=100, width=100, height=100, color=(1,1,1,1), parent=None):
"""Create a LUI Plane (using Sprite with blank texture)"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
# 创建LUIObject容器
obj = LUIObject(parent=parent, x=x, y=y)
obj.width = width
obj.height = height
obj.solid = True # 重要设置为solid才能接收鼠标事件
# 设置组件层级在Canvas背景之上 (Plane)
if hasattr(obj, 'set_sort'):
obj.set_sort(1) # 在Canvas背景之上
# 创建Sprite作为平面背景
spr = LUISprite(obj, "blank", "skin")
spr.width = width
spr.height = height
spr.color = color
spr.left = 0
spr.top = 0
comp_data = {
'type': 'Plane',
'object': obj,
'sprite': spr,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"Plane_{len(manager.components)}",
'color': color,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return obj
def create_image(manager, texture_path="blank", x=100, y=100, width=100, height=100, parent=None):
"""Create a LUI Image"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
# 创建LUIObject容器
obj = LUIObject(parent=parent, x=x, y=y)
obj.width = width
obj.height = height
obj.solid = True # 重要设置为solid才能接收鼠标事件
# 设置组件层级在Canvas背景之上 (Image)
if hasattr(obj, 'set_sort'):
obj.set_sort(1) # 在Canvas背景之上
# 加载纹理 (使用皮肤中的'blank'或外部路径)
# 注意: LUISprite通常使用图集图像名称或纹理
# 对于外部图像可能需要加载纹理并使用不同的LUI元素
# 或将其注册到图集中。LUISprite(parent, image, atlas)
# 为了简单起见我们使用默认皮肤sprite它有'blank'
img_name = "blank"
spr = LUISprite(obj, img_name, "skin")
spr.width = width
spr.height = height
spr.left = 0
spr.top = 0
# 设置默认图像颜色(浅灰色,表示这是一个图像占位符)
spr.color = (1.0, 1.0, 1.0, 1.0)
comp_data = {
'type': 'Image',
'object': obj,
'sprite': spr,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"Image_{len(manager.components)}",
'texture_path': texture_path,
'color': (1.0, 1.0, 1.0, 1.0),
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return obj
def create_video(manager, video_path="", x=100, y=100, width=320, height=240, parent=None):
"""Create a LUI Video Player component"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
# 1. 创建容器对象
obj = LUIObject(parent=parent, x=x, y=y)
obj.width = width
obj.height = height
obj.solid = True
# 设置层级
if hasattr(obj, 'set_sort'):
obj.set_sort(1)
# 2. 准备视频纹理
video_texture = None
if video_path:
try:
from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData
# Trim whitespace which could cause "file not found" errors
video_path = video_path.strip()
# Allow network protocols
loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file")
if "://" in video_path:
# 尝试更稳定的加载序列
try:
video_src = MovieVideo.get(Filename(video_path))
video_texture = MovieTexture("RemoteVideo")
if not video_texture.load(video_src):
video_texture = manager.world.loader.loadTexture(video_path)
except:
video_texture = manager.world.loader.loadTexture(video_path)
else:
video_texture = manager.world.loader.loadTexture(Filename.from_os_specific(video_path))
if video_texture:
print(f"✓ 视频加载成功: {video_path}")
# 尝试同步视频尺寸比例
orig_w = video_texture.getOrigFileXSize()
orig_h = video_texture.getOrigFileYSize()
if orig_w > 0 and orig_h > 0:
ratio = orig_w / orig_h
# 保持宽度,调整高度
new_height = width / ratio
obj.height = new_height
height = new_height # Update local var
except Exception as e:
print(f"⚠ 加载视频失败: {e}")
# 2.1 ???????????????????????????????????????
audio_sound = None
audio_path = ""
audio_from_video = False
if video_path and "://" not in video_path:
try:
audio_path = video_path
audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(video_path))
if audio_sound:
audio_from_video = True
if hasattr(audio_sound, 'setLoop'):
audio_sound.setLoop(True)
if hasattr(audio_sound, 'setVolume'):
audio_sound.setVolume(1.0)
except Exception:
audio_sound = None
audio_path = ""
# 3. 创建Sprite
if video_texture:
# 直接使用纹理创建不使用Atlas
spr = LUISprite(obj, video_texture)
spr.color = (1, 1, 1, 1) # Video needs white to show original colors
else:
# 使用默认黑色背景
spr = LUISprite(obj, "blank", "skin")
spr.color = (0.0, 0.0, 0.0, 1.0)
spr.width = width
spr.height = height
if video_texture:
obj.video_texture = video_texture # Prevent GC
# Ensure full UVs
if hasattr(spr, 'set_uv_range'):
spr.set_uv_range(0, 0, 1, 1)
# 如果支持,自动播放
if hasattr(video_texture, 'play'):
# Stop first to reset state, critical for some video formats
if hasattr(video_texture, 'stop'):
video_texture.stop()
video_texture.play()
if hasattr(video_texture, 'setLoop'):
video_texture.setLoop(True)
if audio_sound:
try:
if hasattr(audio_sound, 'stop'):
audio_sound.stop()
if hasattr(audio_sound, 'play'):
audio_sound.play()
except Exception:
pass
# 4. 创建简单的控制栏 (可选,这里先做个占位符)
# TODO: Play/Pause controls
comp_data = {
'type': 'Video',
'object': obj,
'sprite': spr,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"Video_{len(manager.components)}",
'video_path': video_path,
'texture': video_texture,
'audio': audio_sound,
'audio_path': audio_path,
'audio_from_video': audio_from_video,
'volume': 1.0,
'is_playing': True,
'loop': True,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return obj
def create_progressbar(manager, value=50, x=100, y=100, width=200, parent=None):
"""Create a LUI Progressbar"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
pg = LUIProgressbar(parent=parent, width=width, value=value)
pg.left = x
pg.top = y
pg.solid = True # Ensure it receives input
if hasattr(pg, 'set_sort'):
pg.set_sort(1)
comp_data = {
'type': 'Progressbar',
'object': pg,
'left': x,
'top': y,
'width': width,
'height': 30, # default height
'canvas_index': manager.current_canvas_index,
'name': f"Progressbar_{len(manager.components)}",
'value': value,
'sort': 1
}
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return pg
def create_selectbox(manager, options=None, x=100, y=100, width=200, parent=None):
"""Create a LUI Selectbox"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
if options is None:
options = [(0, "Option 1"), (1, "Option 2"), (2, "Option 3")]
sb = LUISelectbox(width=width, options=options, selected_option=0)
sb.parent = parent
sb.left = x
sb.top = y
sb.solid = True # Ensure it receives input
if hasattr(sb, 'set_sort'):
sb.set_sort(1)
comp_data = {
'type': 'Selectbox',
'object': sb,
'left': x,
'top': y,
'width': width,
'height': 30, # default height
'canvas_index': manager.current_canvas_index,
'name': f"Selectbox_{len(manager.components)}",
'options': options,
'sort': 1
}
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return sb
def create_scrollable_region(manager, x=100, y=100, width=200, height=200, parent=None):
"""Create a LUI Scrollable Region"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
sr = LUIScrollableRegion(parent=parent, width=width, height=height)
sr.left = x
sr.top = y
sr.solid = True # Ensure it receives input
if hasattr(sr, 'set_sort'):
sr.set_sort(1)
comp_data = {
'type': 'ScrollableRegion',
'object': sr,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"ScrollableRegion_{len(manager.components)}",
'sort': 1
}
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return sr
def create_tabbed_frame(manager, x=100, y=100, width=300, height=200, parent=None):
"""Create a LUI Tabbed Frame"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
tf = LUITabbedFrame(parent=parent, width=width, height=height)
tf.left = x
tf.top = y
tf.solid = True # Ensure it receives input
# Add default tabs
f1 = LUIFrame(width=width, height=height)
tf.add("Tab 1", f1)
f2 = LUIFrame(width=width, height=height)
tf.add("Tab 2", f2)
if hasattr(tf, 'set_sort'):
tf.set_sort(1)
comp_data = {
'type': 'TabbedFrame',
'object': tf,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"TabbedFrame_{len(manager.components)}",
'sort': 1
}
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return tf
@staticmethod
def delete_component(manager, index):
"""Delete a LUI component and update all references"""
if index < 0 or index >= len(manager.components):
return
# 1. Identify all components to delete (recursive)
to_delete = {index}
queue = list(manager.components[index].get('children_indices', []))
while queue:
curr = queue.pop(0)
if curr not in to_delete:
to_delete.add(curr)
if curr < len(manager.components):
queue.extend(manager.components[curr].get('children_indices', []))
# Cleanup any cached outlines for deleted components
if hasattr(manager, 'debug_outlines') and isinstance(manager.debug_outlines, dict):
for del_idx in list(to_delete):
borders = manager.debug_outlines.pop(del_idx, None)
if borders:
for b in borders:
try:
if hasattr(b, 'remove'):
b.remove()
else:
b.visible = False
except Exception:
pass
# Sort indices descending to delete from end first
sorted_indices = sorted(list(to_delete), reverse=True)
# Clear selection if affected
if manager.selected_index in to_delete:
manager.selected_index = -1
if hasattr(manager, '_hide_resize_handles'):
manager._hide_resize_handles()
# Process deletions
for i in sorted_indices:
if i >= len(manager.components): continue
deleted_comp = manager.components.pop(i)
# Cleanup physical objects
if 'object' in deleted_comp:
obj = deleted_comp['object']
if hasattr(obj, 'parent') and obj.parent:
if hasattr(obj.parent, 'remove_child'):
obj.parent.remove_child(obj)
# Cleanup audio if any
audio = deleted_comp.get("audio")
if audio:
try:
if hasattr(audio, "stop"): audio.stop()
except Exception:
pass
# Cleanup associated node handles if any
if hasattr(manager, 'resize_handles') and manager.resizing_handle and getattr(manager.resizing_handle, '_resize_index', -1) == i:
manager.resizing_handle = None
print(f"✓ Deleted LUI component: {deleted_comp.get('name', 'Unknown')}")
# Shift indices > i down by 1
if manager.selected_index > i:
manager.selected_index -= 1
for c in manager.components:
# Update parent_index
p_idx = c.get('parent_index')
if p_idx is not None and p_idx > i:
c['parent_index'] = p_idx - 1
elif p_idx == i:
c['parent_index'] = -1
# Update children_indices
if 'children_indices' in c:
new_children = []
for child_idx in c['children_indices']:
if child_idx == i: continue
elif child_idx > i: new_children.append(child_idx - 1)
else: new_children.append(child_idx)
c['children_indices'] = new_children
# Reindex remaining outlines after component indices shift
if hasattr(manager, 'debug_outlines') and isinstance(manager.debug_outlines, dict) and manager.debug_outlines:
deleted_sorted = sorted(to_delete)
new_outlines = {}
for old_idx, borders in manager.debug_outlines.items():
shift = 0
for d in deleted_sorted:
if d < old_idx:
shift += 1
else:
break
new_outlines[old_idx - shift] = borders
manager.debug_outlines = new_outlines
def _load_ui_texture(manager, file_path, name_hint="", max_size=1024):
"""Load image as a standalone Panda3D Texture (avoid LUI atlas)."""
try:
panda_path = p3d.Filename.from_os_specific(file_path)
pnm = p3d.PNMImage()
if not pnm.read(panda_path):
print(f"⚠ Failed to read image: {file_path}")
return None
# Ensure alpha exists to avoid unexpected black/transparent results.
if not pnm.hasAlpha():
pnm.addAlpha()
pnm.alphaFill(1.0)
orig_x = pnm.getXSize()
orig_y = pnm.getYSize()
# Power-of-two resize for better compatibility and to avoid atlas issues.
def next_pow2(v):
v = max(1, int(v))
return 1 << (v - 1).bit_length()
pot_x = min(next_pow2(orig_x), max_size)
pot_y = min(next_pow2(orig_y), max_size)
if pot_x != orig_x or pot_y != orig_y:
pnm_scaled = p3d.PNMImage(pot_x, pot_y, pnm.getNumChannels())
pnm_scaled.quickFilterFrom(pnm)
pnm = pnm_scaled
print(f"⚠ Resized to POT: {orig_x}x{orig_y} -> {pot_x}x{pot_y}")
tex = p3d.Texture()
safe_name = f"{name_hint}_{os.path.basename(file_path)}" if name_hint else os.path.basename(file_path)
tex.setName(safe_name)
tex.load(pnm)
tex.setKeepRamImage(True)
tex.setMinfilter(p3d.Texture.FT_linear)
tex.setMagfilter(p3d.Texture.FT_linear)
tex.setWrapU(p3d.Texture.WM_clamp)
tex.setWrapV(p3d.Texture.WM_clamp)
tex.setCompression(p3d.Texture.CM_off)
tex.setQualityLevel(p3d.Texture.QL_best)
return tex
except Exception as e:
print(f"⚠ Texture load failed: {e}")
try:
panda_path = p3d.Filename.from_os_specific(file_path)
return manager.world.loader.loadTexture(panda_path)
except Exception as e2:
print(f"⚠ Texture fallback load failed: {e2}")
return None
def _get_image_atlas_page(manager, size=2048):
"""Get or create an atlas page for UI images."""
if not hasattr(manager, "_image_atlas_pages"):
manager._image_atlas_pages = []
# Try existing pages first
for page in manager._image_atlas_pages:
if page.get("size") == size and not page.get("full", False):
return page
# Create a new page
pnm = p3d.PNMImage(size, size, 4)
pnm.fill(0, 0, 0)
pnm.alphaFill(0.0)
tex = p3d.Texture()
tex.setName(f"ui_atlas_{len(manager._image_atlas_pages)}_{size}")
tex.load(pnm)
tex.setKeepRamImage(True)
tex.setMinfilter(p3d.Texture.FT_linear)
tex.setMagfilter(p3d.Texture.FT_linear)
tex.setWrapU(p3d.Texture.WM_clamp)
tex.setWrapV(p3d.Texture.WM_clamp)
tex.setCompression(p3d.Texture.CM_off)
tex.setQualityLevel(p3d.Texture.QL_best)
page = {
"size": size,
"pnm": pnm,
"tex": tex,
"cursor_x": 0,
"cursor_y": 0,
"row_h": 0,
"full": False,
}
manager._image_atlas_pages.append(page)
return page
def _add_image_to_atlas(manager, file_path, atlas_size=2048):
"""Add image to atlas and return (tex, u0, v0, u1, v1, w, h, orig_w, orig_h)."""
try:
panda_path = p3d.Filename.from_os_specific(file_path)
src = p3d.PNMImage()
if not src.read(panda_path):
print(f"⚠ Failed to read image: {file_path}")
return None
if not src.hasAlpha():
src.addAlpha()
src.alphaFill(1.0)
w = src.getXSize()
h = src.getYSize()
orig_w = w
orig_h = h
# Downscale if too large for atlas
if w > atlas_size or h > atlas_size:
scale = min(atlas_size / max(1, w), atlas_size / max(1, h))
new_w = max(1, int(w * scale))
new_h = max(1, int(h * scale))
scaled = p3d.PNMImage(new_w, new_h, src.getNumChannels())
scaled.quickFilterFrom(src)
src = scaled
w, h = new_w, new_h
print(f"⚠ Downscaled for atlas: {file_path} -> {w}x{h}")
page = manager.luiFunction._get_image_atlas_page(manager, size=atlas_size)
# Shelf packing
if page["cursor_x"] + w > atlas_size:
page["cursor_x"] = 0
page["cursor_y"] += page["row_h"]
page["row_h"] = 0
if page["cursor_y"] + h > atlas_size:
page["full"] = True
# Create a new page and try again
page = manager.luiFunction._get_image_atlas_page(manager, size=atlas_size)
if page["cursor_x"] + w > atlas_size or page["cursor_y"] + h > atlas_size:
print(f"⚠ Atlas full for image: {file_path}")
return None
x = page["cursor_x"]
y = page["cursor_y"]
page["pnm"].copySubImage(src, x, y, 0, 0, w, h)
page["tex"].load(page["pnm"])
page["cursor_x"] += w
page["row_h"] = max(page["row_h"], h)
size = float(atlas_size)
u0 = x / size
v0 = y / size
u1 = (x + w) / size
v1 = (y + h) / size
return (page["tex"], u0, v0, u1, v1, w, h, orig_w, orig_h)
except Exception as e:
print(f"⚠ Atlas insert failed: {e}")
return None
def _draw_component_properties(manager, index):
"""绘制组件属性编辑面板"""
if index < 0 or index >= len(manager.components):
return
comp_data = manager.components[index]
comp_obj = comp_data['object']
comp_type = comp_data['type']
widget = comp_data.get('widget',comp_obj)
# 文本属性
# Text content
# Text content
if comp_type in ['Button', 'Text', 'Label']:
imgui.text("Text Content")
imgui.separator()
if 'text' in comp_data:
imgui.push_item_width(-1)
changed, new_text = imgui.input_text("##text_input", comp_data['text'], 256)
imgui.pop_item_width()
if changed:
comp_data['text'] = new_text
comp_obj.text = new_text
print(f"Text updated: {new_text}")
if comp_type in ['Text', 'Label']:
imgui.spacing()
imgui.text("Font Size")
font_size = int(comp_data.get('font_size', 14))
changed, new_size = imgui.slider_int("##font_size", font_size, 8, 96)
if changed:
comp_data['font_size'] = int(new_size)
try:
if hasattr(comp_obj, '_text') and hasattr(comp_obj._text, 'font_size'):
comp_obj._text.font_size = int(new_size)
if hasattr(comp_obj, '_shadow_text') and hasattr(comp_obj._shadow_text, 'font_size'):
comp_obj._shadow_text.font_size = int(new_size)
if hasattr(comp_obj, 'text_handle') and hasattr(comp_obj.text_handle, 'font_size'):
comp_obj.text_handle.font_size = int(new_size)
except Exception as e:
print(f"Font size update failed: {e}")
imgui.spacing()
imgui.text("Color")
color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0))
color = list(color) if isinstance(color, tuple) else color
changed, new_color = imgui.color_edit4("##color_edit", color)
if changed:
comp_data['color'] = tuple(new_color)
if comp_type == 'Button':
if hasattr(comp_obj, '_layout'):
try:
if hasattr(comp_obj._layout, '_sprite_left'):
comp_obj._layout._sprite_left.color = tuple(new_color)
if hasattr(comp_obj._layout, '_sprite_mid'):
comp_obj._layout._sprite_mid.color = tuple(new_color)
if hasattr(comp_obj._layout, '_sprite_right'):
comp_obj._layout._sprite_right.color = tuple(new_color)
except Exception as e:
print(f"Button color set failed: {e}")
else:
if hasattr(comp_obj, '_text'):
comp_obj._text.color = tuple(new_color)
imgui.separator()
if comp_type == 'Button':
if hasattr(comp_obj, '_layout'):
try:
if hasattr(comp_obj._layout, '_sprite_left'):
comp_obj._layout._sprite_left.color = tuple(new_color)
if hasattr(comp_obj._layout, '_sprite_mid'):
comp_obj._layout._sprite_mid.color = tuple(new_color)
if hasattr(comp_obj._layout, '_sprite_right'):
comp_obj._layout._sprite_right.color = tuple(new_color)
except Exception as e:
print(f"\u8bbe\u7f6e\u6309\u94ae\u989c\u8272\u5931\u8d25: {e}")
else:
if hasattr(comp_obj, '_text'):
comp_obj._text.color = tuple(new_color)
imgui.separator()
imgui.spacing()
imgui.text("\u989c\u8272")
color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0))
if comp_type == 'Button':
imgui.text("\u6309\u94ae\u56fe\u7247")
def _apply_button_textures():
normal_tex = comp_data.get('button_texture_ref_normal') or comp_data.get('button_texture_ref')
normal_uv = comp_data.get('button_atlas_uv_normal') or comp_data.get('button_atlas_uv')
hover_tex = comp_data.get('button_texture_ref_hover')
hover_uv = comp_data.get('button_atlas_uv_hover')
pressed_tex = comp_data.get('button_texture_ref_pressed')
pressed_uv = comp_data.get('button_atlas_uv_pressed')
if hasattr(comp_obj, 'set_custom_textures') and normal_tex is not None:
comp_obj.set_custom_textures(normal_tex, normal_uv, hover_tex, hover_uv, pressed_tex, pressed_uv)
elif hasattr(comp_obj, 'set_custom_texture') and normal_tex is not None:
comp_obj.set_custom_texture(normal_tex, normal_uv)
if imgui.button("\u66f4\u6539\u9ed8\u8ba4\u56fe##button_img_norm", (160, 20)):
selected_path = manager._change_image_texture(
title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6",
filetypes = [
("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
("PNG", "*.png"),
("JPEG", "*.jpg;*.jpeg"),
("\u6240\u6709\u6587\u4ef6", "*.*")
]
)
if selected_path:
try:
atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048)
if atlas_result:
if len(atlas_result) >= 7:
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
else:
tex, u0, v0, u1, v1 = atlas_result
w = int(max(1, round((u1 - u0) * tex.getXSize())))
h = int(max(1, round((v1 - v0) * tex.getYSize())))
comp_data['button_texture_path'] = selected_path
comp_data['button_texture_path_normal'] = selected_path
comp_data['button_atlas_uv'] = (u0, v0, u1, v1)
comp_data['button_atlas_uv_normal'] = (u0, v0, u1, v1)
comp_data['button_atlas_tex'] = tex
comp_data['button_texture_ref'] = tex
comp_data['button_texture_ref_normal'] = tex
comp_data['button_image_size'] = (int(w), int(h))
comp_data['button_image_size_normal'] = (int(w), int(h))
_apply_button_textures()
except Exception as e:
print(f"Button texture set failed: {e}")
if imgui.button("\u66f4\u6539\u60ac\u505c\u56fe##button_img_hover", (160, 20)):
selected_path = manager._change_image_texture(
title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6",
filetypes = [
("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
("PNG", "*.png"),
("JPEG", "*.jpg;*.jpeg"),
("\u6240\u6709\u6587\u4ef6", "*.*")
]
)
if selected_path:
try:
atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048)
if atlas_result:
if len(atlas_result) >= 7:
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
else:
tex, u0, v0, u1, v1 = atlas_result
w = int(max(1, round((u1 - u0) * tex.getXSize())))
h = int(max(1, round((v1 - v0) * tex.getYSize())))
comp_data['button_texture_path_hover'] = selected_path
comp_data['button_atlas_uv_hover'] = (u0, v0, u1, v1)
comp_data['button_texture_ref_hover'] = tex
comp_data['button_image_size_hover'] = (int(w), int(h))
_apply_button_textures()
except Exception as e:
print(f"Button hover texture set failed: {e}")
if imgui.button("\u66f4\u6539\u6309\u4e0b\u56fe##button_img_pressed", (160, 20)):
selected_path = manager._change_image_texture(
title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6",
filetypes = [
("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
("PNG", "*.png"),
("JPEG", "*.jpg;*.jpeg"),
("\u6240\u6709\u6587\u4ef6", "*.*")
]
)
if selected_path:
try:
atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048)
if atlas_result:
if len(atlas_result) >= 7:
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
else:
tex, u0, v0, u1, v1 = atlas_result
w = int(max(1, round((u1 - u0) * tex.getXSize())))
h = int(max(1, round((v1 - v0) * tex.getYSize())))
comp_data['button_texture_path_pressed'] = selected_path
comp_data['button_atlas_uv_pressed'] = (u0, v0, u1, v1)
comp_data['button_texture_ref_pressed'] = tex
comp_data['button_image_size_pressed'] = (int(w), int(h))
_apply_button_textures()
except Exception as e:
print(f"Button pressed texture set failed: {e}")
if imgui.button("\u4f7f\u7528\u9ed8\u8ba4\u56fe\u5c3a\u5bf8##button_fit", (160, 20)):
w = h = None
if 'button_image_size_normal' in comp_data and comp_data['button_image_size_normal']:
try:
w, h = comp_data['button_image_size_normal']
except Exception:
w = h = None
elif 'button_image_size' in comp_data and comp_data['button_image_size']:
try:
w, h = comp_data['button_image_size']
except Exception:
w = h = None
if w is not None and h is not None and w > 0 and h > 0:
comp_data['width'] = float(w)
comp_data['height'] = float(h)
comp_obj.width = float(w)
comp_obj.height = float(h)
if hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
if imgui.button("\u6062\u590d\u9ed8\u8ba4\u6309\u94ae##button_default", (160, 20)):
if hasattr(comp_obj, 'clear_custom_texture'):
comp_obj.clear_custom_texture()
for key in [
'button_texture_path', 'button_texture_path_normal', 'button_texture_path_hover',
'button_texture_path_pressed', 'button_texture_ref', 'button_texture_ref_normal',
'button_texture_ref_hover', 'button_texture_ref_pressed', 'button_atlas_uv',
'button_atlas_uv_normal', 'button_atlas_uv_hover', 'button_atlas_uv_pressed',
'button_image_size', 'button_image_size_normal', 'button_image_size_hover', 'button_image_size_pressed'
]:
if key in comp_data:
comp_data[key] = None
if comp_data.get('button_texture_path_normal'):
imgui.text(f"\u9ed8\u8ba4\u56fe: {comp_data['button_texture_path_normal']}")
if comp_data.get('button_texture_path_hover'):
imgui.text(f"\u60ac\u505c\u56fe: {comp_data['button_texture_path_hover']}")
if comp_data.get('button_texture_path_pressed'):
imgui.text(f"\u6309\u4e0b\u56fe: {comp_data['button_texture_path_pressed']}")
# 布局模式
layout_mode = comp_data.get('layout_mode', 'manual')
is_fill = (layout_mode == 'fill')
changed, new_fill = imgui.checkbox("填充父级", is_fill)
if changed:
comp_data['layout_mode'] = 'fill' if new_fill else 'manual'
if new_fill:
# Fill overrides anchor positioning
comp_data['anchored_to_parent'] = False
comp_data.setdefault('fill_margin_left', 0.0)
comp_data.setdefault('fill_margin_right', 0.0)
comp_data.setdefault('fill_margin_top', 0.0)
comp_data.setdefault('fill_margin_bottom', 0.0)
if hasattr(manager, '_apply_fill_layout'):
manager._apply_fill_layout(index)
is_fill = (comp_data.get('layout_mode') == 'fill')
if is_fill:
imgui.text_disabled("填充模式下,位置/尺寸由父级决定")
imgui.text("边距")
m_left = float(comp_data.get('fill_margin_left', 0.0))
m_right = float(comp_data.get('fill_margin_right', 0.0))
m_top = float(comp_data.get('fill_margin_top', 0.0))
m_bottom = float(comp_data.get('fill_margin_bottom', 0.0))
changed_l, new_l = imgui.input_float("", m_left, 1.0, 10.0, "%.1f")
changed_r, new_r = imgui.input_float("", m_right, 1.0, 10.0, "%.1f")
changed_t, new_t = imgui.input_float("", m_top, 1.0, 10.0, "%.1f")
changed_b, new_b = imgui.input_float("", m_bottom, 1.0, 10.0, "%.1f")
if changed_l or changed_r or changed_t or changed_b:
comp_data['fill_margin_left'] = new_l
comp_data['fill_margin_right'] = new_r
comp_data['fill_margin_top'] = new_t
comp_data['fill_margin_bottom'] = new_b
if hasattr(manager, '_apply_fill_layout'):
manager._apply_fill_layout(index)
imgui.separator()
# 位置属性
imgui.text("位置")
if is_fill:
imgui.text(f"Left: {comp_data.get('left', 0.0):.1f}")
imgui.text(f"Top: {comp_data.get('top', 0.0):.1f}")
else:
if 'left' in comp_data:
changed, new_left = imgui.input_float("Left", comp_data['left'], 1.0, 10.0, "%.1f")
if changed:
comp_data['left'] = new_left
comp_obj.left = new_left
if 'top' in comp_data:
changed, new_top = imgui.input_float("Top", comp_data['top'], 1.0, 10.0, "%.1f")
if changed:
comp_data['top'] = new_top
comp_obj.top = new_top
# 尺寸属性
imgui.spacing()
imgui.text("尺寸")
if is_fill:
imgui.text(f"Width: {comp_data.get('width', 0.0):.1f}")
imgui.text(f"Height: {comp_data.get('height', 0.0):.1f}")
else:
width_changed = False
height_changed = False
if 'width' in comp_data:
changed, new_width = imgui.input_float("Width", comp_data['width'], 1.0, 10.0, "%.1f")
if changed and new_width > 0:
comp_data['width'] = new_width
width_changed = True
if hasattr(comp_obj, 'width'):
comp_obj.width = new_width
# 同步更新内部 Sprite (针对 Image/Plane/Video)
if 'sprite' in comp_data:
comp_data['sprite'].width = new_width
if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
if comp_type == 'Slider' and hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
if comp_type == 'InputField' and hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if 'height' in comp_data:
changed, new_height = imgui.input_float("Height", comp_data['height'], 1.0, 10.0, "%.1f")
if changed and new_height > 0:
comp_data['height'] = new_height
height_changed = True
if hasattr(comp_obj, 'height'):
comp_obj.height = new_height
# 同步更新内部 Sprite (针对 Image/Plane/Video)
if 'sprite' in comp_data:
comp_data['sprite'].height = new_height
if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if (width_changed or height_changed) and hasattr(manager, '_update_anchored_children'):
manager._update_anchored_children(index)
if comp_type in ['VerticalLayout', 'HorizontalLayout'] and hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
# Layout group properties
if comp_type in ['VerticalLayout', 'HorizontalLayout']:
imgui.spacing()
imgui.text("Layout Group")
spacing = float(comp_data.get('layout_spacing', 0.0))
changed, new_spacing = imgui.input_float("Spacing", spacing, 1.0, 10.0, "%.1f")
if changed:
comp_data['layout_spacing'] = new_spacing
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
imgui.text("Padding")
pad_left = float(comp_data.get('layout_padding_left', 0.0))
pad_right = float(comp_data.get('layout_padding_right', 0.0))
pad_top = float(comp_data.get('layout_padding_top', 0.0))
pad_bottom = float(comp_data.get('layout_padding_bottom', 0.0))
changed_l, new_l = imgui.input_float("Pad Left", pad_left, 1.0, 10.0, "%.1f")
changed_r, new_r = imgui.input_float("Pad Right", pad_right, 1.0, 10.0, "%.1f")
changed_t, new_t = imgui.input_float("Pad Top", pad_top, 1.0, 10.0, "%.1f")
changed_b, new_b = imgui.input_float("Pad Bottom", pad_bottom, 1.0, 10.0, "%.1f")
if changed_l or changed_r or changed_t or changed_b:
comp_data['layout_padding_left'] = new_l
comp_data['layout_padding_right'] = new_r
comp_data['layout_padding_top'] = new_t
comp_data['layout_padding_bottom'] = new_b
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
align_options = ["start", "center", "end", "stretch"]
if comp_type in ['HorizontalLayout', 'VerticalLayout']:
wrap_enabled = bool(comp_data.get('layout_wrap', True))
changed, new_wrap = imgui.checkbox("Wrap", wrap_enabled)
if changed:
comp_data['layout_wrap'] = new_wrap
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
line_spacing = float(comp_data.get('layout_line_spacing', 0.0))
changed, new_line_spacing = imgui.input_float("Line Spacing (Wrap)", line_spacing, 1.0, 10.0, "%.1f")
if changed:
comp_data['layout_line_spacing'] = new_line_spacing
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
current_align = comp_data.get('layout_align', 'start')
if imgui.begin_combo("Align", current_align):
for opt in align_options:
if imgui.selectable(opt, current_align == opt)[0]:
comp_data['layout_align'] = opt
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
imgui.end_combo()
# --- 层级与显示顺序 (LUI 中组件层级由 Z-Offset 控制) ---
imgui.spacing()
imgui.text("层级与显示顺序")
# 获取当前深度并确保其在 comp_data 中
current_z = comp_data.get('z_offset')
if current_z is None:
# 兼容旧组件的 sort 属性,如果存在则作为初始 z_offset
current_z = comp_data.get('sort', 0.0)
if current_z == 0 and hasattr(comp_obj, 'z_offset'):
current_z = comp_obj.z_offset
comp_data['z_offset'] = float(current_z)
# 1. 整数层级设置 (方便快速调整)
layer_val = int(current_z)
changed_layer, new_layer = imgui.input_int("渲染层级 (Layer)", layer_val)
if changed_layer:
comp_data['z_offset'] = float(new_layer)
if hasattr(comp_obj, 'set_z_offset'):
comp_obj.set_z_offset(float(new_layer))
print(f"✓ 组件 #{index} 层级已映射到 Z-Offset: {new_layer}")
# 2. 深度微调 (Z-Offset 浮点数)
changed_z, new_z = imgui.input_float("深度微调 (Z-Offset)", comp_data['z_offset'], 0.1, 1.0, "%.2f")
if changed_z:
comp_data['z_offset'] = new_z
if hasattr(comp_obj, 'set_z_offset'):
comp_obj.set_z_offset(new_z)
elif hasattr(comp_obj, 'z_offset'):
comp_obj.z_offset = new_z
print(f"✓ 组件 #{index} Z-Offset 已微调为: {new_z}")
imgui.text_disabled("(注: LUI 内部组件通过 Z-Offset 决定遮挡关系)")
# 特定类型的属性
if comp_type == 'InputField':
imgui.spacing()
imgui.text("输入框属性")
imgui.text("输入框图片")
if imgui.button("更改图片##input_img", (120, 20)):
selected_path = manager._change_image_texture(
title = "选择图片文件",
filetypes = [
("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
("PNG", "*.png"),
("JPEG", "*.jpg;*.jpeg"),
("所有文件", "*.*")
]
)
if selected_path:
try:
atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048)
if atlas_result:
if len(atlas_result) >= 7:
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
else:
tex, u0, v0, u1, v1 = atlas_result
w = int(max(1, round((u1 - u0) * tex.getXSize())))
h = int(max(1, round((v1 - v0) * tex.getYSize())))
comp_data['input_texture_path'] = selected_path
comp_data['input_atlas_uv'] = (u0, v0, u1, v1)
comp_data['input_texture_ref'] = tex
comp_data['input_image_size'] = (int(w), int(h))
layout = getattr(comp_obj, '_layout', None)
if layout is not None:
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
spr = getattr(layout, attr, None)
if spr is not None and hasattr(spr, 'set_texture'):
spr.set_texture(tex, resize=False)
if hasattr(spr, 'set_uv_range'):
spr.set_uv_range(u0, v0, u1, v1)
except Exception as e:
print(f"InputField texture set failed: {e}")
if imgui.button("使用图片尺寸##input_fit", (120, 20)):
w = h = None
if 'input_image_size' in comp_data and comp_data['input_image_size']:
try:
w, h = comp_data['input_image_size']
except Exception:
w = h = None
elif 'input_atlas_uv' in comp_data and 'input_texture_ref' in comp_data and comp_data['input_texture_ref']:
try:
u0, v0, u1, v1 = comp_data['input_atlas_uv']
tex = comp_data['input_texture_ref']
tw = tex.getXSize() if hasattr(tex, 'getXSize') else 0
th = tex.getYSize() if hasattr(tex, 'getYSize') else 0
w = int(max(1, round((u1 - u0) * tw)))
h = int(max(1, round((v1 - v0) * th)))
except Exception:
w = h = None
if w is not None and h is not None and w > 0 and h > 0:
comp_data['width'] = float(w)
comp_data['height'] = float(h)
comp_obj.width = float(w)
comp_obj.height = float(h)
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(float(w))
if hasattr(comp_obj, 'set_height'):
comp_obj.set_height(float(h))
layout = getattr(comp_obj, '_layout', None)
if layout is not None:
if hasattr(layout, 'width'):
layout.width = "100%"
if hasattr(layout, 'height'):
layout.height = "100%"
imgui.text("输入框颜色")
in_color = comp_data.get('input_color', (1.0, 1.0, 1.0, 1.0))
in_color = list(in_color) if isinstance(in_color, tuple) else in_color
changed, new_in_color = imgui.color_edit4("##input_color", in_color)
if changed:
comp_data['input_color'] = tuple(new_in_color)
layout = getattr(comp_obj, '_layout', None)
if layout is not None:
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
spr = getattr(layout, attr, None)
if spr is not None:
spr.color = tuple(new_in_color)
if 'value' in comp_data:
changed, new_value = imgui.input_text("当前值", comp_data['value'], 256)
if changed:
comp_data['value'] = new_value
comp_obj.value = new_value
elif comp_type == 'Slider':
imgui.spacing()
imgui.text("滑块属性")
if 'value' in comp_data:
min_val = comp_data.get('min_value', 0.0)
max_val = comp_data.get('max_value', 100.0)
changed, new_value = imgui.slider_float("当前值", comp_data['value'], min_val, max_val, "%.1f")
if changed:
comp_data['value'] = new_value
if hasattr(comp_obj, 'set_value'):
comp_obj.set_value(new_value)
elif comp_type == 'Checkbox':
imgui.spacing()
imgui.text("复选框属性")
if 'text' in comp_data:
changed, new_text = imgui.input_text("标签文本", comp_data['text'], 256)
if changed:
comp_data['text'] = new_text
# Checkbox uses a child label for text
if hasattr(comp_obj, 'label') and hasattr(comp_obj.label, 'text'):
comp_obj.label.text = new_text
elif hasattr(comp_obj, 'text'):
comp_obj.text = new_text
if 'checked' in comp_data:
changed, new_checked = imgui.checkbox("选中状态", comp_data['checked'])
if changed:
comp_data['checked'] = new_checked
if hasattr(comp_obj, 'checked'):
comp_obj.checked = new_checked
elif comp_type in ['Plane', 'Image']:
imgui.spacing()
imgui.text(f"{comp_type}属性")
# 颜色属性
if 'color' in comp_data:
color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0))
color = list(color) if isinstance(color, tuple) else color
changed, new_color = imgui.color_edit4("颜色", color)
if changed:
comp_data['color'] = tuple(new_color)
# 更新sprite颜色
if 'sprite' in comp_data:
comp_data['sprite'].color = tuple(new_color)
# 如果是Image显示纹理路径
if comp_type == 'Image' and 'texture_path' in comp_data:
# Fit size to image texture
if imgui.button("使用图片尺寸##fit_image"):
tex = None
# Prefer direct texture reference if available
if 'texture_ref' in comp_data and comp_data['texture_ref']:
tex = comp_data['texture_ref']
elif 'texture' in comp_data and comp_data['texture']:
tex = comp_data['texture']
else:
# Try to read from sprite python tag if present
try:
target = comp_data.get('sprite', comp_obj)
if target is not None and hasattr(target, 'get_python_tag'):
tex = target.get_python_tag('texture_ref')
except Exception:
tex = None
w = h = None
if 'image_size' in comp_data and comp_data['image_size']:
try:
w, h = comp_data['image_size']
except Exception:
w = h = None
elif 'atlas_uv' in comp_data and 'atlas_tex' in comp_data and comp_data['atlas_tex']:
try:
u0, v0, u1, v1 = comp_data['atlas_uv']
atlas_tex = comp_data['atlas_tex']
atlas_w = atlas_tex.getXSize() if hasattr(atlas_tex, 'getXSize') else 0
atlas_h = atlas_tex.getYSize() if hasattr(atlas_tex, 'getYSize') else 0
w = int(max(1, round((u1 - u0) * atlas_w)))
h = int(max(1, round((v1 - v0) * atlas_h)))
except Exception:
w = h = None
elif tex is not None:
if hasattr(tex, 'getOrigFileXSize') and hasattr(tex, 'getOrigFileYSize'):
ow = tex.getOrigFileXSize()
oh = tex.getOrigFileYSize()
if ow and oh:
w, h = ow, oh
if w is None or h is None:
if hasattr(tex, 'getXSize') and hasattr(tex, 'getYSize'):
w = tex.getXSize()
h = tex.getYSize()
if w is not None and h is not None and w > 0 and h > 0:
comp_data['width'] = float(w)
comp_data['height'] = float(h)
comp_obj.width = float(w)
comp_obj.height = float(h)
# Sync sprite size (Image/Plane/Video)
if 'sprite' in comp_data and comp_data['sprite']:
spr = comp_data['sprite']
if hasattr(spr, 'set_size'):
spr.set_size(float(w), float(h))
else:
spr.width = float(w)
spr.height = float(h)
if hasattr(manager, '_hide_resize_handles'):
manager._hide_resize_handles()
else:
print("Image size not available: missing texture")
imgui.text(f"纹理路径: {comp_data['texture_path']}")
if imgui.button("更改纹理", (100, 20)):
# 实现纹理更改功能
selected_path = manager._change_image_texture(
title = "选择图片文件",
filetypes = [
("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
("PNG", "*.png"),
("JPEG", "*.jpg;*.jpeg"),
("所有文件", "*.*")
]
)
if selected_path:
new_path = selected_path
comp_data['texture_path'] = new_path
try:
atlas_result = manager.luiFunction._add_image_to_atlas(manager, new_path, atlas_size=2048)
if atlas_result:
if len(atlas_result) >= 7:
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
else:
tex, u0, v0, u1, v1 = atlas_result
w = int(max(1, round((u1 - u0) * tex.getXSize())))
h = int(max(1, round((v1 - v0) * tex.getYSize())))
print(f"? Texture loaded: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}")
comp_data["current_texture"] = os.path.basename(new_path)
comp_data["atlas_uv"] = (u0, v0, u1, v1)
comp_data["atlas_tex"] = tex
comp_data["image_size"] = (int(w), int(h))
old_spr = comp_data.get("sprite")
if old_spr:
try:
old_spr.parent = None
if hasattr(old_spr, "set_texture"): old_spr.set_texture(None)
if hasattr(old_spr, "destroy"): old_spr.destroy()
except:
pass
try:
new_spr = LUISprite(comp_obj, tex)
if hasattr(new_spr, "set_texture"):
new_spr.set_texture(tex, resize=False)
if hasattr(new_spr, "set_uv_range"):
new_spr.set_uv_range(u0, v0, u1, v1)
new_spr.width = comp_data.get("width", 100)
new_spr.height = comp_data.get("height", 100)
new_spr.left = 0
new_spr.top = 0
new_spr.z_offset = comp_data.get("z_offset", 0)
new_spr.color = comp_data.get("color", (1,1,1,1))
if hasattr(new_spr, "set_uv_range"):
new_spr.set_uv_range(u0, v0, u1, v1)
comp_data["sprite"] = new_spr
comp_data["texture_ref"] = tex
if not hasattr(manager, "texture_refs"):
manager.texture_refs = []
manager.texture_refs.append(tex)
if hasattr(new_spr, "set_python_tag"):
new_spr.set_python_tag("texture_ref", tex)
if hasattr(comp_obj, "set_python_tag"):
comp_obj.set_python_tag("texture_ref", tex)
print(f"? Replaced sprite with atlas texture image: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}")
except Exception as ex:
print(f"? Failed to create new sprite: {ex}")
target = comp_data.get("widget", comp_data.get("object"))
if hasattr(target, "set_texture"):
target.set_texture(tex)
else:
print(f"? Texture loading returned None for: {new_path}")
except Exception as e:
print(f"Image texture update failed: {e}")
if 'sprite' in comp_data:
sprite = comp_data['sprite']
imgui.text(f"当前纹理: {comp_data.get('current_texture', 'blank')}")
elif comp_type == 'Frame':
imgui.spacing()
imgui.text("框架属性")
# 框架特有的颜色属性
if 'color' in comp_data:
color = comp_data.get('color', (0.7, 0.7, 0.7, 0.8))
color = list(color) if isinstance(color, tuple) else color
changed, new_color = imgui.color_edit4("背景颜色", color)
if changed:
comp_data['color'] = tuple(new_color)
# 更新Frame颜色
if hasattr(comp_obj, 'set_color'):
comp_obj.set_color(tuple(new_color))
elif comp_type == 'Selectbox':
imgui.spacing()
imgui.text("Selectbox Options")
imgui.separator()
options = comp_data.get('options', []) or []
# normalize to list of (id,label)
normalized = []
for i, opt in enumerate(options):
try:
opt_id, opt_label = opt
except Exception:
opt_id, opt_label = i, str(opt)
normalized.append((opt_id, str(opt_label)))
options = normalized
comp_data['options'] = options
current_selected = comp_data.get('selected_option_id')
if hasattr(comp_obj, 'get_selected_option'):
try:
current_selected = comp_obj.get_selected_option()
except Exception:
pass
opt_labels = [label for _, label in options] if options else ["(empty)"]
opt_ids = [oid for oid, _ in options] if options else [None]
try:
current_index = opt_ids.index(current_selected)
except Exception:
current_index = 0
changed_sel, new_index = imgui.combo("Selected", current_index, opt_labels)
if changed_sel and options:
sel_id = opt_ids[new_index]
comp_data['selected_option_id'] = sel_id
try:
if hasattr(comp_obj, '_select_option'):
comp_obj._select_option(sel_id)
except Exception:
pass
imgui.spacing()
imgui.text("Edit Options")
imgui.separator()
new_labels = []
dirty = False
for i, (opt_id, opt_label) in enumerate(options):
imgui.push_item_width(-40)
changed, new_label = imgui.input_text(f"##opt_label_{i}", opt_label, 128)
imgui.pop_item_width()
if changed:
opt_label = new_label
dirty = True
imgui.same_line()
if imgui.button(f"Delete##opt_del_{i}", (60, 20)):
dirty = True
continue
new_labels.append(opt_label)
if imgui.button("Add Option", (100, 24)):
new_labels.append(f"Option {len(new_labels)+1}")
dirty = True
if dirty:
# rebuild options with sequential ids to avoid duplicates
options = [(i, label) for i, label in enumerate(new_labels)]
comp_data['options'] = options
# keep selection if possible
if options:
if current_selected not in [oid for oid, _ in options]:
current_selected = options[0][0]
comp_data['selected_option_id'] = current_selected
else:
comp_data['selected_option_id'] = None
try:
if hasattr(comp_obj, 'set_options'):
comp_obj.set_options(options)
elif hasattr(comp_obj, 'options'):
comp_obj.options = options
if options and hasattr(comp_obj, '_select_option'):
comp_obj._select_option(comp_data['selected_option_id'])
except Exception as e:
print(f"Selectbox options update failed: {e}")
elif comp_type == 'Video':
imgui.spacing()
imgui.text("视频属性")
imgui.separator()
# Audio
imgui.spacing()
imgui.text("Audio")
imgui.separator()
current_audio = comp_data.get('audio_path', '')
audio_display = os.path.basename(current_audio) if current_audio else ''
imgui.text(f"Current Audio: {audio_display if audio_display else 'None'}")
if imgui.button("Select Audio", (110, 24)):
selected_audio = manager._change_image_texture(
title = "Select Audio File",
filetypes = [("Audio", "*.mp3;*.wav;*.ogg;*.flac;*.m4a"), ("All Files", "*.*")]
)
if selected_audio:
try:
audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(selected_audio))
if audio_sound:
comp_data["audio"] = audio_sound
comp_data["audio_path"] = selected_audio
comp_data["audio_from_video"] = False
if hasattr(audio_sound, "setLoop"):
audio_sound.setLoop(comp_data.get("loop", True))
if hasattr(audio_sound, "setVolume"):
audio_sound.setVolume(comp_data.get("volume", 1.0))
if comp_data.get("is_playing") and hasattr(audio_sound, "play"):
audio_sound.play()
except Exception as e:
print(f"Audio load failed: {e}")
# Volume
vol = comp_data.get("volume", 1.0)
changed, new_vol = imgui.slider_float("Volume", vol, 0.0, 1.0, "%.2f")
if changed:
comp_data["volume"] = new_vol
audio_sound = comp_data.get("audio")
if audio_sound and hasattr(audio_sound, "setVolume"):
audio_sound.setVolume(new_vol)
# 视频源显示与更改 (支持本地文件和 URL 流)
if 'video_path' in comp_data:
current_source = comp_data.get('video_path', '')
is_url = '://' in current_source
# 顶部状态显示
source_display = os.path.basename(current_source) if (current_source and not is_url) else current_source
imgui.text(f"当前源: {source_display if source_display else '未设置'}")
# -- 方式 1: 本地文件 --
if imgui.button("选择本地视频", (110, 24)):
selected_path = manager._change_image_texture(
title = "选择视频文件",
filetypes = [
("视频文件", "*.mp4;*.avi;*.mov;*.mkv;*.webm;*.ogv"),
("所有文件", "*.*")
]
)
if selected_path:
comp_data['_pending_video_source'] = selected_path
# imgui.spacing()
# imgui.text("视频URL")
# imgui.separator()
# # -- 方式 2: URL / Stream --
# # 使用缓存的临时输入,避免每帧刷新导致光标丢失
# url_input_key = f"video_url_input_{index}"
# if url_input_key not in comp_data:
# comp_data[url_input_key] = current_source if is_url else ""
# changed, new_url = imgui.input_text("##vurl", comp_data[url_input_key], 1024)
# if changed:
# comp_data[url_input_key] = new_url
# imgui.same_line()
# if imgui.button("加载 URL", (80, 24)):
# comp_data['_pending_video_source'] = comp_data[url_input_key]
# 统一执行加载逻辑
if '_pending_video_source' in comp_data:
selected_path = comp_data.pop('_pending_video_source')
if selected_path:
comp_data['video_path'] = selected_path
# 重置时长以强制新视频重新探测
comp_data['duration'] = 0.0
print(f"✓ 尝试加载视频源: {selected_path}")
try:
# 重新加载视频纹理
from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData
# Trim trailing spaces which are visible in error logs
selected_path = selected_path.strip()
# Whitelist protocols for FFmpeg
loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file")
loadPrcFileData("", "ffmpeg-show-error #t")
if "://" in selected_path:
print(f"🔍 尝试加载 URL: [{selected_path}]")
try:
# Use MovieVideo for better protocol handling
video_src = MovieVideo.get(Filename(selected_path))
video_texture = MovieTexture("RemoteVideo")
if not video_texture.load(video_src):
print("⚠ MovieVideo 加载失败,回退到 Texture.read...")
if not video_texture.read(Filename(selected_path)):
raise Exception("MovieTexture 无法读取 URL")
except Exception as e:
print(f"⚠ 显式流加载失败: {e},尝试使用 Loader...")
video_texture = manager.world.loader.loadTexture(selected_path)
else:
video_texture = manager.world.loader.loadTexture(Filename.from_os_specific(selected_path))
if video_texture:
# 更新AspectRatio
orig_w = video_texture.getOrigFileXSize()
orig_h = video_texture.getOrigFileYSize()
if orig_w > 0 and orig_h > 0:
ratio = orig_w / orig_h
ratio = orig_w / orig_h
width = comp_data.get('width', 320)
new_height = width / ratio
print(f"✓Loaded Video Texture: {orig_w}x{orig_h}")
# Debug: check threading support
from panda3d.core import ConfigVariableBool
if not ConfigVariableBool("support-threads").getValue():
print("⚠ 警告: support-threads 为 False, 视频播放可能失败!")
if 'sprite' in comp_data:
spr = comp_data['sprite']
# Ensure full UV coordinate usage
if hasattr(spr, 'set_uv_range'):
spr.set_uv_range(0, 0, 1, 1)
# 更新组件高度
comp_data['height'] = new_height
comp_obj.height = new_height
if 'sprite' in comp_data:
comp_data['sprite'].height = new_height
# Re-create sprite to ensure no atlas conflict and clean up old one
if 'sprite' in comp_data:
# Detach old sprite to prevent "ghost" rectangles
old_spr = comp_data['sprite']
if old_spr:
try:
# Try standard LUI detachment
old_spr.parent = None
if hasattr(old_spr, 'hide'):
old_spr.hide()
except Exception as ex:
print(f"⚠ Failed to detach old sprite: {ex}")
# Create new sprite with direct texture
# Note: LUISprite(parent, texture) is valid if texture is a Texture object
new_spr = LUISprite(comp_obj, video_texture)
# Force set texture again with resize=False to ensure binding
if hasattr(new_spr, 'set_texture'):
new_spr.set_texture(video_texture, resize=False)
new_spr.width = width
new_spr.height = new_height
# Video needs white color to modulate correctly with texture
new_spr.color = (1, 1, 1, 1)
new_spr.z_offset = 0
# Ensure UVs are full frame
if hasattr(new_spr, 'set_uv_range'):
new_spr.set_uv_range(0, 0, 1, 1)
comp_data['sprite'] = new_spr
# WORKAROUND: Force texture update by adding a dummy node to scene graph
# LUI alone might not trigger MovieTexture updates in some pipeline configs
if 'keep_alive' in comp_data and comp_data['keep_alive']:
try:
comp_data['keep_alive'].destroy()
except:
pass
try:
from direct.gui.OnscreenImage import OnscreenImage
from panda3d.core import TransparencyAttrib
# Create a nearly invisible card (alpha=0.01) to force update
# We position it at (0,0,0) with a small but visible scale to prevent culling
dummy = OnscreenImage(image=video_texture, parent=manager.world.render2d)
dummy.setScale(0.1)
dummy.setPos(0, 0, 0)
dummy.setTransparency(TransparencyAttrib.MAlpha)
dummy.setColorScale(1, 1, 1, 0.01) # Nearly invisible
# Ensure it's behind other UI if possible, though in render2d everything is overlay
dummy.setBin("background", -10)
comp_data['keep_alive'] = dummy
print("✓ Created keep-alive node for video")
except Exception as e:
print(f"⚠ Keep-alive creation failed: {e}")
# 更新引用
comp_data['texture'] = video_texture
comp_obj.video_texture = video_texture # Prevent GC
if comp_data.get("audio_from_video"):
old_audio = comp_data.get("audio")
if old_audio and hasattr(old_audio, "stop"):
old_audio.stop()
try:
audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(selected_path))
if audio_sound:
comp_data["audio"] = audio_sound
comp_data["audio_path"] = selected_path
if hasattr(audio_sound, "setLoop"):
audio_sound.setLoop(comp_data.get("loop", True))
if hasattr(audio_sound, "setVolume"):
audio_sound.setVolume(comp_data.get("volume", 1.0))
if comp_data.get("is_playing") and hasattr(audio_sound, "play"):
audio_sound.play()
except Exception as e:
print(f"Audio load failed: {e}")
if hasattr(video_texture, 'play'):
if hasattr(video_texture, 'stop'):
video_texture.stop() # Reset
video_texture.play()
if hasattr(video_texture, 'setLoop'):
video_texture.setLoop(comp_data.get("loop", True))
comp_data['is_playing'] = True
except Exception as e:
print(f"⚠ 加载视频失败: {e}")
# 播放控制
imgui.spacing()
imgui.text("播放控制")
video_tex = comp_data.get('texture')
audio_sound = comp_data.get('audio')
if video_tex:
# Play/Pause Button
is_playing = comp_data.get('is_playing', False)
if imgui.button("暂停" if is_playing else "播放", (60, 24)):
if is_playing:
if hasattr(video_tex, 'stop'): video_tex.stop()
if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop()
comp_data['is_playing'] = False
else:
if hasattr(video_tex, 'play'): video_tex.play()
if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play()
comp_data['is_playing'] = True
imgui.same_line()
if imgui.button("重播", (60, 24)):
if hasattr(video_tex, 'stop'): video_tex.stop()
if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop()
if hasattr(video_tex, 'play'): video_tex.play()
if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play()
comp_data['is_playing'] = True
# Loop Checkbox
loop = comp_data.get('loop', True)
changed, new_loop = imgui.checkbox("循环播放", loop)
if changed:
comp_data['loop'] = new_loop
if hasattr(video_tex, 'setLoop'):
video_tex.setLoop(new_loop)
if audio_sound and hasattr(audio_sound, 'setLoop'): audio_sound.setLoop(new_loop)
# Time Display
if hasattr(video_tex, 'getTime'):
t = video_tex.getTime()
# 检查是否需要循环播放
duration = comp_data.get('duration', 0.0)
if duration > 0 and t >= duration:
# 播放时间超过总时长,重置到开头
if hasattr(video_tex, 'setTime'):
video_tex.setTime(0.0)
t = 0.0
print(f"✓ 视频播放完毕,重新开始循环播放")
imgui.text(f"当前时间: {t:.2f}s")
# Force refresh if stuck (hack)
# if t == 0.0 and comp_data.get('is_playing'):
# video_tex.play()
# imgui.separator()
# imgui.text("调试工具")
# if imgui.button("在Render2D中测试播放"):
# # Create a standard OnscreenImage to test playback capability
# from direct.gui.OnscreenImage import OnscreenImage
# from panda3d.core import TransparencyAttrib
# # Remove previous test if any
# if hasattr(manager, '_debug_video_node') and manager._debug_video_node:
# manager._debug_video_node.destroy()
# manager._debug_video_node = OnscreenImage(image=video_tex, pos=(0, 0, 0), scale=0.5, parent=manager.world.render2d)
# manager._debug_video_node.setTransparency(TransparencyAttrib.MAlpha)
# print(f"Debug: Created OnscreenImage with texture {video_tex}")
# if imgui.button("清除Render2D测试"):
# if hasattr(manager, '_debug_video_node') and manager._debug_video_node:
# manager._debug_video_node.destroy()
# manager._debug_video_node = None
# print("Debug: Cleared OnscreenImage")
# Time Display and Control
if hasattr(video_tex, 'getTime'):
current_time = video_tex.getTime()
if 'duration' not in comp_data or comp_data['duration'] <= 0.0:
# Attempt to detect duration automatically
detected_duration = 0.0
# Method 1: standard MovieTexture interface (if available)
if hasattr(video_tex, 'getVideoLength'):
detected_duration = video_tex.getVideoLength()
# Method 2: check if it has a 'length' property
elif hasattr(video_tex, 'length'):
detected_duration = video_tex.length
if detected_duration > 0:
comp_data['duration'] = detected_duration
print(f"✓ Automatically detected video duration: {detected_duration}s")
# Manual Duration Input (Essential if auto-detection fails)
# We only show this if we really don't know the duration
current_dur = comp_data.get('duration', 0.0)
if current_dur <= 0.1:
imgui.text_colored((1, 1, 0, 1), "⚠未知时长,请手动设置:")
changed, new_dur = imgui.input_float("总时长(s)", current_dur, 1.0, 10.0, "%.1f")
if changed:
comp_data['duration'] = new_dur
duration = comp_data.get('duration', 0.0)
# 检查是否需要循环播放
if duration > 0 and current_time >= duration:
# 播放时间超过总时长,重置到开头
if hasattr(video_tex, 'setTime'):
video_tex.setTime(0.0)
current_time = 0.0
print(f"✓ 视频播放完毕,重新开始循环播放")
# If we still don't have a valid duration, use a fallback for display but warn user
# REVERT: Do not auto-extend duration, it causes confusion. Trust detection or user.
display_max = duration if duration > 0 else max(current_time + 10.0, 60.0)
# Format function for time
def format_time(seconds):
m = int(seconds // 60)
s = int(seconds % 60)
return f"{m:02d}:{s:02d}"
# Progress Slider
imgui.text(f"进度: {format_time(current_time)} / {format_time(display_max)}")
imgui.same_line()
# Use push_item_width to make it fit
imgui.push_item_width(-1)
# Use a custom format for the slider to show seconds, but maybe we can show MM:SS in the text
changed, seek_time = imgui.slider_float("##seek_slider", current_time, 0.0, display_max, "%.2fs")
imgui.pop_item_width()
if changed:
if hasattr(video_tex, 'setTime'):
video_tex.setTime(seek_time)
if audio_sound and hasattr(audio_sound, 'setTime'): audio_sound.setTime(seek_time)
# If paused, we might need to show the frame.
# Usually setTime works.
print(f"Seek to {seek_time}")
# Update cached duration if we find a way, or if user inputs it?
# For now just show time
# imgui.text(f"时间: {current_time:.2f}s") # Already shown in slider
else:
imgui.text_colored((1, 0, 0, 1), "无有效视频纹理")
# 锚点设置
imgui.spacing()
imgui.separator()
imgui.text("锚点设置")
parent_index = comp_data.get('parent_index')
has_parent = (parent_index is not None and parent_index >= 0 and parent_index < len(manager.components))
# 允许对父组件或Canvas进行锚点设置
if has_parent or manager.current_canvas_index >= 0:
is_anchored = comp_data.get('anchored_to_parent', False)
anchor_pos = comp_data.get('anchor_position', '未设置')
if has_parent:
parent_comp = manager.components[parent_index]
imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"父组件: {parent_comp['type']}")
else:
imgui.text_colored((0.0, 1.0, 0.0, 1.0), "父容器: Canvas")
if is_anchored:
imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"当前锚点: {anchor_pos}")
# 锚点位置选择器 - 9宫格布局
imgui.spacing()
imgui.text("锚点位置:")
# 定义9个锚点位置
anchor_positions = [
['top-left', 'top-center', 'top-right'],
['middle-left', 'center', 'middle-right'],
['bottom-left', 'bottom-center', 'bottom-right']
]
# 中文显示名称
anchor_names = {
'top-left': '左上', 'top-center': '中上', 'top-right': '右上',
'middle-left': '左中', 'center': '中心', 'middle-right': '右中',
'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下'
}
# 绘制3x3网格按钮
for row_idx, row in enumerate(anchor_positions):
for col_idx, pos in enumerate(row):
# 检查是否是当前选中的锚点
is_current = (anchor_pos == pos)
# 如果是当前锚点,使用不同颜色
if is_current:
imgui.push_style_color(imgui.Col_.button, (0.2, 0.7, 0.2, 1.0))
imgui.push_style_color(imgui.Col_.button_hovered, (0.3, 0.8, 0.3, 1.0))
imgui.push_style_color(imgui.Col_.button_active, (0.1, 0.6, 0.1, 1.0))
# 绘制按钮
button_size = (50, 25)
if imgui.button(f"{anchor_names[pos]}##anchor_{pos}", button_size):
# 更新锚点位置
manager._update_component_anchor_position(index, pos)
if is_current:
imgui.pop_style_color(3)
# 同一行的按钮在同一行显示
if col_idx < len(row) - 1:
imgui.same_line()
imgui.spacing()
# 锚点偏移微调
imgui.text("位置微调:")
anchor_offset_x = comp_data.get('anchor_manual_offset_x', 0.0)
anchor_offset_y = comp_data.get('anchor_manual_offset_y', 0.0)
changed_x, new_offset_x = imgui.input_float("X偏移", anchor_offset_x, 1.0, 10.0, "%.1f")
if changed_x:
comp_data['anchor_manual_offset_x'] = new_offset_x
manager._update_component_anchor_position(index, anchor_pos, manual_offset=(new_offset_x, anchor_offset_y))
changed_y, new_offset_y = imgui.input_float("Y偏移", anchor_offset_y, 1.0, 10.0, "%.1f")
if changed_y:
comp_data['anchor_manual_offset_y'] = new_offset_y
manager._update_component_anchor_position(index, anchor_pos, manual_offset=(anchor_offset_x, new_offset_y))
imgui.spacing()
# 取消锚点按钮
if imgui.button("取消锚点", (100, 20)):
comp_data['anchored_to_parent'] = False
comp_data['anchor_manual_offset_x'] = 0.0
comp_data['anchor_manual_offset_y'] = 0.0
print("✓ 已取消组件的锚点关系")
else:
imgui.text_colored((1.0, 0.0, 0.0, 1.0), "未使用锚点定位")
if imgui.button("设置锚点", (100, 20)):
imgui.open_popup("选择锚点位置")
manager._temp_selected_index_for_anchor = index
else:
imgui.text_disabled("无法设置锚点 (无父组件且无有效Canvas)")
# Hierarchy & Parent Management
imgui.spacing()
imgui.text("层级与父级管理")
imgui.separator()
current_parent_index = comp_data.get('parent_index')
parent_text = "ROOT (无父组件)" if current_parent_index is None or current_parent_index < 0 else f"{manager.components[current_parent_index]['type']} #{current_parent_index}"
imgui.text(f"当前父级: {parent_text}")
# Create list of potential parents (exclude self and any children to avoid circular loops)
def get_all_descendants(idx):
desc = set()
comp = manager.components[idx]
for c_idx in comp.get('children_indices', []):
desc.add(c_idx)
desc.update(get_all_descendants(c_idx))
return desc
descendants = get_all_descendants(index)
parent_options = ["(无 / ROOT)"]
parent_indices = [-1]
for i, comp in enumerate(manager.components):
if i != index and i not in descendants:
parent_options.append(f"{comp['type']} #{i}: {comp.get('name', '')}")
parent_indices.append(i)
# Current selection in combo
try:
current_p_select = parent_indices.index(current_parent_index if current_parent_index is not None else -1)
except:
current_p_select = 0
changed, new_p_select = imgui.combo("更改父级", current_p_select, parent_options)
if changed:
new_parent_idx = parent_indices[new_p_select]
if new_parent_idx == -1:
# Unparent to Root
if current_parent_index is not None and current_parent_index >= 0:
# Remove from old parent
old_p = manager.components[current_parent_index]
if index in old_p.get('children_indices', []):
old_p['children_indices'].remove(index)
comp_data['parent_index'] = -1
# Physical reparent to Canvas or Root
if manager.current_canvas_index >= 0:
canvas_panel = manager.canvases[manager.current_canvas_index]['panel']
comp_obj.reparent_to(canvas_panel)
else:
# Fallback to overlay root
comp_obj.reparent_to(manager.overlay_root)
print(f"✓ 已将组件 #{index} 设为根节点")
else:
# Set New Parent
manager._set_parent_child_relationship(index, new_parent_idx, keep_world=True)
print(f"✓ 已更改父级: 组件 #{index} -> 父级 #{new_parent_idx}")
# 删除按钮
imgui.spacing()
imgui.separator()
imgui.push_style_color(imgui.Col_.button, (0.8, 0.2, 0.2, 1.0))
imgui.push_style_color(imgui.Col_.button_hovered, (0.9, 0.3, 0.3, 1.0))
imgui.push_style_color(imgui.Col_.button_active, (0.7, 0.1, 0.1, 1.0))
if imgui.button("删除组件", (-1, 30)):
manager.luiFunction.delete_component(manager,index)
imgui.pop_style_color(3)
# Draw the anchor popup if requested
manager._handle_anchor_popup()