"""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()