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