1779 lines
81 KiB
Python
1779 lines
81 KiB
Python
"""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):
|
||
"""Orchestrator entry for component property rendering."""
|
||
return manager.luiFunction._draw_component_properties_core(manager, index)
|
||
|
||
def _draw_component_properties_core(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
|
||
manager.luiFunction._draw_text_and_button_properties(
|
||
manager, comp_data, comp_obj, comp_type
|
||
)
|
||
|
||
manager.luiFunction._draw_layout_transform_properties(
|
||
manager, index, comp_data, comp_obj, comp_type
|
||
)
|
||
|
||
manager.luiFunction._draw_type_specific_properties(
|
||
manager, index, comp_data, comp_obj, comp_type
|
||
)
|
||
manager.luiFunction._draw_anchor_and_hierarchy_properties(
|
||
manager, index, comp_data, comp_obj
|
||
)
|
||
|
||
def _draw_text_and_button_properties(manager, comp_data, comp_obj, comp_type):
|
||
if comp_type not in ['Button', 'Text', 'Label']:
|
||
return
|
||
|
||
# 编排入口:文本编辑/字号/颜色与按钮贴图拆分,降低单函数复杂度。
|
||
manager.luiFunction._draw_text_content_editor(comp_data, comp_obj)
|
||
if comp_type in ['Text', 'Label']:
|
||
manager.luiFunction._draw_text_font_size_editor(comp_data, comp_obj)
|
||
|
||
draw_color = manager.luiFunction._draw_text_color_editor(comp_data)
|
||
manager.luiFunction._sync_text_or_button_color(comp_obj, comp_type, draw_color)
|
||
|
||
if comp_type == 'Button':
|
||
manager.luiFunction._draw_button_texture_controls(manager, comp_data, comp_obj)
|
||
|
||
def _draw_text_content_editor(manager, comp_data, comp_obj):
|
||
imgui.text("Text Content")
|
||
imgui.separator()
|
||
if 'text' not in comp_data:
|
||
return
|
||
|
||
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}")
|
||
|
||
def _draw_text_font_size_editor(manager, comp_data, comp_obj):
|
||
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 not changed:
|
||
return
|
||
|
||
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}")
|
||
|
||
def _draw_text_color_editor(manager, comp_data):
|
||
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)
|
||
draw_color = tuple(new_color)
|
||
if changed:
|
||
comp_data['color'] = draw_color
|
||
imgui.separator()
|
||
return draw_color
|
||
|
||
def _sync_text_or_button_color(manager, comp_obj, comp_type, draw_color):
|
||
# 保留原行为:每帧同步当前编辑色,避免按钮状态贴图切换后颜色不同步。
|
||
if comp_type == 'Button':
|
||
manager.luiFunction._apply_button_layout_color(comp_obj, draw_color)
|
||
else:
|
||
manager.luiFunction._apply_text_color(comp_obj, draw_color)
|
||
|
||
imgui.separator()
|
||
imgui.spacing()
|
||
imgui.text("颜色")
|
||
|
||
def _apply_button_layout_color(manager, comp_obj, color_value):
|
||
if not hasattr(comp_obj, '_layout'):
|
||
return
|
||
try:
|
||
if hasattr(comp_obj._layout, '_sprite_left'):
|
||
comp_obj._layout._sprite_left.color = color_value
|
||
if hasattr(comp_obj._layout, '_sprite_mid'):
|
||
comp_obj._layout._sprite_mid.color = color_value
|
||
if hasattr(comp_obj._layout, '_sprite_right'):
|
||
comp_obj._layout._sprite_right.color = color_value
|
||
except Exception as e:
|
||
print(f"Button color set failed: {e}")
|
||
|
||
def _apply_text_color(manager, comp_obj, color_value):
|
||
if hasattr(comp_obj, '_text'):
|
||
comp_obj._text.color = color_value
|
||
|
||
def _draw_button_texture_controls(manager, comp_data, comp_obj):
|
||
imgui.text("按钮图片")
|
||
manager.luiFunction._draw_button_texture_pick_button(
|
||
manager, comp_data, comp_obj, "更改默认图##button_img_norm", "normal"
|
||
)
|
||
manager.luiFunction._draw_button_texture_pick_button(
|
||
manager, comp_data, comp_obj, "更改悬停图##button_img_hover", "hover"
|
||
)
|
||
manager.luiFunction._draw_button_texture_pick_button(
|
||
manager, comp_data, comp_obj, "更改按下图##button_img_pressed", "pressed"
|
||
)
|
||
|
||
if imgui.button("使用默认图尺寸##button_fit", (160, 20)):
|
||
manager.luiFunction._fit_button_to_default_texture_size(comp_data, comp_obj)
|
||
|
||
if imgui.button("恢复默认按钮##button_default", (160, 20)):
|
||
manager.luiFunction._clear_button_custom_textures(comp_data, comp_obj)
|
||
|
||
manager.luiFunction._draw_button_texture_path_labels(comp_data)
|
||
|
||
def _draw_button_texture_pick_button(manager, comp_data, comp_obj, button_label, variant):
|
||
if not imgui.button(button_label, (160, 20)):
|
||
return
|
||
|
||
selected_path = manager._change_image_texture(
|
||
title="选择图片文件",
|
||
filetypes=[
|
||
("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
|
||
("PNG", "*.png"),
|
||
("JPEG", "*.jpg;*.jpeg"),
|
||
("所有文件", "*.*"),
|
||
],
|
||
)
|
||
if not selected_path:
|
||
return
|
||
|
||
try:
|
||
manager.luiFunction._apply_button_texture_variant(
|
||
manager, comp_data, comp_obj, selected_path, variant
|
||
)
|
||
except Exception as e:
|
||
print(f"Button {variant} texture set failed: {e}")
|
||
|
||
def _apply_button_texture_variant(manager, comp_data, comp_obj, selected_path, variant):
|
||
atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048)
|
||
if not atlas_result:
|
||
return
|
||
|
||
tex, u0, v0, u1, v1, w, h = manager.luiFunction._unpack_button_atlas_result(atlas_result)
|
||
if variant == "normal":
|
||
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))
|
||
elif variant == "hover":
|
||
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))
|
||
elif variant == "pressed":
|
||
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))
|
||
|
||
manager.luiFunction._apply_button_custom_textures(comp_data, comp_obj)
|
||
|
||
def _unpack_button_atlas_result(manager, atlas_result):
|
||
if len(atlas_result) >= 7:
|
||
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
|
||
return tex, u0, v0, u1, v1, w, h
|
||
|
||
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())))
|
||
return tex, u0, v0, u1, v1, w, h
|
||
|
||
def _apply_button_custom_textures(manager, comp_data, comp_obj):
|
||
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)
|
||
|
||
def _fit_button_to_default_texture_size(manager, comp_data, comp_obj):
|
||
w = h = None
|
||
if comp_data.get('button_image_size_normal'):
|
||
try:
|
||
w, h = comp_data['button_image_size_normal']
|
||
except Exception:
|
||
w = h = None
|
||
elif comp_data.get('button_image_size'):
|
||
try:
|
||
w, h = comp_data['button_image_size']
|
||
except Exception:
|
||
w = h = None
|
||
|
||
if w is None or h is None or w <= 0 or h <= 0:
|
||
return
|
||
|
||
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()
|
||
|
||
def _clear_button_custom_textures(manager, comp_data, comp_obj):
|
||
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
|
||
|
||
def _draw_button_texture_path_labels(manager, comp_data):
|
||
if comp_data.get('button_texture_path_normal'):
|
||
imgui.text(f"默认图: {comp_data['button_texture_path_normal']}")
|
||
if comp_data.get('button_texture_path_hover'):
|
||
imgui.text(f"悬停图: {comp_data['button_texture_path_hover']}")
|
||
if comp_data.get('button_texture_path_pressed'):
|
||
imgui.text(f"按下图: {comp_data['button_texture_path_pressed']}")
|
||
|
||
def _draw_layout_transform_properties(manager, index, comp_data, comp_obj, comp_type):
|
||
# 编排入口:布局模式/坐标尺寸/布局组/Z-Offset 采用分步函数,降低维护成本。
|
||
is_fill = manager.luiFunction._draw_fill_layout_controls(manager, index, comp_data)
|
||
imgui.separator()
|
||
manager.luiFunction._draw_position_controls(manager, comp_data, comp_obj, is_fill)
|
||
manager.luiFunction._draw_size_controls(
|
||
manager, index, comp_data, comp_obj, comp_type, is_fill
|
||
)
|
||
manager.luiFunction._draw_layout_group_controls(manager, index, comp_data, comp_type)
|
||
manager.luiFunction._draw_z_offset_controls(manager, index, comp_data, comp_obj)
|
||
|
||
def _draw_fill_layout_controls(manager, index, comp_data):
|
||
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:
|
||
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 not is_fill:
|
||
return False
|
||
|
||
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)
|
||
return True
|
||
|
||
def _draw_position_controls(manager, comp_data, comp_obj, is_fill):
|
||
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}")
|
||
return
|
||
|
||
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
|
||
|
||
def _draw_size_controls(manager, index, comp_data, comp_obj, comp_type, is_fill):
|
||
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}")
|
||
return
|
||
|
||
width_changed = manager.luiFunction._draw_width_control(
|
||
manager, comp_data, comp_obj, comp_type
|
||
)
|
||
height_changed = manager.luiFunction._draw_height_control(
|
||
manager, comp_data, comp_obj, comp_type
|
||
)
|
||
manager.luiFunction._post_size_control_sync(
|
||
manager, index, comp_data, comp_type, width_changed, height_changed
|
||
)
|
||
|
||
def _draw_width_control(manager, comp_data, comp_obj, comp_type):
|
||
if 'width' not in comp_data:
|
||
return False
|
||
changed, new_width = imgui.input_float("Width", comp_data['width'], 1.0, 10.0, "%.1f")
|
||
if not changed or new_width <= 0:
|
||
return False
|
||
|
||
comp_data['width'] = new_width
|
||
if hasattr(comp_obj, 'width'):
|
||
comp_obj.width = new_width
|
||
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)
|
||
return True
|
||
|
||
def _draw_height_control(manager, comp_data, comp_obj, comp_type):
|
||
if 'height' not in comp_data:
|
||
return False
|
||
changed, new_height = imgui.input_float("Height", comp_data['height'], 1.0, 10.0, "%.1f")
|
||
if not changed or new_height <= 0:
|
||
return False
|
||
|
||
comp_data['height'] = new_height
|
||
if hasattr(comp_obj, 'height'):
|
||
comp_obj.height = new_height
|
||
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)))
|
||
return True
|
||
|
||
def _post_size_control_sync(manager, index, comp_data, comp_type, width_changed, height_changed):
|
||
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)
|
||
|
||
def _draw_layout_group_controls(manager, index, comp_data, comp_type):
|
||
if comp_type not in ['VerticalLayout', 'HorizontalLayout']:
|
||
return
|
||
|
||
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)
|
||
|
||
wrap_enabled = bool(comp_data.get('layout_wrap', True))
|
||
changed_wrap, new_wrap = imgui.checkbox("Wrap", wrap_enabled)
|
||
if changed_wrap:
|
||
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_line, new_line_spacing = imgui.input_float("Line Spacing (Wrap)", line_spacing, 1.0, 10.0, "%.1f")
|
||
if changed_line:
|
||
comp_data['layout_line_spacing'] = new_line_spacing
|
||
if hasattr(manager, '_update_layout_inner'):
|
||
manager._update_layout_inner(index)
|
||
|
||
align_options = ["start", "center", "end", "stretch"]
|
||
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()
|
||
|
||
def _draw_z_offset_controls(manager, index, comp_data, comp_obj):
|
||
imgui.spacing()
|
||
imgui.text("层级与显示顺序")
|
||
current_z = comp_data.get('z_offset')
|
||
if current_z is None:
|
||
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)
|
||
|
||
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}")
|
||
|
||
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 决定遮挡关系)")
|
||
|
||
# 特定类型的属性
|
||
|
||
def _draw_type_specific_properties(manager, index, comp_data, comp_obj, comp_type):
|
||
if comp_type == 'InputField':
|
||
manager.luiFunction._draw_type_props_input_field(manager, index, comp_data, comp_obj, comp_type)
|
||
return
|
||
if comp_type == 'Slider':
|
||
manager.luiFunction._draw_type_props_slider(manager, index, comp_data, comp_obj, comp_type)
|
||
return
|
||
if comp_type == 'Checkbox':
|
||
manager.luiFunction._draw_type_props_checkbox(manager, index, comp_data, comp_obj, comp_type)
|
||
return
|
||
if comp_type in ['Plane', 'Image']:
|
||
manager.luiFunction._draw_type_props_plane_image(manager, index, comp_data, comp_obj, comp_type)
|
||
return
|
||
if comp_type == 'Frame':
|
||
manager.luiFunction._draw_type_props_frame(manager, index, comp_data, comp_obj, comp_type)
|
||
return
|
||
if comp_type == 'Selectbox':
|
||
manager.luiFunction._draw_type_props_selectbox(manager, index, comp_data, comp_obj, comp_type)
|
||
return
|
||
if comp_type == 'HttpText':
|
||
manager.luiFunction._draw_type_props_http_text(manager, index, comp_data, comp_obj, comp_type)
|
||
return
|
||
if comp_type == 'Video':
|
||
manager.luiFunction._draw_type_props_video(manager, index, comp_data, comp_obj, comp_type)
|
||
return
|
||
|
||
def _draw_type_props_input_field(manager, index, comp_data, comp_obj, comp_type):
|
||
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
|
||
|
||
def _draw_type_props_slider(manager, index, comp_data, comp_obj, comp_type):
|
||
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)
|
||
|
||
def _draw_type_props_checkbox(manager, index, comp_data, comp_obj, comp_type):
|
||
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
|
||
|
||
def _draw_type_props_plane_image(manager, index, comp_data, comp_obj, comp_type):
|
||
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')}")
|
||
|
||
def _draw_type_props_frame(manager, index, comp_data, comp_obj, comp_type):
|
||
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))
|
||
|
||
def _draw_type_props_selectbox(manager, index, comp_data, comp_obj, comp_type):
|
||
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}")
|
||
|
||
def _draw_type_props_http_text(manager, index, comp_data, comp_obj, comp_type):
|
||
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¤t=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)
|
||
|
||
def _draw_type_props_video(manager, index, comp_data, comp_obj, comp_type):
|
||
imgui.spacing()
|
||
imgui.text("视频属性")
|
||
imgui.separator()
|
||
manager.luiFunction._draw_video_source_and_audio_controls(
|
||
manager, index, comp_data, comp_obj, comp_type
|
||
)
|
||
manager.luiFunction._draw_video_playback_controls(
|
||
manager, index, comp_data, comp_obj, comp_type
|
||
)
|
||
|
||
def _draw_video_source_and_audio_controls(manager, index, comp_data, comp_obj, comp_type):
|
||
manager.luiFunction._draw_video_audio_controls(manager, comp_data)
|
||
manager.luiFunction._draw_video_source_controls(manager, comp_data)
|
||
manager.luiFunction._process_pending_video_source(manager, comp_data, comp_obj)
|
||
|
||
def _draw_video_audio_controls(manager, comp_data):
|
||
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:
|
||
manager.luiFunction._load_audio_for_component(
|
||
manager, comp_data, selected_audio, from_video=False, stop_current=False
|
||
)
|
||
|
||
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)
|
||
|
||
def _draw_video_source_controls(manager, comp_data):
|
||
if "video_path" not in comp_data:
|
||
return
|
||
|
||
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 '未设置'}")
|
||
|
||
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
|
||
|
||
def _process_pending_video_source(manager, comp_data, comp_obj):
|
||
if "_pending_video_source" not in comp_data:
|
||
return
|
||
|
||
selected_path = comp_data.pop("_pending_video_source")
|
||
if not selected_path:
|
||
return
|
||
|
||
selected_path = selected_path.strip()
|
||
comp_data["video_path"] = selected_path
|
||
comp_data["duration"] = 0.0
|
||
print(f"✓ 尝试加载视频源: {selected_path}")
|
||
|
||
try:
|
||
video_texture = manager.luiFunction._load_video_texture_from_source(manager, selected_path)
|
||
if not video_texture:
|
||
return
|
||
manager.luiFunction._apply_loaded_video_texture(
|
||
manager, comp_data, comp_obj, video_texture, selected_path
|
||
)
|
||
except Exception as e:
|
||
print(f"⚠ 加载视频失败: {e}")
|
||
|
||
def _load_video_texture_from_source(manager, selected_path):
|
||
from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData
|
||
|
||
loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file")
|
||
loadPrcFileData("", "ffmpeg-show-error #t")
|
||
|
||
if "://" in selected_path:
|
||
print(f"🔍 尝试加载 URL: [{selected_path}]")
|
||
try:
|
||
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")
|
||
return video_texture
|
||
except Exception as e:
|
||
print(f"⚠ 显式流加载失败: {e},尝试使用 Loader...")
|
||
return manager.world.loader.loadTexture(selected_path)
|
||
|
||
return manager.world.loader.loadTexture(Filename.from_os_specific(selected_path))
|
||
|
||
def _apply_loaded_video_texture(manager, comp_data, comp_obj, video_texture, selected_path):
|
||
manager.luiFunction._sync_video_size_and_sprite(
|
||
manager, comp_data, comp_obj, video_texture
|
||
)
|
||
comp_data["texture"] = video_texture
|
||
comp_obj.video_texture = video_texture
|
||
|
||
if comp_data.get("audio_from_video"):
|
||
manager.luiFunction._load_audio_for_component(
|
||
manager, comp_data, selected_path, from_video=True, stop_current=True
|
||
)
|
||
|
||
manager.luiFunction._start_video_texture_playback(manager, video_texture, comp_data)
|
||
comp_data["is_playing"] = True
|
||
|
||
def _sync_video_size_and_sprite(manager, comp_data, comp_obj, video_texture):
|
||
orig_w = video_texture.getOrigFileXSize()
|
||
orig_h = video_texture.getOrigFileYSize()
|
||
if orig_w <= 0 or orig_h <= 0:
|
||
return
|
||
|
||
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}")
|
||
|
||
from panda3d.core import ConfigVariableBool
|
||
if not ConfigVariableBool("support-threads").getValue():
|
||
print("⚠ 警告: support-threads 为 False, 视频播放可能失败!")
|
||
|
||
if "sprite" in comp_data:
|
||
spr = comp_data["sprite"]
|
||
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
|
||
|
||
manager.luiFunction._replace_video_sprite(
|
||
manager, comp_data, comp_obj, video_texture, width, new_height
|
||
)
|
||
|
||
def _replace_video_sprite(manager, comp_data, comp_obj, video_texture, width, new_height):
|
||
if "sprite" in comp_data:
|
||
old_spr = comp_data["sprite"]
|
||
if old_spr:
|
||
try:
|
||
old_spr.parent = None
|
||
if hasattr(old_spr, "hide"):
|
||
old_spr.hide()
|
||
except Exception as ex:
|
||
print(f"⚠ Failed to detach old sprite: {ex}")
|
||
|
||
new_spr = LUISprite(comp_obj, video_texture)
|
||
if hasattr(new_spr, "set_texture"):
|
||
new_spr.set_texture(video_texture, resize=False)
|
||
new_spr.width = width
|
||
new_spr.height = new_height
|
||
new_spr.color = (1, 1, 1, 1)
|
||
new_spr.z_offset = 0
|
||
if hasattr(new_spr, "set_uv_range"):
|
||
new_spr.set_uv_range(0, 0, 1, 1)
|
||
|
||
comp_data["sprite"] = new_spr
|
||
manager.luiFunction._refresh_video_keep_alive_node(manager, comp_data, video_texture)
|
||
|
||
def _refresh_video_keep_alive_node(manager, comp_data, video_texture):
|
||
if "keep_alive" in comp_data and comp_data["keep_alive"]:
|
||
try:
|
||
comp_data["keep_alive"].destroy()
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
from direct.gui.OnscreenImage import OnscreenImage
|
||
from panda3d.core import TransparencyAttrib
|
||
|
||
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)
|
||
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}")
|
||
|
||
def _load_audio_for_component(manager, comp_data, audio_path, from_video, stop_current):
|
||
if stop_current:
|
||
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(audio_path))
|
||
if not audio_sound:
|
||
return
|
||
|
||
comp_data["audio"] = audio_sound
|
||
comp_data["audio_path"] = audio_path
|
||
comp_data["audio_from_video"] = from_video
|
||
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}")
|
||
|
||
def _start_video_texture_playback(manager, video_texture, comp_data):
|
||
if hasattr(video_texture, "play"):
|
||
if hasattr(video_texture, "stop"):
|
||
video_texture.stop()
|
||
video_texture.play()
|
||
if hasattr(video_texture, "setLoop"):
|
||
video_texture.setLoop(comp_data.get("loop", True))
|
||
|
||
def _draw_video_playback_controls(manager, index, comp_data, comp_obj, comp_type):
|
||
# 播放控制
|
||
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), "无有效视频纹理")
|
||
|
||
def _draw_anchor_and_hierarchy_properties(manager, index, comp_data, comp_obj):
|
||
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()
|