EG/ui/LUI/lui_function_properties.py
2026-02-27 16:52:00 +08:00

1708 lines
87 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""luiFunction component property drawing mixin."""
import os
from pathlib import Path
import panda3d.core as p3d
from imgui_bundle import imgui, imgui_ctx
class LUIFunctionPropertiesMixin:
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
# Keep async HTTP results synchronized even when not in dedicated task loop.
if hasattr(manager.luiFunction, 'update_http_components'):
try:
manager.luiFunction.update_http_components(manager)
except Exception:
pass
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(float(comp_data.get('height', 16)))
if comp_type == 'InputField' and hasattr(comp_obj, 'set_height'):
comp_obj.set_height(float(comp_data.get('height', 24)))
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(float(comp_data.get('width', 200)))
if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(float(comp_data.get('width', 200)))
if width_changed or height_changed:
if comp_type == 'HttpText' and hasattr(manager.luiFunction, 'sync_http_text_layout'):
manager.luiFunction.sync_http_text_layout(manager, comp_data)
if 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 == 'HttpText':
imgui.spacing()
imgui.text("HTTP通信")
imgui.separator()
http_url = str(comp_data.get('http_url', ''))
changed_url, new_url = imgui.input_text("URL", http_url, 512)
if changed_url:
comp_data['http_url'] = new_url
method_options = ["GET", "POST"]
curr_method = str(comp_data.get('http_method', 'GET')).upper()
curr_method_idx = 1 if curr_method == "POST" else 0
changed_method, new_method_idx = imgui.combo("Method", curr_method_idx, method_options)
if changed_method:
comp_data['http_method'] = method_options[new_method_idx]
timeout_val = float(comp_data.get('http_timeout', 8.0))
changed_timeout, new_timeout = imgui.input_float("Timeout(s)", timeout_val, 1.0, 5.0, "%.1f")
if changed_timeout:
comp_data['http_timeout'] = max(1.0, new_timeout)
auto_refresh = bool(comp_data.get('auto_refresh', True))
changed_auto, new_auto = imgui.checkbox("自动刷新", auto_refresh)
if changed_auto:
comp_data['auto_refresh'] = new_auto
interval_val = float(comp_data.get('refresh_interval', 60.0))
changed_interval, new_interval = imgui.input_float("刷新间隔(s)", interval_val, 1.0, 10.0, "%.1f")
if changed_interval:
comp_data['refresh_interval'] = max(1.0, new_interval)
json_path = str(comp_data.get('http_json_path', ''))
changed_path, new_path = imgui.input_text("JSON路径(可选)", json_path, 256)
if changed_path:
comp_data['http_json_path'] = new_path
headers_text = str(comp_data.get('http_headers', '{}'))
changed_headers, new_headers = imgui.input_text("Headers(JSON)", headers_text, 512)
if changed_headers:
comp_data['http_headers'] = new_headers
body_text = str(comp_data.get('http_body', ''))
changed_body, new_body = imgui.input_text("Body", body_text, 512)
if changed_body:
comp_data['http_body'] = new_body
max_chars = int(comp_data.get('max_chars', 300))
changed_max, new_max = imgui.input_int("最大显示字符", max_chars)
if changed_max:
comp_data['max_chars'] = max(32, int(new_max))
imgui.text(f"状态: {comp_data.get('http_status', '未请求')}")
last_error = comp_data.get('last_error', '')
if last_error:
imgui.text_colored((1.0, 0.4, 0.4, 1.0), f"错误: {last_error}")
if comp_data.get('_http_inflight'):
imgui.text("请求中...")
if imgui.button("北京天气示例", (120, 24)):
comp_data['http_url'] = "https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m&timezone=Asia%2FShanghai"
comp_data['http_method'] = "GET"
comp_data['http_json_path'] = "current"
comp_data['http_headers'] = "{}"
comp_data['http_body'] = ""
comp_data['refresh_interval'] = 60.0
comp_data['auto_refresh'] = True
imgui.same_line()
if imgui.button("立即请求", (100, 24)):
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
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()