2557 lines
116 KiB
Python
2557 lines
116 KiB
Python
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()
|