1248 lines
45 KiB
Python
1248 lines
45 KiB
Python
"""luiFunction component and HTTP creation mixin."""
|
||
|
||
import json
|
||
import threading
|
||
import time
|
||
import urllib.error
|
||
import urllib.request
|
||
from pathlib import Path
|
||
|
||
import panda3d.core as p3d
|
||
from panda3d.core import CardMaker, NodePath
|
||
from imgui_bundle import imgui, imgui_ctx
|
||
|
||
from ui.LUI.lui_shared import (
|
||
LUICheckbox,
|
||
LUIFrame,
|
||
LUIHorizontalLayout,
|
||
LUIButton,
|
||
LUILabel,
|
||
LUIInputField,
|
||
LUISlider,
|
||
LUISprite,
|
||
LUIObject,
|
||
LUIProgressbar,
|
||
LUISelectbox,
|
||
LUIScrollableRegion,
|
||
LUITabbedFrame,
|
||
LUIVerticalLayout,
|
||
)
|
||
|
||
|
||
class LUIFunctionComponentsMixin:
|
||
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_http_text(manager, x=100, y=100, width=320, height=120, url="https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m&timezone=Asia%2FShanghai", parent=None):
|
||
"""Create a HTTP text component that fetches data from URL and renders it."""
|
||
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
|
||
|
||
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)
|
||
|
||
# Panel background
|
||
spr = LUISprite(obj, "blank", "skin")
|
||
spr.width = width
|
||
spr.height = height
|
||
spr.left = 0
|
||
spr.top = 0
|
||
spr.color = (0.08, 0.14, 0.20, 0.92)
|
||
|
||
# Text content
|
||
text_label = LUILabel(
|
||
parent=obj,
|
||
text="HTTP组件\n点击“立即请求”获取数据",
|
||
x=8,
|
||
y=8,
|
||
font_size=14,
|
||
wordwrap=True
|
||
)
|
||
text_label.color = (0.92, 0.95, 1.0, 1.0)
|
||
|
||
comp_data = {
|
||
'type': 'HttpText',
|
||
'object': obj,
|
||
'sprite': spr,
|
||
'text_label': text_label,
|
||
'text': "HTTP组件\n点击“立即请求”获取数据",
|
||
'left': x,
|
||
'top': y,
|
||
'width': width,
|
||
'height': height,
|
||
'canvas_index': manager.current_canvas_index,
|
||
'name': f"HttpText_{len(manager.components)}",
|
||
'sort': 1,
|
||
'color': (0.08, 0.14, 0.20, 0.92),
|
||
'text_color': (0.92, 0.95, 1.0, 1.0),
|
||
'error_color': (1.0, 0.40, 0.40, 1.0),
|
||
'text_padding': 8.0,
|
||
# HTTP settings
|
||
'http_url': url,
|
||
'http_method': 'GET',
|
||
'http_headers': '{}',
|
||
'http_body': '',
|
||
'http_timeout': 8.0,
|
||
'http_json_path': 'current',
|
||
'auto_refresh': True,
|
||
'refresh_interval': 60.0,
|
||
'max_chars': 300,
|
||
'http_status': '未请求',
|
||
'last_error': '',
|
||
'last_response_raw': '',
|
||
'last_refresh_time': 0.0,
|
||
# Runtime flags
|
||
'_http_inflight': False,
|
||
'_http_pending_result': None,
|
||
}
|
||
|
||
manager.luiFunction.sync_http_text_layout(manager, comp_data)
|
||
|
||
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)
|
||
|
||
# Trigger first request in background
|
||
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
|
||
return obj
|
||
|
||
def sync_http_text_layout(manager, comp_data):
|
||
"""Keep HttpText internal label in sync with panel size and padding."""
|
||
if comp_data.get('type') != 'HttpText':
|
||
return
|
||
|
||
label = comp_data.get('text_label')
|
||
if label is None:
|
||
return
|
||
|
||
pad = float(comp_data.get('text_padding', 8.0))
|
||
width = float(comp_data.get('width', 100.0))
|
||
height = float(comp_data.get('height', 40.0))
|
||
inner_w = max(1.0, width - pad * 2.0)
|
||
inner_h = max(1.0, height - pad * 2.0)
|
||
|
||
try:
|
||
label.left = pad
|
||
label.top = pad
|
||
if hasattr(label, 'width'):
|
||
label.width = inner_w
|
||
if hasattr(label, 'height'):
|
||
label.height = inner_h
|
||
except Exception:
|
||
pass
|
||
|
||
def _extract_json_by_path(path_text, payload):
|
||
"""Extract value from JSON payload by a simple path: a.b[0].c"""
|
||
if not path_text:
|
||
return payload
|
||
|
||
path = str(path_text).strip()
|
||
if not path:
|
||
return payload
|
||
|
||
tokens = []
|
||
current = ""
|
||
i = 0
|
||
while i < len(path):
|
||
ch = path[i]
|
||
if ch == '.':
|
||
if current:
|
||
tokens.append(('key', current))
|
||
current = ""
|
||
i += 1
|
||
continue
|
||
if ch == '[':
|
||
if current:
|
||
tokens.append(('key', current))
|
||
current = ""
|
||
j = path.find(']', i + 1)
|
||
if j < 0:
|
||
raise ValueError(f"JSON路径非法: {path_text}")
|
||
idx_text = path[i + 1:j].strip()
|
||
if not idx_text:
|
||
raise ValueError(f"JSON路径非法: {path_text}")
|
||
tokens.append(('index', int(idx_text)))
|
||
i = j + 1
|
||
continue
|
||
current += ch
|
||
i += 1
|
||
|
||
if current:
|
||
tokens.append(('key', current))
|
||
|
||
node = payload
|
||
for token_type, token_val in tokens:
|
||
if token_type == 'key':
|
||
if not isinstance(node, dict):
|
||
raise KeyError(f"JSON路径错误,期望对象: {token_val}")
|
||
node = node[token_val]
|
||
else:
|
||
if not isinstance(node, (list, tuple)):
|
||
raise KeyError(f"JSON路径错误,期望数组: [{token_val}]")
|
||
node = node[token_val]
|
||
|
||
return node
|
||
|
||
def _set_http_text_display(manager, comp_data, text, is_error=False):
|
||
"""Update HttpText display text and color."""
|
||
msg = text if text is not None else ""
|
||
comp_data['text'] = msg
|
||
label = comp_data.get('text_label')
|
||
if label is None:
|
||
return
|
||
|
||
try:
|
||
label.text = msg
|
||
if is_error:
|
||
color = comp_data.get('error_color', (1.0, 0.4, 0.4, 1.0))
|
||
else:
|
||
color = comp_data.get('text_color', (0.92, 0.95, 1.0, 1.0))
|
||
if hasattr(label, 'set_color'):
|
||
label.set_color(color)
|
||
else:
|
||
label.color = color
|
||
except Exception:
|
||
pass
|
||
|
||
def trigger_http_request(manager, comp_data, force=False):
|
||
"""Start one async HTTP request for HttpText component."""
|
||
if comp_data.get('type') != 'HttpText':
|
||
return False
|
||
|
||
if comp_data.get('_http_inflight'):
|
||
return False
|
||
|
||
now_ts = time.time()
|
||
if not force:
|
||
interval = max(1.0, float(comp_data.get('refresh_interval', 60.0)))
|
||
last_ts = float(comp_data.get('last_refresh_time', 0.0))
|
||
if now_ts - last_ts < interval:
|
||
return False
|
||
|
||
url = str(comp_data.get('http_url', '')).strip()
|
||
if not url:
|
||
comp_data['http_status'] = "URL为空"
|
||
comp_data['last_error'] = "URL为空"
|
||
manager.luiFunction._set_http_text_display(manager, comp_data, "HTTP请求失败: URL为空", is_error=True)
|
||
return False
|
||
|
||
method = str(comp_data.get('http_method', 'GET')).upper().strip()
|
||
if method not in ('GET', 'POST'):
|
||
method = 'GET'
|
||
comp_data['http_method'] = 'GET'
|
||
|
||
timeout_sec = max(1.0, float(comp_data.get('http_timeout', 8.0)))
|
||
headers_raw = str(comp_data.get('http_headers', '{}'))
|
||
body_raw = str(comp_data.get('http_body', ''))
|
||
json_path = str(comp_data.get('http_json_path', '')).strip()
|
||
max_chars = max(32, int(comp_data.get('max_chars', 300)))
|
||
|
||
comp_data['_http_inflight'] = True
|
||
comp_data['http_status'] = "请求中..."
|
||
|
||
def _worker():
|
||
result = {
|
||
'ok': False,
|
||
'message': '',
|
||
'status_code': None,
|
||
'error': '',
|
||
'time': time.time(),
|
||
'raw': '',
|
||
}
|
||
try:
|
||
headers = {}
|
||
if headers_raw.strip():
|
||
parsed_headers = json.loads(headers_raw)
|
||
if not isinstance(parsed_headers, dict):
|
||
raise ValueError("Headers 必须是 JSON 对象")
|
||
headers = {str(k): str(v) for k, v in parsed_headers.items()}
|
||
|
||
data_bytes = None
|
||
if body_raw:
|
||
data_bytes = body_raw.encode('utf-8')
|
||
|
||
request = urllib.request.Request(
|
||
url=url,
|
||
data=data_bytes,
|
||
headers=headers,
|
||
method=method
|
||
)
|
||
|
||
with urllib.request.urlopen(request, timeout=timeout_sec) as response:
|
||
status_code = getattr(response, 'status', 200)
|
||
raw_bytes = response.read()
|
||
charset = None
|
||
response_headers = getattr(response, 'headers', None)
|
||
if response_headers is not None and hasattr(response_headers, 'get_content_charset'):
|
||
charset = response_headers.get_content_charset()
|
||
if not charset:
|
||
charset = "utf-8"
|
||
raw_text = raw_bytes.decode(charset, errors='replace')
|
||
|
||
display_text = raw_text.strip()
|
||
if json_path:
|
||
json_payload = json.loads(raw_text)
|
||
extracted = manager.luiFunction._extract_json_by_path(json_path, json_payload)
|
||
if isinstance(extracted, (dict, list)):
|
||
display_text = json.dumps(extracted, ensure_ascii=False)
|
||
else:
|
||
display_text = str(extracted)
|
||
|
||
if len(display_text) > max_chars:
|
||
display_text = display_text[:max_chars] + "..."
|
||
|
||
result['ok'] = True
|
||
result['message'] = display_text
|
||
result['status_code'] = status_code
|
||
result['raw'] = raw_text
|
||
except Exception as err:
|
||
result['error'] = str(err)
|
||
|
||
comp_data['_http_pending_result'] = result
|
||
|
||
thread = threading.Thread(target=_worker, daemon=True)
|
||
thread.start()
|
||
return True
|
||
|
||
def update_http_components(manager):
|
||
"""Consume async HTTP results and run auto-refresh."""
|
||
now_ts = time.time()
|
||
for comp_data in manager.components:
|
||
if comp_data.get('type') != 'HttpText':
|
||
continue
|
||
|
||
pending = comp_data.get('_http_pending_result')
|
||
if pending is not None:
|
||
comp_data['_http_pending_result'] = None
|
||
comp_data['_http_inflight'] = False
|
||
comp_data['last_refresh_time'] = float(pending.get('time', now_ts))
|
||
comp_data['last_response_raw'] = pending.get('raw', '')
|
||
|
||
if pending.get('ok'):
|
||
status_code = pending.get('status_code')
|
||
stamp = time.strftime("%H:%M:%S", time.localtime(comp_data['last_refresh_time']))
|
||
comp_data['http_status'] = f"HTTP {status_code} @ {stamp}"
|
||
comp_data['last_error'] = ''
|
||
manager.luiFunction._set_http_text_display(
|
||
manager,
|
||
comp_data,
|
||
pending.get('message', ''),
|
||
is_error=False
|
||
)
|
||
else:
|
||
error_msg = pending.get('error', '未知错误')
|
||
comp_data['last_error'] = error_msg
|
||
comp_data['http_status'] = "请求失败"
|
||
manager.luiFunction._set_http_text_display(
|
||
manager,
|
||
comp_data,
|
||
f"HTTP请求失败: {error_msg}",
|
||
is_error=True
|
||
)
|
||
|
||
# Auto refresh
|
||
if comp_data.get('auto_refresh', False) and not comp_data.get('_http_inflight'):
|
||
interval = max(1.0, float(comp_data.get('refresh_interval', 60.0)))
|
||
last_ts = float(comp_data.get('last_refresh_time', 0.0))
|
||
if now_ts - last_ts >= interval:
|
||
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
|
||
|
||
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
|