import os import sys import struct from pathlib import Path import panda3d.core as p3d from panda3d.core import NodePath, CardMaker from ui.lui_function import luiFunction # 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)) # Handle DLL loading for lui.pyd import panda3d panda_dir = os.path.dirname(panda3d.__file__) if hasattr(os, "add_dll_directory"): try: os.add_dll_directory(panda_dir) os.add_dll_directory(str(UI_DIR)) except Exception as e: print(f"Warning: Failed to add DLL directory: {e}") 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 except ImportError as e: print(f"Error: Failed to import LUI: {e}") lui = None from imgui_bundle import imgui, imgui_ctx class LUIManager: """Manager for LUI system integration""" def __init__(self, world): self.world = world self.lui_enabled = (lui is not None) # Always initialize selection fields, even when LUI is unavailable. self.selected_index = -1 self._last_selected_index = -1 self.luiFunction = luiFunction # Editor State self.show_editor = True self.play_mode = False self._selected_type_index = 0 if not self.lui_enabled: print("LUI is not available, LUIManager will be disabled.") return # 1. 优先注册中文字体 (必须在皮肤加载前) self._register_chinese_font() # 2. 初始化核心 LUI 区域 self._init_ui_regions() # 3. 加载皮肤资源 self.lui_skin = LUIDefaultSkin() self.lui_skin.load() # 4. 状态管理 - 与engine保持一致 self.canvases = [] self.current_canvas_index = -1 self.canvas_counter = 0 self.components = [] self.root_order = [] self._tree_drag_src = None self.selected_index = -1 self._last_selected_index = -1 # 拖拽和选择状态 self.dragging_comp = None self.pending_drag_comp = None self.pending_drag_index = -1 self.pending_drag_start_mouse = None self.pending_drag_start_abs = None self.pending_drag_start_parent = None self.dragging_index = -1 self.drag_start_threshold = 6.0 self.drag_offset = (0, 0) # 锚点功能相关变量 self.anchor_popup_open = False self.pending_child_creation = False self.pending_parent_index = -1 # 子父对齐功能 self.child_parent_alignment_enabled = True # Resize handles 相关变量 self.resize_handles = [] self.selection_box = [] self.debug_outlines = {} # comp_index -> [border sprites] self.resizing_handle = None self.resize_start_pos = (0, 0) self.resize_start_bounds = {} self.min_size = 20 # 使用外部字典管理状态,避免修改 C++ 对象属性导致 AttributeError self.drag_states = {} self._last_interaction_frame = -1 # 记录最后一次交互的帧数,用于防止事件穿透 self._input_enabled = True # 创建resize handles和selection box(延迟创建,确保在Canvas之后) if hasattr(self.world, 'taskMgr'): self.world.taskMgr.doMethodLater(0.1, self._delayed_create_resize_handles, "delayed_create_resize_handles") else: self._create_resize_handles() # 添加任务来处理拖动和resize handles更新 if hasattr(self.world, 'taskMgr'): self.world.taskMgr.add(self._update_drag, "update_lui_drag") self.world.taskMgr.add(self._update_resize_handles, "update_resize_handles") self.world.taskMgr.add(self._update_component_outlines, "update_lui_outlines") print("✓ LUIManager initialized with complete functionality") def set_input_enabled(self, enabled): """Enable/disable LUI input handling (used to avoid ImGui click-through).""" if not self.lui_enabled: return if enabled == self._input_enabled: return self._input_enabled = enabled try: if enabled: if hasattr(self, "overlay_handler_node") and self.overlay_handler_node: self.overlay_handler_node.reparent_to(self.world.mouseWatcher) if hasattr(self, "overlay_region") and self.overlay_region: self.overlay_region.set_input_handler(self.overlay_handler) else: if hasattr(self, "overlay_region") and self.overlay_region: self.overlay_region.set_input_handler(None) if hasattr(self, "overlay_handler_node") and self.overlay_handler_node: self.overlay_handler_node.detach_node() except Exception as e: print(f"Warning: Failed to toggle LUI input state: {e}") def _register_chinese_font(self): """注册支持中文的字体""" from panda3d.lui import LUIFontPool import os # 尝试常见的系统路径,确保使用完整的 OS 路径 candidate_fonts = [ "C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/simhei.ttf", "C:/Windows/Fonts/arial.ttf" ] font_registered = False for fpath in candidate_fonts: if os.path.exists(fpath): try: # 在 LUI 中,建议直接用 loader.loadFont 载入后获取动态库指针 # 或者直接传递路径字符串 font = self.world.loader.loadFont(fpath) if font: LUIFontPool.get_global_ptr().register_font("default", font) LUIFontPool.get_global_ptr().register_font("label", font) print(f"✓ LUI 成功注册字体: {fpath}") font_registered = True break except Exception as e: print(f"LUI 字体注册失败 {fpath}: {e}") if not font_registered: # 如果没有系统字体,至少注册一个默认的避免崩溃 print("⚠️ 警告: 未能找到合适的 LUI 中文字体") def _init_ui_regions(self): """初始化 LUI 区域 (Overlay, Camera, WorldSpace)""" # 渲染层级说明(参考engine/main.py的设置): # - 3D场景(render)默认sort=0 # - LUI组件设置sort=5,在3D场景之上 # - pixel2d (ImGui) 默认sort=20,在LUI之上 # 这样:3D场景 < LUI组件 < ImGui面板 self.overlay_region = LUIRegion.make("OverlayUI", self.world.win) self.overlay_handler = LUIInputHandler() self.overlay_handler_node = self.world.mouseWatcher.attach_new_node(self.overlay_handler) self.overlay_region.set_input_handler(self.overlay_handler) self.overlay_region.set_sort(5) # 在3D场景之上,在ImGui之下(与engine保持一致) self.overlay_root = self.overlay_region.root # 调试输出:确认层级设置 actual_sort = self.overlay_region.getSort() print(f"✓ LUI Overlay Region 层级设置: {actual_sort}") # 显示所有DisplayRegion的层级信息 num_regions = self.world.win.getNumDisplayRegions() print(f"✓ 窗口总共有 {num_regions} 个DisplayRegion:") for i in range(num_regions): dr = self.world.win.getDisplayRegion(i) sort_value = dr.getSort() camera = dr.getCamera() camera_name = camera.getName() if not camera.isEmpty() else "无摄像机" print(f" Region {i}: sort={sort_value}, camera={camera_name}") # 当前操作的根节点 self.root = self.overlay_root def _make_canvas_draggable(self, canvas_panel): """使Canvas可拖拽""" def on_canvas_drag_start(event): # 检查是否有组件或手柄在当前帧被点击 current_frame = 0 if hasattr(p3d, 'ClockObject'): current_frame = p3d.ClockObject.getGlobalClock().getFrameCount() else: current_frame = self.world.taskMgr.globalClock.getFrameCount() if self._last_interaction_frame == current_frame: # 当前帧已经处理了组件或手柄的点击,忽略Canvas点击事件 return # 点击空白处:不自动取消选中,避免事件穿透导致闪退 # self.deselect_all() # 如果启用填充模式,则自动禁用,以便用户可以自由拖动 canvas_index = -1 for i, c in enumerate(self.canvases): if c['panel'] == canvas_panel: canvas_index = i break if canvas_index >= 0: canvas_data = self.canvases[canvas_index] # 如果是固定模式,则禁止拖动初始化 if canvas_data.get('fixed', False): return if canvas_data.get('fill_mode', False): canvas_data['fill_mode'] = False print("✓ 自动禁用填充模式以允许自由拖动") # 记录Canvas拖拽状态 if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 pos = canvas_panel.get_pos() self.canvas_drag_offset = (pixel_x - pos.x, pixel_y - pos.y) self.dragging_canvas = canvas_panel canvas_panel.bind("mousedown", on_canvas_drag_start) def switch_canvas(self, index): """切换到指定Canvas并显示/隐藏相应的UI组件""" if index < 0 or index >= len(self.canvases): return self.current_canvas_index = index # 显示选中的Canvas,隐藏其他Canvas for i, canvas in enumerate(self.canvases): if i == index: canvas['panel'].show() canvas['visible'] = True else: canvas['panel'].hide() canvas['visible'] = False # 显示/隐藏属于不同Canvas的组件(包括子组件) visible_count = 0 hidden_count = 0 for comp in self.components: comp_canvas_index = comp.get('canvas_index') comp_obj = comp['object'] comp_name = comp.get('name', 'Unknown') should_be_visible = (comp_canvas_index == index) if should_be_visible: if hasattr(comp_obj, 'show'): comp_obj.show() elif hasattr(comp_obj, 'visible'): comp_obj.visible = True visible_count += 1 else: if hasattr(comp_obj, 'hide'): comp_obj.hide() elif hasattr(comp_obj, 'visible'): comp_obj.visible = False hidden_count += 1 # 更新当前根节点 if index >= 0: self.root = self.canvases[index]['panel'] # 如果选中的组件不属于当前Canvas,取消选中 if self.selected_index >= 0 and self.selected_index < len(self.components): selected_comp = self.components[self.selected_index] if selected_comp.get('canvas_index') != index: if getattr(self, '_force_keep_selection_frames', 0) > 0: pass else: self.selected_index = -1 self._hide_selection() print(f"✓ Switched to Canvas: {self.canvases[index]['name']}") print(f" 显示组件: {visible_count}, 隐藏组件: {hidden_count}") def _update_canvas_geometry(self, canvas_data): """根据填充模式和边距更新Canvas几何信息""" if not canvas_data.get('fill_mode', False): return panel = canvas_data['panel'] margins = canvas_data.get('margins', {'left':0,'right':0,'top':0,'bottom':0}) win_width = 800 win_height = 600 if hasattr(self.world, 'win'): win_width = self.world.win.getXSize() win_height = self.world.win.getYSize() new_x = margins['left'] new_y = margins['top'] new_width = max(10, win_width - margins['left'] - margins['right']) new_height = max(10, win_height - margins['top'] - margins['bottom']) panel.set_pos(new_x, new_y) panel.width = new_width panel.height = new_height if 'background' in canvas_data: bg = canvas_data['background'] bg.width = new_width bg.height = new_height def _setup_component_drag(self, comp_data, comp_index): """为组件设置拖动功能 - 支持Canvas相对坐标系""" comp_obj = comp_data['object'] def on_mouse_down(event): comp_type = comp_data.get('type') if getattr(self, 'play_mode', False): # Let runtime widgets handle their own events (especially Slider) if comp_type == 'Slider': return try: if hasattr(comp_obj, 'on_mousedown'): comp_obj.on_mousedown(event) except Exception: pass return # Prevent re-entrant drag if self.dragging_comp is not None: return # Find current index current_index = -1 for i, c in enumerate(self.components): if c is comp_data: current_index = i break if current_index == -1: return # Mark interaction frame if hasattr(p3d, 'ClockObject'): self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount() else: self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount() # Select component self.selected_index = current_index self._last_selected_index = current_index self._force_keep_selection_frames = 3 # Force component to current canvas to avoid immediate deselect try: comp_data['canvas_index'] = self.current_canvas_index except Exception: pass try: if comp_data.get('canvas_index') is None: comp_data['canvas_index'] = self.current_canvas_index except Exception: pass try: self._show_and_update_handles(comp_data) except Exception: pass # Slider: allow knob/bar drag in edit mode without hijacking if comp_type == 'Slider': sender = getattr(event, 'sender', None) knob = getattr(comp_obj, '_knob', None) bg = getattr(comp_obj, '_slider_bg', None) if sender is not None and (sender == knob or sender == bg): # Let slider handle its own drag logic try: if hasattr(comp_obj, '_start_drag'): comp_obj._start_drag(event) except Exception: pass return parent_idx = comp_data.get('parent_index') if parent_idx is not None and parent_idx >= 0: parent_type = self.components[parent_idx].get('type') if parent_type in ['VerticalLayout', 'HorizontalLayout']: return if not comp_data.get('draggable', True): return self.pending_drag_comp = comp_data self.pending_drag_index = current_index self.pending_drag_start_abs = self._get_component_accumulated_pos(current_index) self.pending_drag_start_parent = comp_obj.parent self._is_drag_reparented = False if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 self.pending_drag_start_mouse = (pixel_x, pixel_y) else: self.pending_drag_start_mouse = None # Bind mouse event comp_obj.bind("mousedown", on_mouse_down) # Slider selection bindings: allow clicking knob/bar to select in edit mode try: if comp_data.get('type') == 'Slider': knob = getattr(comp_obj, '_knob', None) bg = getattr(comp_obj, '_slider_bg', None) except Exception: pass def on_mouse_up(event): if getattr(self, 'play_mode', False): try: if hasattr(comp_obj, 'on_mouseup'): comp_obj.on_mouseup(event) except Exception: pass return if comp_data.get('type') == 'Slider': sender = getattr(event, 'sender', None) knob = getattr(comp_obj, '_knob', None) if sender is not None and sender == knob: try: if hasattr(comp_obj, '_stop_drag'): comp_obj._stop_drag(event) except Exception: pass comp_obj.bind("mouseup", on_mouse_up) def _make_draggable(self, lui_obj): """启用 LUI 对象的鼠标拖拽功能 - 兼容旧接口""" # 查找对应的组件索引 comp_index = -1 for i, comp in enumerate(self.components): if comp.get('object') == lui_obj: comp_index = i break if comp_index >= 0: self._setup_component_drag(self.components[comp_index], comp_index) def _get_component_accumulated_pos(self, comp_index): """递归计算组件相对于Canvas的累积位置""" if comp_index < 0 or comp_index >= len(self.components): return 0, 0 comp_data = self.components[comp_index] comp_obj = comp_data['object'] # 优先使用递归计算(更可靠) left = comp_data.get('left', 0) top = comp_data.get('top', 0) parent_index = comp_data.get('parent_index') if parent_index is not None and parent_index >= 0: parent_left, parent_top = self._get_component_accumulated_pos(parent_index) result_x = parent_left + left result_y = parent_top + top print(f"[_get_component_accumulated_pos] Component #{comp_index} (has parent #{parent_index}): local=({left:.1f}, {top:.1f}), parent_offset=({parent_left:.1f}, {parent_top:.1f}), result=({result_x:.1f}, {result_y:.1f})") return result_x, result_y # 没有父组件,直接返回局部坐标(相对于Canvas) print(f"[_get_component_accumulated_pos] Component #{comp_index} (root): local=({left:.1f}, {top:.1f})") return left, top def _update_drag(self, task): """每帧更新拖动状态 - 支持Canvas边界约束和局部坐标""" if getattr(self, 'play_mode', False): # Disable editor drag logic while in play mode return task.cont # 1. 处理Canvas拖动 if hasattr(self, 'dragging_canvas') and self.dragging_canvas: # 查找对应的Canvas数据 canvas_data = None for c in self.canvases: if c['panel'] == self.dragging_canvas: canvas_data = c break # 检查Canvas是否锁定 if canvas_data and canvas_data.get('fixed', False): return task.cont if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): return task.cont import panda3d.core as p3d if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): self.dragging_canvas = None return task.cont mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 new_x = pixel_x - self.canvas_drag_offset[0] new_y = pixel_y - self.canvas_drag_offset[1] self.dragging_canvas.set_pos(new_x, new_y) return task.cont # 2. 处理组件拖动 if self.dragging_comp is None: # Pending drag: only start after threshold if self.pending_drag_comp is None: return task.cont if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): return task.cont import panda3d.core as p3d if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): # click without drag self.pending_drag_comp = None self.pending_drag_index = -1 self.pending_drag_start_mouse = None self.pending_drag_start_abs = None self.pending_drag_start_parent = None return task.cont mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 if self.pending_drag_start_mouse is None: self.pending_drag_start_mouse = (pixel_x, pixel_y) return task.cont dx = pixel_x - self.pending_drag_start_mouse[0] dy = pixel_y - self.pending_drag_start_mouse[1] if (dx * dx + dy * dy) < (self.drag_start_threshold * self.drag_start_threshold): return task.cont # Start drag now comp_data = self.pending_drag_comp self.dragging_comp = comp_data self.dragging_index = self.pending_drag_index self._original_parent_obj = self.pending_drag_start_parent self._is_drag_reparented = False acc_left, acc_top = self.pending_drag_start_abs if acc_left is None or acc_top is None: acc_left, acc_top = self._get_component_accumulated_pos(self.pending_drag_index) # If child, reparent to canvas for free drag p_idx = comp_data.get('parent_index') if p_idx is not None and p_idx >= 0 and self.current_canvas_index >= 0: canvas_panel = self.canvases[self.current_canvas_index]['panel'] comp_obj = comp_data['object'] if not comp_data.get('visual_parent_canvas'): try: if getattr(comp_obj, 'parent', None) == canvas_panel: pass elif hasattr(comp_obj, 'reparent_to'): comp_obj.reparent_to(canvas_panel) if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(acc_left, acc_top) else: comp_obj.left = acc_left comp_obj.top = acc_top self._is_drag_reparented = True except Exception: pass # Compute drag offset canvas_abs_left = 0 canvas_abs_top = 0 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_panel = self.canvases[self.current_canvas_index]['panel'] canvas_abs_left = getattr(canvas_panel, 'left', 0) canvas_abs_top = getattr(canvas_panel, 'top', 0) if canvas_abs_left == 0 and canvas_abs_top == 0: try: canvas_pos = canvas_panel.get_pos() canvas_abs_left = canvas_pos.x canvas_abs_top = canvas_pos.y except: canvas_abs_left = 300 canvas_abs_top = 100 comp_abs_left = canvas_abs_left + acc_left comp_abs_top = canvas_abs_top + acc_top self.drag_offset = (pixel_x - comp_abs_left, pixel_y - comp_abs_top) # Clear pending state self.pending_drag_comp = None self.pending_drag_index = -1 self.pending_drag_start_mouse = None self.pending_drag_start_abs = None self.pending_drag_start_parent = None # Continue with drag in same frame # 检查鼠标状态 if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): return task.cont # 检查鼠标左键是否释放 import panda3d.core as p3d if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): if self.dragging_comp: comp_data = self.dragging_comp print(f"组件移动到: left={comp_data.get('left', 0):.1f}, top={comp_data.get('top', 0):.1f} (局部坐标)") # 拖动结束:检查是否需要接触父子关系 (Auto-unparent) should_unparent = False if getattr(self, '_is_drag_reparented', False): comp_obj = comp_data['object'] # Check if center of component is outside parent bounds p_idx = comp_data.get('parent_index') if p_idx is not None and p_idx >= 0: parent_data = self.components[p_idx] p_w = parent_data.get('width', 100) p_h = parent_data.get('height', 100) c_w = comp_data.get('width', 0) c_h = comp_data.get('height', 0) # Current comp_data['left'] is LOCAL to parent local_l = comp_data.get('left', 0) local_t = comp_data.get('top', 0) center_x = local_l + c_w / 2 center_y = local_t + c_h / 2 # Check bounds: center must be in 0..p_w, 0..p_h # Allow some leniency or strict? Let's say if center is outside, unparent. if center_x < 0 or center_x > p_w or center_y < 0 or center_y > p_h: should_unparent = True if should_unparent: # Unparent! # The component is currently visually attached to Canvas (from drag start) # We just need to update the data to reflect this permanent change. # Calculate the new global (canvas-relative) position # current local + parent offset p_off_x, p_off_y = self._get_component_accumulated_pos(p_idx) new_canvas_left = local_l + p_off_x new_canvas_top = local_t + p_off_y comp_data['left'] = new_canvas_left comp_data['top'] = new_canvas_top # Remove parent linkage if 'parent_index' in comp_data: del comp_data['parent_index'] if 'anchored_to_parent' in comp_data: del comp_data['anchored_to_parent'] print(f"✓ Auto-unparented component from #{p_idx} (Dragged out)") elif hasattr(comp_obj, 'reparent_to') and hasattr(self, '_original_parent_obj'): comp_obj.reparent_to(self._original_parent_obj) # 重新应用计算出的局部坐标 comp_obj.left = comp_data.get('left', 0) comp_obj.top = comp_data.get('top', 0) print(f"✓ 组件归位到逻辑父级") self.dragging_comp = None return task.cont # 获取鼠标位置 mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() # 转换为像素坐标 win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 # 计算新的绝对位置(减去偏移量) new_abs_left = pixel_x - self.drag_offset[0] new_abs_top = pixel_y - self.drag_offset[1] # 获取Canvas信息 canvas_abs_left = 0 canvas_abs_top = 0 canvas_width = 800 canvas_height = 600 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_data = self.canvases[self.current_canvas_index] canvas_panel = canvas_data['panel'] # 获取Canvas的绝对位置 canvas_abs_left = getattr(canvas_panel, 'left', 0) canvas_abs_top = getattr(canvas_panel, 'top', 0) if canvas_abs_left == 0 and canvas_abs_top == 0: try: pos = canvas_panel.get_pos() canvas_abs_left = pos.x canvas_abs_top = pos.y except: # 如果获取失败,使用默认值 canvas_abs_left = canvas_data.get('margins', {}).get('left', 240) canvas_abs_top = canvas_data.get('margins', {}).get('top', 89) # 获取Canvas的实际尺寸 try: if hasattr(canvas_panel, 'width') and canvas_panel.width is not None: canvas_width = float(canvas_panel.width) elif hasattr(canvas_panel, 'get_width'): canvas_width = float(canvas_panel.get_width()) if hasattr(canvas_panel, 'height') and canvas_panel.height is not None: canvas_height = float(canvas_panel.height) elif hasattr(canvas_panel, 'get_height'): canvas_height = float(canvas_panel.get_height()) except Exception as e: print(f"Warning: Failed to get canvas size: {e}") # 使用窗口尺寸减去边距作为后备 margins = canvas_data.get('margins', {'left': 240, 'right': 480, 'top': 89, 'bottom': 220}) canvas_width = win_x - margins.get('left', 0) - margins.get('right', 0) canvas_height = win_y - margins.get('top', 0) - margins.get('bottom', 0) # 转换为Canvas相对坐标 (Global Canvas Pos) canvas_relative_left = new_abs_left - canvas_abs_left canvas_relative_top = new_abs_top - canvas_abs_top # 获取当前拖拽的组件数据 comp_data = self.dragging_comp comp_obj = comp_data['object'] # 获取组件宽高 (用于边界限制) comp_width = comp_data.get('width', 0) comp_height = comp_data.get('height', 0) # 尝试从对象获取更准确的宽高 if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0: comp_width = comp_obj.width if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0: comp_height = comp_obj.height # 限制在Canvas范围内 # 确保左边界不小于0 if canvas_relative_left < 0: canvas_relative_left = 0 # 确保上边界不小于0 if canvas_relative_top < 0: canvas_relative_top = 0 # 确保右边界不超出Canvas宽度 if canvas_relative_left + comp_width > canvas_width: canvas_relative_left = max(0, canvas_width - comp_width) # 确保下边界不超出Canvas高度 if canvas_relative_top + comp_height > canvas_height: canvas_relative_top = max(0, canvas_height - comp_height) # 如果有父组件,需要转换为相对于父组件的局部坐标 comp_data = self.dragging_comp parent_index = comp_data.get('parent_index') parent_offset_x = 0 parent_offset_y = 0 if parent_index is not None and parent_index >= 0: # 获取父组件相对于Canvas的累积位置 parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index) # 计算新的局部坐标 new_local_left = canvas_relative_left - parent_offset_x new_local_top = canvas_relative_top - parent_offset_y # 如果物理上已脱离父级(挂在Canvas上),则物理坐标就是Canvas相对坐标 comp_obj = comp_data['object'] if getattr(self, '_is_drag_reparented', False): # 子组件被重定向到Canvas,使其可以移动到负数局部坐标(即父组件外侧) if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(canvas_relative_left, canvas_relative_top) else: comp_obj.left = canvas_relative_left comp_obj.top = canvas_relative_top # print(f"Dragging Child (Canvas Rel): {canvas_relative_left:.1f}, {canvas_relative_top:.1f} | Local: {new_local_left:.1f}, {new_local_top:.1f}") else: # If visual parent is canvas, keep world position while storing local if comp_data.get('visual_parent_canvas'): abs_left = parent_offset_x + new_local_left abs_top = parent_offset_y + new_local_top if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(abs_left, abs_top) else: comp_obj.left = abs_left comp_obj.top = abs_top else: if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(new_local_left, new_local_top) else: comp_obj.left = new_local_left comp_obj.top = new_local_top # 更新存储的逻辑坐标数据 (保持始终相对于逻辑父级) comp_data['left'] = new_local_left comp_data['top'] = new_local_top # Sync visually-canvas children for non-container parents if self.dragging_index is not None and self.dragging_index >= 0: self._sync_canvas_children(self.dragging_index) return task.cont def _create_resize_handles(self): """创建选择框和8个resize手柄""" from Builtin.LUISprite import LUISprite from Builtin.LUIObject import LUIObject print("开始创建resize handles...") # 先创建4条选择框边线(蓝色,2px宽) for i in range(4): border = LUISprite(self.overlay_root, "blank", "skin") border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色 border.visible = False # 设置选择框层级,在组件之上但在LUI内部 if hasattr(border, 'z_offset'): border.z_offset = 40 # 选中框在普通边框(25)之上 elif hasattr(border, 'set_sort'): border.set_sort(40) # 确保边框在最后创建,这样在渲染顺序上会在前面 self.selection_box.append(border) # 最后创建8个resize手柄(白色圆形,蓝色边框) handle_positions = [ 'top-left', 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left' ] for i, pos in enumerate(handle_positions): # 创建容器对象用于接收鼠标事件 handle_container = LUIObject(parent=self.overlay_root) handle_container.width = 10 handle_container.height = 10 handle_container.solid = True handle_container.visible = False # 设置手柄层级,在选择框之上但在LUI内部 if hasattr(handle_container, 'z_offset'): handle_container.z_offset = 50 # 在选择框之上 elif hasattr(handle_container, 'set_sort'): handle_container.set_sort(50) # 在选择框之上 # 创建蓝色边框(外圈) handle_border = LUISprite(handle_container, "blank", "skin") handle_border.width = 10 handle_border.height = 10 handle_border.left = 0 handle_border.top = 0 handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框 # 创建白色圆形背景(内圈) handle_bg = LUISprite(handle_container, "blank", "skin") handle_bg.width = 8 handle_bg.height = 8 handle_bg.left = 1 handle_bg.top = 1 handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色 # 存储手柄位置信息 handle_container._resize_position = pos handle_container._resize_index = i handle_container._bg = handle_bg handle_container._border = handle_border # 绑定鼠标事件 handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h)) self.resize_handles.append(handle_container) print(f"✓ 创建了{len(self.selection_box)}条边框和{len(self.resize_handles)}个手柄") # 强制将手柄移到最前面(在LUI中,后创建的元素通常在前面) for handle in self.resize_handles: # 尝试重新设置父节点来强制更新渲染顺序 if hasattr(handle, 'reparent_to'): handle.reparent_to(self.overlay_root) for border in self.selection_box: if hasattr(border, 'reparent_to'): border.reparent_to(self.overlay_root) def _delayed_create_resize_handles(self, task): """延迟创建resize handles,确保在Canvas之后创建""" self._create_resize_handles() return task.done def _on_handle_mousedown(self, event, handle): """手柄鼠标按下事件""" # 标记交互帧,防止Canvas点击事件触发取消选中 if hasattr(p3d, 'ClockObject'): self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount() else: # Fallback if specific import not avail in this scope (though p3d is imported) self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount() if self.selected_index < 0: return comp_data = self.components[self.selected_index] print(f"✓ 开始resize: 手柄位置={handle._resize_position}") # 记录开始调整大小的状态 self.resizing_handle = handle # 获取鼠标位置 if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 self.resize_start_pos = (pixel_x, pixel_y) # 记录组件的初始边界 self.resize_start_bounds = { 'left': comp_data.get('left', 0), 'top': comp_data.get('top', 0), 'width': comp_data.get('width', 100), 'height': comp_data.get('height', 30), } def _update_resize_handles(self, task): # Restore selection if it was cleared immediately if self.selected_index < 0 and getattr(self, '_force_keep_selection_frames', 0) > 0: if getattr(self, '_last_selected_index', -1) >= 0: self.selected_index = self._last_selected_index if getattr(self, '_force_keep_selection_frames', 0) > 0: self._force_keep_selection_frames -= 1 """更新resize handles的位置和可见性""" # 检查当前Canvas是否可见 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas = self.canvases[self.current_canvas_index] if not canvas.get('visible', True): self._hide_resize_handles() return task.cont if self.selected_index < 0 or self.selected_index >= len(self.components): # 隐藏所有handles self._hide_resize_handles() return task.cont comp_data = self.components[self.selected_index] comp_type = comp_data['type'] # 只对Frame、Button、Slider、InputField、Plane、Image显示resize handles if comp_type not in ['Frame', 'Button', 'Slider', 'InputField', 'Plane', 'Image', 'Checkbox', 'Text', 'Label', 'Video', 'Progressbar', 'Selectbox', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout']: self._hide_resize_handles() return task.cont # 显示并更新handles位置 self._show_and_update_handles(comp_data) # 处理resize拖拽 if self.resizing_handle is not None: self._handle_resize_drag() return task.cont def _hide_resize_handles(self): """隐藏所有resize handles和selection box""" for border in self.selection_box: border.visible = False for handle in self.resize_handles: handle.visible = False def _ensure_component_outline(self, comp_index): 'Create a simple 4-edge outline for a component if not exists.' if comp_index in self.debug_outlines: return self.debug_outlines[comp_index] from Builtin.LUISprite import LUISprite borders = [] for _ in range(4): border = LUISprite(self.overlay_root, "blank", "skin") border.color = (0.2, 0.7, 1.0, 1.0) border.visible = False # Ensure outline is above components if hasattr(border, 'z_offset'): border.z_offset = 25 elif hasattr(border, 'set_sort'): border.set_sort(25) borders.append(border) self.debug_outlines[comp_index] = borders return borders def _update_component_outlines(self, task): 'Show component outlines in edit mode, hide in play mode.' if getattr(self, 'play_mode', False) or not self.lui_enabled or not self.show_editor: for borders in self.debug_outlines.values(): for b in borders: b.visible = False return task.cont for idx, comp_data in enumerate(self.components): # If selected, let the resize handles/selection box handle the outline (Bright Blue) # We hide the "debug" outline for the selected item to avoid double-drawing or z-fighting if idx == self.selected_index: borders = self.debug_outlines.get(idx) if borders: for b in borders: b.visible = False continue comp_obj = comp_data.get('object') if comp_obj is None: continue canvas_index = comp_data.get('canvas_index', self.current_canvas_index) if canvas_index is None or canvas_index < 0 or canvas_index >= len(self.canvases): # Component not assigned to a valid canvas borders = self.debug_outlines.get(idx) if borders: for b in borders: b.visible = False continue canvas = self.canvases[canvas_index] if not canvas.get('visible', True): # Hide outlines for components on hidden canvases borders = self.debug_outlines.get(idx) if borders: for b in borders: b.visible = False continue # Determine size width = comp_data.get('width', 0) height = comp_data.get('height', 0) try: if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0: width = comp_obj.width if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0: height = comp_obj.height except Exception: pass if width <= 0 or height <= 0: continue # Determine Position (Absolute) canvas_abs_left = 0 canvas_abs_top = 0 canvas_panel = self.canvases[canvas_index]['panel'] canvas_abs_left = getattr(canvas_panel, 'left', 0) canvas_abs_top = getattr(canvas_panel, 'top', 0) if canvas_abs_left == 0 and canvas_abs_top == 0: try: canvas_pos = canvas_panel.get_pos() canvas_abs_left = canvas_pos.x canvas_abs_top = canvas_pos.y except Exception: canvas_abs_left = 300 canvas_abs_top = 100 if hasattr(comp_obj, 'get_abs_pos'): try: abs_pos = comp_obj.get_abs_pos() abs_left = abs_pos.x abs_top = abs_pos.y except Exception: acc_left, acc_top = self._get_component_accumulated_pos(idx) abs_left = canvas_abs_left + acc_left abs_top = canvas_abs_top + acc_top else: acc_left, acc_top = self._get_component_accumulated_pos(idx) abs_left = canvas_abs_left + acc_left abs_top = canvas_abs_top + acc_top # Draw Outline borders = self._ensure_component_outline(idx) # Style for unselected components: Gray, thinner or same thickness for b in borders: b.color = (0.7, 0.7, 0.7, 0.5) # Light gray, semi-transparent if hasattr(b, 'z_offset'): b.z_offset = 25 elif hasattr(b, 'set_sort'): b.set_sort(25) b.visible = True t = 1.0 # Thickness borders[0].left = abs_left borders[0].top = abs_top - t borders[0].width = width borders[0].height = t borders[1].left = abs_left borders[1].top = abs_top + height borders[1].width = width borders[1].height = t borders[2].left = abs_left - t borders[2].top = abs_top borders[2].width = t borders[2].height = height borders[3].left = abs_left + width borders[3].top = abs_top borders[3].width = t borders[3].height = height return task.cont def _show_and_update_handles(self, comp_data): """显示并更新handles位置 - 修复坐标系不一致问题""" # 每次显示时重新创建手柄,确保它们在最前面 if not hasattr(self, '_handles_recreated'): self._recreate_handles_on_top() self._handles_recreated = True # 获取组件的Canvas相对位置和尺寸 comp_obj = comp_data['object'] # 获取Canvas的绝对位置用于坐标转换 canvas_abs_left = 0 canvas_abs_top = 0 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_panel = self.canvases[self.current_canvas_index]['panel'] # 使用LUI对象的left和top属性,而不是get_pos()方法 canvas_abs_left = getattr(canvas_panel, 'left', 0) canvas_abs_top = getattr(canvas_panel, 'top', 0) # 如果left/top属性不存在,尝试使用get_pos() if canvas_abs_left == 0 and canvas_abs_top == 0: try: canvas_pos = canvas_panel.get_pos() canvas_abs_left = canvas_pos.x canvas_abs_top = canvas_pos.y except: # 如果get_pos()也失败,使用默认值 canvas_abs_left = 300 # 默认Canvas位置 canvas_abs_top = 100 # 尝试直接获取组件的绝对屏幕坐标 if hasattr(comp_obj, 'get_abs_pos'): try: abs_pos = comp_obj.get_abs_pos() abs_left = abs_pos.x abs_top = abs_pos.y # print(f"使用 get_abs_pos(): {abs_left}, {abs_top}") except Exception as e: print(f"get_abs_pos() 失败: {e}") # Fallback to manual calculation acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index) abs_left = canvas_abs_left + acc_left abs_top = canvas_abs_top + acc_top else: # 递归计算组件的 Canvas 相对位置(累加所有父组件的相对位置) # 使用 self.selected_index 似乎最安全,或者反查 comp_data if self.selected_index >= 0 and self.components[self.selected_index] == comp_data: acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index) else: acc_left = comp_data.get('left', 0) acc_top = comp_data.get('top', 0) # 转换为绝对屏幕坐标(用于手柄和边框显示) abs_left = canvas_abs_left + acc_left abs_top = canvas_abs_top + acc_top # 获取组件尺寸 - 优先从LUI对象获取实际尺寸 comp_type = comp_data['type'] width = comp_data.get('width', 100) height = comp_data.get('height', 30) # 尝试从LUI对象获取实际尺寸 try: if hasattr(comp_obj, 'width') and comp_obj.width is not None: actual_width = comp_obj.width if actual_width > 0: width = actual_width if hasattr(comp_obj, 'height') and comp_obj.height is not None: actual_height = comp_obj.height if actual_height > 0: height = actual_height except: pass # 确保宽高是数值类型 try: width = float(width) height = float(height) except (TypeError, ValueError): width = 100.0 height = 30.0 # 更新comp_data中的尺寸 comp_data['width'] = width comp_data['height'] = height # 更新4条边框(使用绝对位置,因为边框在overlay_root下) # 确保 selection_box 存在 if not self.selection_box: self._create_resize_handles() if not self.selection_box: # Still empty? return # 上边框 self.selection_box[0].left = abs_left self.selection_box[0].top = abs_top - 2 self.selection_box[0].width = width self.selection_box[0].height = 2 self.selection_box[0].visible = True # 下边框 self.selection_box[1].left = abs_left self.selection_box[1].top = abs_top + height self.selection_box[1].width = width self.selection_box[1].height = 2 self.selection_box[1].visible = True # 左边框 self.selection_box[2].left = abs_left - 2 self.selection_box[2].top = abs_top self.selection_box[2].width = 2 self.selection_box[2].height = height self.selection_box[2].visible = True # 右边框 self.selection_box[3].left = abs_left + width self.selection_box[3].top = abs_top self.selection_box[3].width = 2 self.selection_box[3].height = height self.selection_box[3].visible = True # 更新8个手柄位置(使用绝对位置,因为手柄在overlay_root下) handle_positions = [ (abs_left - 5, abs_top - 5), # 左上 (abs_left + width/2 - 5, abs_top - 5), # 上 (abs_left + width - 5, abs_top - 5), # 右上 (abs_left + width - 5, abs_top + height/2 - 5), # 右 (abs_left + width - 5, abs_top + height - 5), # 右下 (abs_left + width/2 - 5, abs_top + height - 5), # 下 (abs_left - 5, abs_top + height - 5), # 左下 (abs_left - 5, abs_top + height/2 - 5), # 左 ] for i, (h_left, h_top) in enumerate(handle_positions): if i < len(self.resize_handles): handle = self.resize_handles[i] handle.left = h_left handle.top = h_top handle.visible = True def _recreate_handles_on_top(self): """重新创建手柄,确保它们在最前面""" from Builtin.LUISprite import LUISprite from Builtin.LUIObject import LUIObject # 清除旧的手柄 for handle in self.resize_handles: if hasattr(handle, 'remove'): handle.remove() for border in self.selection_box: if hasattr(border, 'remove'): border.remove() self.resize_handles.clear() self.selection_box.clear() # 重新创建边框 for i in range(4): border = LUISprite(self.overlay_root, "blank", "skin") border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色 border.visible = False # 设置选择框层级,在组件之上但在LUI内部 if hasattr(border, 'z_offset'): border.z_offset = 40 elif hasattr(border, 'set_sort'): border.set_sort(40) self.selection_box.append(border) # 重新创建手柄 handle_positions = [ 'top-left', 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left' ] for i, pos in enumerate(handle_positions): handle_container = LUIObject(parent=self.overlay_root) handle_container.width = 10 handle_container.height = 10 handle_container.solid = True handle_container.visible = False # 设置手柄层级,在选择框之上但在LUI内部 if hasattr(handle_container, 'z_offset'): handle_container.z_offset = 50 elif hasattr(handle_container, 'set_sort'): handle_container.set_sort(50) # 创建蓝色边框(外圈) handle_border = LUISprite(handle_container, "blank", "skin") handle_border.width = 10 handle_border.height = 10 handle_border.left = 0 handle_border.top = 0 handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框 # 创建白色圆形背景(内圈) handle_bg = LUISprite(handle_container, "blank", "skin") handle_bg.width = 8 handle_bg.height = 8 handle_bg.left = 1 handle_bg.top = 1 handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色 # 存储手柄位置信息 handle_container._resize_position = pos handle_container._resize_index = i handle_container._bg = handle_bg handle_container._border = handle_border # 绑定鼠标事件 handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h)) self.resize_handles.append(handle_container) print("✓ 重新创建了手柄,确保在最前面") def _handle_resize_drag(self): """处理resize拖拽 - 完整功能实现,支持Shift和Alt键操作""" if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): return # 检查鼠标左键是否释放 import panda3d.core as p3d if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): if self.resizing_handle: comp_data = self.components[self.selected_index] width_val = comp_data.get('width', 0) height_val = comp_data.get('height', 0) print(f"✓ 组件调整完成: width={width_val:.1f}, height={height_val:.1f}") # 更新锚点位置(如果有锚点) if comp_data.get('anchored_to_parent'): anchor_pos = comp_data.get('anchor_position') if anchor_pos: self._update_component_anchor_position(self.selected_index, anchor_pos) self.resizing_handle = None return # 获取当前鼠标位置 mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 # 计算鼠标移动距离 delta_x = pixel_x - self.resize_start_pos[0] delta_y = pixel_y - self.resize_start_pos[1] # 检查修饰键 shift_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.shift()) alt_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.alt()) # 获取组件数据 comp_data = self.components[self.selected_index] comp_obj = comp_data['object'] comp_type = comp_data['type'] # 获取初始边界(Canvas相对坐标) start_left = self.resize_start_bounds['left'] start_top = self.resize_start_bounds['top'] start_width = self.resize_start_bounds['width'] start_height = self.resize_start_bounds['height'] # 计算初始宽高比 aspect_ratio = start_width / start_height if start_height > 0 else 1 # 根据手柄位置计算新的边界 handle_pos = self.resizing_handle._resize_position new_left = start_left new_top = start_top new_width = start_width new_height = start_height # 根据手柄位置调整尺寸 if handle_pos == 'top-left': new_left = start_left + delta_x new_top = start_top + delta_y new_width = start_width - delta_x new_height = start_height - delta_y elif handle_pos == 'top': new_top = start_top + delta_y new_height = start_height - delta_y elif handle_pos == 'top-right': new_top = start_top + delta_y new_width = start_width + delta_x new_height = start_height - delta_y elif handle_pos == 'right': new_width = start_width + delta_x elif handle_pos == 'bottom-right': new_width = start_width + delta_x new_height = start_height + delta_y elif handle_pos == 'bottom': new_height = start_height + delta_y elif handle_pos == 'bottom-left': new_left = start_left + delta_x new_width = start_width - delta_x new_height = start_height + delta_y elif handle_pos == 'left': new_left = start_left + delta_x new_width = start_width - delta_x # 应用最小尺寸限制 if new_width < self.min_size: if handle_pos in ['top-left', 'left', 'bottom-left']: new_left = start_left + start_width - self.min_size new_width = self.min_size if new_height < self.min_size: if handle_pos in ['top-left', 'top', 'top-right']: new_top = start_top + start_height - self.min_size new_height = self.min_size # Shift键:保持宽高比 if shift_held: width_change = abs(new_width - start_width) height_change = abs(new_height - start_height) if width_change > height_change: new_height = new_width / aspect_ratio if handle_pos in ['top-left', 'top', 'top-right']: new_top = start_top + start_height - new_height else: new_width = new_height * aspect_ratio if handle_pos in ['top-left', 'left', 'bottom-left']: new_left = start_left + start_width - new_width # Alt键:从中心点缩放 if alt_held: center_x = start_left + start_width / 2 center_y = start_top + start_height / 2 width_diff = new_width - start_width height_diff = new_height - start_height new_width = start_width + width_diff * 2 new_height = start_height + height_diff * 2 new_left = center_x - new_width / 2 new_top = center_y - new_height / 2 # 再次应用最小尺寸限制 if new_width < self.min_size: new_width = self.min_size new_left = center_x - new_width / 2 if new_height < self.min_size: new_height = self.min_size new_top = center_y - new_height / 2 # 获取Canvas边界约束 canvas_width = 800 # 默认Canvas宽度 canvas_height = 600 # 默认Canvas高度 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_panel = self.canvases[self.current_canvas_index]['panel'] canvas_width = canvas_panel.get_width() canvas_height = canvas_panel.get_height() # 最后的安全检查,防止宽高变为负数 if new_width < 1.0: new_width = 1.0 if new_height < 1.0: new_height = 1.0 if self.selected_index < 0: return comp_data = self.components[self.selected_index] parent_index = comp_data.get('parent_index') parent_offset_x = 0 parent_offset_y = 0 if parent_index is not None and parent_index >= 0: parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index) # Update component data comp_data['left'] = new_left comp_data['top'] = new_top comp_data['width'] = new_width comp_data['height'] = new_height # Update component position child_parent = getattr(comp_obj, 'parent', None) parent_obj = None if parent_index is not None and parent_index >= 0: parent_obj = self.components[parent_index].get('object') # Check if actually parented in LUI scene graph is_scene_parented = (child_parent is not None and child_parent == parent_obj) if is_scene_parented: # Component is physically parented to its logical parent -> Use Relative coords comp_obj.left = new_left comp_obj.top = new_top else: # Component is physically root/canvas -> Use Absolute coords (Parent Abs + Relative) comp_obj.left = parent_offset_x + new_left comp_obj.top = parent_offset_y + new_top # Update component size try: if hasattr(comp_obj, 'width'): comp_obj.width = new_width if hasattr(comp_obj, 'height'): comp_obj.height = new_height if comp_type == 'Frame': pass elif comp_type in ['Plane', 'Image', 'Video']: sprite = comp_data.get('sprite') if sprite is not None: sprite.width = new_width sprite.height = new_height elif comp_type == 'Button': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) if hasattr(comp_obj, 'set_height'): comp_obj.set_height(new_height) if hasattr(comp_obj, '_apply_stretch_sizes'): comp_obj._apply_stretch_sizes() elif comp_type == 'InputField': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) if hasattr(comp_obj, 'set_height'): comp_obj.set_height(new_height) if hasattr(comp_obj, 'width'): comp_obj.width = new_width if hasattr(comp_obj, 'height'): comp_obj.height = new_height try: 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%' inner = getattr(layout, '_layout', None) if inner is not None: if hasattr(inner, 'width'): inner.width = '100%' if hasattr(inner, 'height'): inner.height = '100%' for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): spr = getattr(layout, attr, None) if spr is not None and hasattr(spr, 'height'): spr.height = '100%' if spr is not None and attr == '_sprite_mid' and hasattr(spr, 'width'): spr.width = '100%' except Exception: pass elif comp_type == 'Slider': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) if hasattr(comp_obj, 'set_height'): comp_obj.set_height(new_height) if comp_type in ['VerticalLayout', 'HorizontalLayout']: self._update_layout_inner(self.selected_index) self._update_anchored_children(self.selected_index) modifier_info = [] if shift_held: modifier_info.append('Shift(keep ratio)') if alt_held: modifier_info.append('Alt(center scale)') modifier_str = ' + '.join(modifier_info) if modifier_info else 'Normal' except Exception as e: print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}") def _add_to_scene_tree(self, comp_data): """Add a virtual node to represent the LUI component in the scene tree""" from panda3d.core import NodePath node_name = f"UI_{comp_data['type']}_{len(self.components)}" ui_node = NodePath(node_name) # Reparent to current canvas node if available if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_node = self.canvases[self.current_canvas_index]['node'] ui_node.reparent_to(canvas_node) comp_data['canvas_index'] = self.current_canvas_index else: ui_node.reparent_to(self.world.render) comp_data['canvas_index'] = None comp_data['ui_node'] = ui_node comp_data['parent_index'] = None comp_data['children_indices'] = [] def _set_parent_child_relationship(self, child_index, parent_index, anchor_position=None, keep_world=False): # Set parent-child relationship if (child_index < 0 or child_index >= len(self.components) or parent_index < 0 or parent_index >= len(self.components)): print("Error: invalid component index") return child_data = self.components[child_index] parent_data = self.components[parent_index] if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: child_data['draggable'] = False if child_data.get('parent_index') == parent_index: print(f"Component {child_index} already under {parent_index}") return current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index) print(f"[_set_parent_child_relationship] Child #{child_index} -> Parent #{parent_index}") print(f" Current absolute position: ({current_abs_left:.1f}, {current_abs_top:.1f})") old_parent_index = child_data.get('parent_index') if old_parent_index is not None and old_parent_index >= 0: old_parent_data = self.components[old_parent_index] if child_index in old_parent_data.get('children_indices', []): old_parent_data['children_indices'].remove(child_index) child_data['parent_index'] = parent_index if 'children_indices' not in parent_data: parent_data['children_indices'] = [] if child_index not in parent_data['children_indices']: parent_data['children_indices'].append(child_index) parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) new_local_left = current_abs_left - parent_abs_left new_local_top = current_abs_top - parent_abs_top print(f" Parent absolute position: ({parent_abs_left:.1f}, {parent_abs_top:.1f})") print(f" Calculated local position: ({new_local_left:.1f}, {new_local_top:.1f})") # 在数据结构中存储局部坐标(相对于父组件) child_data['left'] = new_local_left child_data['top'] = new_local_top child_obj = child_data['object'] parent_obj = parent_data['object'] parent_type = parent_data.get('type') visual_container_types = [ 'Frame', 'Plane', 'Video', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout' ] if parent_data.get('type') == 'ScrollableRegion': parent_obj = parent_data['object'].content_node elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): if parent_data.get('layout_wrap', True): parent_obj = parent_data.get('object') else: parent_obj = parent_data.get('layout_obj').cell() try: if parent_type in visual_container_types: child_data['visual_parent_canvas'] = False # 关键修改:不要调用 reparent_to(),避免破坏内部结构 # 只更新数据结构,保持原有的父对象关系 print(f"[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)") else: # Keep visual parent on canvas to avoid clipping, but keep logical parent child_data['visual_parent_canvas'] = True # If not preserving world position, clamp into parent bounds for visibility if not keep_world: try: parent_w = parent_data.get('width') parent_h = parent_data.get('height') if parent_w is None and hasattr(parent_obj, 'width'): parent_w = parent_obj.width if parent_h is None and hasattr(parent_obj, 'height'): parent_h = parent_obj.height child_w = child_data.get('width') child_h = child_data.get('height') if child_w is None and hasattr(child_obj, 'width'): child_w = child_obj.width if child_h is None and hasattr(child_obj, 'height'): child_h = child_obj.height if parent_w and parent_h and child_w and child_h: max_x = max(0.0, float(parent_w) - float(child_w)) max_y = max(0.0, float(parent_h) - float(child_h)) new_local_left = max(0.0, min(float(new_local_left), max_x)) new_local_top = max(0.0, min(float(new_local_top), max_y)) child_data['left'] = new_local_left child_data['top'] = new_local_top except Exception: pass # 关键修改:由于我们不实际重新父化对象,所有组件都保持相对于Canvas的绝对位置 # 只在数据结构中记录相对于父组件的局部坐标 if child_data.get('visual_parent_canvas'): # Keep world position try: child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index) except Exception: pass # 根据keep_world参数决定是否保持世界位置 if keep_world: # 保持世界位置:使用绝对坐标 if hasattr(child_obj, 'left'): child_obj.left = current_abs_left if hasattr(child_obj, 'top'): child_obj.top = current_abs_top print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") else: # 不保持世界位置:使用局部坐标(但由于没有实际重新父化,这会导致位置跳变) # 为了避免跳变,我们仍然使用绝对坐标 if hasattr(child_obj, 'left'): child_obj.left = current_abs_left if hasattr(child_obj, 'top'): child_obj.top = current_abs_top print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") # Ensure child visuals are visible above parent if hasattr(child_obj, 'z_offset'): try: parent_z = getattr(parent_obj, 'z_offset', 0) child_obj.z_offset = float(parent_z) + 2.0 except Exception: pass # Special handling for Button: raise internal sprites/label above parent if child_data.get('type') == 'Button': try: layout = getattr(child_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, 'z_offset'): spr.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 1.0 lbl = getattr(child_obj, '_label', None) if lbl is not None and hasattr(lbl, 'z_offset'): lbl.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 2.0 # Ensure label visible if lbl is not None and hasattr(lbl, 'show'): lbl.show() except Exception: pass # Special handling for Frame: ensure visibility elif child_data.get('type') == 'Frame': try: if hasattr(child_obj, 'show'): child_obj.show() if hasattr(child_obj, 'visible'): child_obj.visible = True print(f"[_set_parent_child_relationship] Frame visibility ensured") except Exception as e: print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}") # Special handling for Plane, Image, Video: ensure sprite visibility elif child_data.get('type') in ['Plane', 'Image', 'Video']: try: spr = child_data.get('sprite') if spr is not None: if hasattr(spr, 'show'): spr.show() if hasattr(spr, 'visible'): spr.visible = True # Ensure sprite color is not transparent if child_data.get('type') == 'Plane': color = child_data.get('color', (1, 1, 1, 1)) if hasattr(spr, 'color'): spr.color = color print(f"[_set_parent_child_relationship] {child_data.get('type')} sprite visibility ensured") except Exception as e: print(f"[_set_parent_child_relationship] Error ensuring {child_data.get('type')} visibility: {e}") child_sprite = child_data.get('sprite') if child_sprite is not None: try: if hasattr(child_sprite, 'show'): child_sprite.show() # Ensure sprite is attached to the child object if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj: child_sprite.parent = child_obj # Restore sprite color if stored if hasattr(child_sprite, 'color') and 'color' in child_data: child_sprite.color = child_data.get('color', child_sprite.color) if hasattr(child_sprite, 'z_offset'): parent_sprite = parent_data.get('sprite') parent_sprite_z = 0.0 if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): parent_sprite_z = float(parent_sprite.z_offset) child_sprite.z_offset = max(parent_sprite_z + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0) except Exception: pass # Keep child visible above parent visuals if hasattr(child_obj, 'z_offset'): parent_z = getattr(parent_obj, 'z_offset', 0) try: child_obj.z_offset = float(parent_z) + 2.0 except Exception: pass # Ensure child sprite renders above parent visuals child_sprite = child_data.get('sprite') if child_sprite is not None and hasattr(child_sprite, 'z_offset'): parent_sprite_z = 0 try: parent_sprite = parent_data.get('sprite') if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): parent_sprite_z = float(parent_sprite.z_offset) except Exception: parent_sprite_z = 0 try: child_sprite.z_offset = max(float(parent_sprite_z) + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0) except Exception: pass if hasattr(child_obj, 'show'): child_obj.show() except Exception as e: print(f"Reparenting error: {e}") # Ensure canvas_index follows parent canvas (visibility) parent_canvas = parent_data.get('canvas_index') if parent_canvas is not None: self._update_canvas_index_recursive(child_index, parent_canvas) if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True): self._apply_wrap_layout(parent_index) print(f"Set parent: child #{child_index} -> parent #{parent_index}") def _set_parent_root(self, child_index, keep_world=False): """Detach a component from its parent and move it to root (canvas).""" if child_index < 0 or child_index >= len(self.components): print("Error: invalid component index") return child_data = self.components[child_index] # 保存当前的累积位置(相对于Canvas的绝对位置) current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index) print(f"[_set_parent_root] Component #{child_index} current accumulated pos: ({current_abs_left:.1f}, {current_abs_top:.1f})") old_parent_index = child_data.get('parent_index') if old_parent_index is not None and old_parent_index >= 0: old_parent_data = self.components[old_parent_index] if child_index in old_parent_data.get('children_indices', []): old_parent_data['children_indices'].remove(child_index) # 更新数据结构 child_data['parent_index'] = None child_data['visual_parent_canvas'] = False child_data['draggable'] = True # 获取Canvas panel parent_obj = None canvas_index = self.current_canvas_index if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases): parent_obj = self.canvases[canvas_index]['panel'] child_data['canvas_index'] = canvas_index else: parent_obj = getattr(self, 'overlay_root', None) or getattr(self.world, 'render', None) child_obj = child_data.get('object') # 关键修改:对于 Button 等复杂组件,不要重新父化! # 只更新位置和数据结构 if child_obj is not None: # 更新位置数据 if keep_world: child_data['left'] = current_abs_left child_data['top'] = current_abs_top # 直接设置对象位置,不改变父对象 if hasattr(child_obj, 'set_pos'): child_obj.set_pos(current_abs_left, current_abs_top) else: if hasattr(child_obj, 'left'): child_obj.left = current_abs_left if hasattr(child_obj, 'top'): child_obj.top = current_abs_top print(f"[_set_parent_root] Set position to: ({current_abs_left:.1f}, {current_abs_top:.1f}) without reparenting") else: # 不保持世界位置:使用原有的局部坐标 if hasattr(child_obj, 'left'): child_obj.left = child_data.get('left', 0) if hasattr(child_obj, 'top'): child_obj.top = child_data.get('top', 0) # 确保可见性 if child_obj is not None: try: if hasattr(child_obj, 'show'): child_obj.show() elif hasattr(child_obj, 'visible'): child_obj.visible = True except Exception: pass # 对于 Button,确保内部组件可见 comp_type = child_data.get('type') if comp_type == 'Button' and child_obj is not None: try: layout = getattr(child_obj, '_layout', None) lbl = getattr(child_obj, '_label', None) if layout is not None: if hasattr(layout, 'show'): layout.show() if hasattr(layout, 'visible'): layout.visible = True if lbl is not None: if hasattr(lbl, 'show'): lbl.show() if hasattr(lbl, 'visible'): lbl.visible = True # 重新设置文本 current_text = child_data.get('text', 'Button') if hasattr(lbl, 'set_text'): lbl.set_text(current_text) elif hasattr(lbl, 'text'): lbl.text = current_text print(f"[_set_parent_root] Button components visibility ensured") except Exception as e: print(f"[_set_parent_root] Error ensuring button visibility: {e}") # 对于 Plane, Image, Video - 确保内部 sprite 可见 elif comp_type in ['Plane', 'Image', 'Video'] and child_obj is not None: try: spr = child_data.get('sprite') if spr is not None: if hasattr(spr, 'show'): spr.show() if hasattr(spr, 'visible'): spr.visible = True # 确保 sprite 的颜色不是透明的 if comp_type == 'Plane': color = child_data.get('color', (1, 1, 1, 1)) if hasattr(spr, 'color'): spr.color = color print(f"[_set_parent_root] {comp_type} sprite visibility ensured") except Exception as e: print(f"[_set_parent_root] Error ensuring {comp_type} visibility: {e}") # 对于 Frame - 直接确保可见 elif comp_type == 'Frame' and child_obj is not None: try: if hasattr(child_obj, 'show'): child_obj.show() if hasattr(child_obj, 'visible'): child_obj.visible = True print(f"[_set_parent_root] Frame visibility ensured") except Exception as e: print(f"[_set_parent_root] Error ensuring Frame visibility: {e}") # Update canvas index recursively self._update_canvas_index_recursive(child_index, child_data.get('canvas_index')) # 调试输出:确认组件状态 print(f"[_set_parent_root] Component #{child_index} ({child_data.get('name', 'Unknown')}) moved to root (data only)") print(f" - Position: ({child_data.get('left', 0):.1f}, {child_data.get('top', 0):.1f})") print(f" - Size: ({child_data.get('width', 0):.1f}, {child_data.get('height', 0):.1f})") print(f" - Canvas index: {child_data.get('canvas_index')}") print(f" - Visible: {getattr(child_obj, 'visible', 'N/A') if child_obj else 'N/A'}") print(f" - Parent (unchanged): {getattr(child_obj, 'parent', 'N/A') if child_obj else 'N/A'}") def create_child_component(self, parent_index, comp_type, anchor_position=None): # Create child component under parent print("\n=== Create child component ===") print(f"Parent index: {parent_index}, Type: {comp_type}") if parent_index < 0 or parent_index >= len(self.components): print("Error: invalid parent index") return None parent_data = self.components[parent_index] parent_obj = parent_data['object'] if parent_data.get('type') == 'ScrollableRegion': parent_obj = parent_data['object'].content_node elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): if parent_data.get('layout_wrap', True): parent_obj = parent_data.get('object') else: parent_obj = parent_data.get('layout_obj').cell() parent_width = parent_data.get('width', 100) parent_height = parent_data.get('height', 30) comp_sizes = { 'Button': (100, 30), 'Text': (80, 20), 'Label': (80, 20), 'CheckBox': (120, 20), 'InputField': (200, 24), 'Slider': (200, 16), 'Frame': (300, 200), 'Plane': (100, 100), 'Image': (100, 100), 'Video': (320, 240), 'Progressbar': (200, 30), 'Selectbox': (200, 30), 'ScrollableRegion': (200, 200), 'TabbedFrame': (300, 200), 'VerticalLayout': (300, 200), 'HorizontalLayout': (300, 200), } child_w, child_h = comp_sizes.get(comp_type, (80, 20)) child_x = (parent_width - child_w) / 2 child_y = (parent_height - child_h) / 2 if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: child_x = 0 child_y = 0 child_obj = None try: if comp_type == 'Button': child_obj = self.luiFunction.create_button(self, text="ChildButton", x=child_x, y=child_y, parent=parent_obj) elif comp_type in ['Text', 'Label']: child_obj = self.luiFunction.create_label(self, text="ChildText", x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'CheckBox': child_obj = self.luiFunction.create_checkbox(self, label="ChildCheckbox", x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'InputField': child_obj = self.luiFunction.create_input_field(self, text="ChildInput", x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Slider': child_obj = self.luiFunction.create_slider(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Frame': child_obj = self.luiFunction.create_frame(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Plane': child_obj = self.luiFunction.create_plane(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Image': child_obj = self.luiFunction.create_image(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Video': child_obj = self.luiFunction.create_video(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Progressbar': child_obj = self.luiFunction.create_progressbar(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Selectbox': child_obj = self.luiFunction.create_selectbox(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'ScrollableRegion': child_obj = self.luiFunction.create_scrollable_region(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'TabbedFrame': child_obj = self.luiFunction.create_tabbed_frame(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'VerticalLayout': child_obj = self.luiFunction.create_vertical_layout(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'HorizontalLayout': child_obj = self.luiFunction.create_horizontal_layout(self, x=child_x, y=child_y, parent=parent_obj) else: print(f"Error: unsupported component type {comp_type}") return None except Exception as e: print(f"Error: create component failed: {e}") import traceback traceback.print_exc() return None if child_obj: child_index = -1 for i, comp in enumerate(self.components): if comp.get('object') == child_obj: child_index = i break if child_index >= 0: child_data = self.components[child_index] if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True): self._apply_wrap_layout(parent_index) if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: child_data['draggable'] = False child_data['parent_index'] = parent_index if 'children_indices' not in parent_data: parent_data['children_indices'] = [] if child_index not in parent_data['children_indices']: parent_data['children_indices'].append(child_index) print(f"Linked child #{child_index} -> parent #{parent_index}") return child_index else: print("Child creation failed") return None def _get_descendants(self, idx): """Return set of all descendant indices for a component.""" desc = set() if idx < 0 or idx >= len(self.components): return desc comp = self.components[idx] for c_idx in comp.get('children_indices', []): if c_idx in desc: continue desc.add(c_idx) desc.update(self._get_descendants(c_idx)) return desc def _update_canvas_index_recursive(self, idx, canvas_index): """Update canvas_index for component and its descendants.""" if idx < 0 or idx >= len(self.components): return comp = self.components[idx] comp['canvas_index'] = canvas_index for c_idx in comp.get('children_indices', []): self._update_canvas_index_recursive(c_idx, canvas_index) def _reorder_list(self, items, src, target): """Move src before target within items list.""" if src == target: return items if src not in items or target not in items: return items items = list(items) items.remove(src) insert_at = items.index(target) items.insert(insert_at, src) return items def _sync_canvas_children(self, parent_index): """Update positions for children that keep visual parent on canvas.""" if parent_index < 0 or parent_index >= len(self.components): return # Get parent's absolute position parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) parent_data = self.components[parent_index] parent_obj = parent_data.get('object') for child_idx in parent_data.get('children_indices', []): if child_idx < 0 or child_idx >= len(self.components): continue child = self.components[child_idx] child_obj = child.get('object') if child_obj is None: continue # Check actual parenting child_parent = getattr(child_obj, 'parent', None) is_scene_parented = (child_parent is not None and child_parent == parent_obj) if not is_scene_parented: # If child is NOT physically parented to the parent object (e.g. it is on Canvas), # we MUST manually update its absolute position to follow the parent. abs_left = parent_abs_left + float(child.get('left', 0)) abs_top = parent_abs_top + float(child.get('top', 0)) if hasattr(child_obj, 'left'): child_obj.left = abs_left if hasattr(child_obj, 'top'): child_obj.top = abs_top if hasattr(child_obj, 'set_pos'): child_obj.set_pos(abs_left, abs_top) # Recurse for grandchildren self._sync_canvas_children(child_idx) def draw_editor(self): """Draw the LUI Editor Window""" if not self.lui_enabled or not self.show_editor: return imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever) with imgui_ctx.begin("LUI编辑器", True) as (opened, show): self.show_editor = opened if not show: return # Play/Stop if imgui.button("Play" if not self.play_mode else "Stop", (120, 25)): self.play_mode = not self.play_mode if self.play_mode: # Clear editor-only state when entering play mode self.selected_index = -1 self.dragging_comp = None self.pending_drag_comp = None self.pending_drag_index = -1 self.pending_drag_start_mouse = None self.pending_drag_start_abs = None self.pending_drag_start_parent = None self._hide_resize_handles() # Keep LUI input enabled for runtime UI interaction self.set_input_enabled(True) imgui.same_line() imgui.text_colored((0.2, 1.0, 0.2, 1.0) if self.play_mode else (1.0, 0.8, 0.2, 1.0), "Running" if self.play_mode else "Editing") imgui.separator() # In play mode, show toolbar only if self.play_mode: return # Canvas Management imgui.text("Canvas 管理") imgui.separator() if imgui.button("创建新Canvas", (150, 25)): self.luiFunction.create_canvas(self) imgui.same_line() # 显示当前Canvas信息 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): current_canvas_name = self.canvases[self.current_canvas_index]['name'] imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"当前: {current_canvas_name}") else: imgui.text_colored((1.0, 0.0, 0.0, 1.0), "无Canvas") if len(self.canvases) > 0: canvas_names = [c['name'] for c in self.canvases] if self.current_canvas_index >= len(canvas_names): self.current_canvas_index = len(canvas_names) - 1 changed, new_index = imgui.combo("选择Canvas", self.current_canvas_index, canvas_names) if changed: self.switch_canvas(new_index) # Canvas Properties if self.current_canvas_index >= 0: canvas = self.canvases[self.current_canvas_index] panel = canvas['panel'] imgui.indent() # Visibility is_visible = canvas.get('visible', True) changed, visible = imgui.checkbox("显示Canvas", is_visible) if changed: canvas['visible'] = visible if visible: panel.show() else: panel.hide() # Fill Mode & Margins is_fill_mode = canvas.get('fill_mode', False) imgui.separator() changed, fill_mode = imgui.checkbox("场景填充模式", is_fill_mode) if changed: canvas['fill_mode'] = fill_mode if fill_mode: # 刚切换到填充模式时,更新一次 self._update_canvas_geometry(canvas) if fill_mode: imgui.text("场景边距设置 (调整以匹配视口)") margins = canvas.get('margins', {'left':0,'right':0,'top':0,'bottom':0}) geometry_changed = False changed, margins['left'] = imgui.drag_float("左边距", margins['left'], 1.0, 0, 1000) if changed: geometry_changed = True changed, margins['right'] = imgui.drag_float("右边距", margins['right'], 1.0, 0, 1000) if changed: geometry_changed = True changed, margins['top'] = imgui.drag_float("顶边距", margins['top'], 1.0, 0, 500) if changed: geometry_changed = True changed, margins['bottom'] = imgui.drag_float("底边距", margins['bottom'], 1.0, 0, 500) if changed: geometry_changed = True if geometry_changed: canvas['margins'] = margins self._update_canvas_geometry(canvas) else: # Manual Position curr_pos_val = panel.get_pos() curr_pos = (curr_pos_val.x, curr_pos_val.y) # 固定开关 is_fixed = canvas.get('fixed', False) changed, fixed = imgui.checkbox("锁定位置 (禁止拖动)", is_fixed) if changed: canvas['fixed'] = fixed if not is_fixed: changed, pos = imgui.drag_float2("Canvas位置", curr_pos) if changed: panel.set_pos(pos[0], pos[1]) # Manual Size curr_size = (panel.get_width(), panel.get_height()) changed, size = imgui.drag_float2("Canvas尺寸", curr_size, 1, 100, 4000) if changed: panel.width = size[0] panel.height = size[1] # 同时更新背景尺寸 if 'background' in canvas: bg = canvas['background'] bg.width = size[0] bg.height = size[1] # Canvas Color if 'background' in canvas: imgui.separator() bg = canvas['background'] # 获取当前颜色 (RGBA) current_color = bg.color # 转换为可编辑的格式 (list) edit_color = [current_color[0], current_color[1], current_color[2], current_color[3]] changed, new_color = imgui.color_edit4("背景颜色/透明度", edit_color) if changed: bg.color = (new_color[0], new_color[1], new_color[2], new_color[3]) # Title settings if 'title_label' in canvas: imgui.separator() imgui.text("标题设置") title = canvas['title_label'] # Visible is_title_visible = title.visible changed, title_visible = imgui.checkbox("显示标题", is_title_visible) if changed: try: if title_visible: title.show() else: title.hide() except: title.visible = title_visible # Text content curr_text = title.text changed, new_text = imgui.input_text("标题文本", curr_text, 128) if changed: title.text = new_text # Position curr_pos_val = title.get_pos() curr_pos = (curr_pos_val.x, curr_pos_val.y) changed, new_pos = imgui.drag_float2("标题位置", curr_pos) if changed: title.set_pos(new_pos[0], new_pos[1]) imgui.unindent() else: imgui.text_colored((1, 1, 0, 1), "请先创建一个Canvas") imgui.spacing() imgui.text("创建组件") imgui.separator() if len(self.canvases) == 0: imgui.begin_disabled() # Component Type Selection component_types = ["按钮 (Button)", "文本 (Text)", "复选框 (CheckBox)", "输入框 (InputField)", "滑块 (Slider)", "框架 (Frame)","面板 (Plane)","图片 (Image)","视频 (Video)", "进度条 (Progressbar)", "下拉框 (Selectbox)", "滚动区域 (ScrollableRegion)", "标签页 (TabbedFrame)", "垂直布局组 (VerticalLayout)", "水平布局组 (HorizontalLayout)"] changed, self._selected_type_index = imgui.combo("组件类型", self._selected_type_index, component_types) if imgui.button("创建组件", (120, 25)): self._create_component_by_type(self._selected_type_index) if len(self.canvases) == 0: imgui.end_disabled() imgui.spacing() imgui.text(f"已创建组件 ({len(self.components)})") imgui.separator() # Component Tree Helper # self.draw_component_tree() imgui.spacing() imgui.separator() # Anchor Popup Handling self._handle_anchor_popup() def draw_component_tree(self): """Draw the component tree (can be used in other windows)""" # Track tree item rects for drop hit-testing (allows drop on earlier items) self._tree_item_rects = {} def _store_item_rect(item_key): try: rmin = imgui.get_item_rect_min() rmax = imgui.get_item_rect_max() self._tree_item_rects[item_key] = (rmin, rmax) except Exception: pass def _hit_test_tree_item(mx, my): for key, (rmin, rmax) in self._tree_item_rects.items(): try: min_x = rmin.x min_y = rmin.y max_x = rmax.x max_y = rmax.y except Exception: min_x = rmin[0] min_y = rmin[1] max_x = rmax[0] max_y = rmax[1] if min_x <= mx <= max_x and min_y <= my <= max_y: return key return None # Root drop target if imgui.selectable("ROOT", self.selected_index < 0): # ???????? pass _store_item_rect(-1) def render_component_tree(idx): if idx >= len(self.components): return True comp = self.components[idx] comp_type = comp.get('type', 'Unknown') label = f"{comp.get('name', 'N/A')}" # Determine flags flags = imgui.TreeNodeFlags_.open_on_arrow | imgui.TreeNodeFlags_.open_on_double_click | imgui.TreeNodeFlags_.default_open if self.selected_index == idx: flags |= imgui.TreeNodeFlags_.selected # Check for children children = list(comp.get('children_indices', [])) if not children: flags |= imgui.TreeNodeFlags_.leaf else: label += f" ({len(children)})" # Draw tree node is_open = imgui.tree_node_ex(f"##node_{idx}", flags, label) _store_item_rect(idx) # Custom drag/drop (avoid payload capsule issues) if imgui.is_item_active() and imgui.is_mouse_dragging(0): self._tree_drag_src = idx imgui.begin_tooltip() imgui.text(f"Move: {label}") imgui.end_tooltip() # Handle selection if imgui.is_item_clicked(): self.selected_index = idx # Clear 3D scene selection if hasattr(self.world, 'selection') and self.world.selection: self.world.selection.selectedNode = None self.world.selection.clearSelectionBox() self.world.selection.clearGizmo() # Right-click menu if imgui.begin_popup_context_item(f"##comp_ctx_{idx}"): if imgui.menu_item("删除", "", False)[0]: self.luiFunction.delete_component(self, idx) imgui.end_popup() if is_open: imgui.tree_pop() return False imgui.separator() if imgui.begin_menu("创建子组件"): child_types = ['Button', 'Text', 'CheckBox', 'InputField', 'Slider', 'Frame', 'Plane','Image', 'Video','Progressbar','Selectbox','ScrollableRegion','TabbedFrame', 'VerticalLayout', 'HorizontalLayout'] cn_names = ['按钮', '标签', '复选框', '输入框', '滑块', '框架', '面板', '图片', '视频','进度条','下拉框','滚动区域','标签页', '垂直布局组', '水平布局组'] for ct_idx, ct in enumerate(child_types): if imgui.menu_item(f"{cn_names[ct_idx]}", "", False)[0]: self.create_child_component(idx, ct) imgui.end_menu() imgui.end_popup() # Draw children if open if is_open: for child_idx in children: # After a deletion, the list might have shifted, # so we check bounds again or just return False to stop this frame's recursion if child_idx < len(self.components): if not render_component_tree(child_idx): # If a child (or sub-child) was deleted, indices shifted. # Stopping current rendering frame's branch is safest. imgui.tree_pop() return False imgui.tree_pop() return True # Find root components roots = [] for i, comp in enumerate(self.components): p_idx = comp.get('parent_index') if p_idx is None or p_idx < 0: roots.append(i) # Maintain root order for sorting if not self.root_order: self.root_order = list(roots) else: self.root_order = [r for r in self.root_order if r in roots] for r in roots: if r not in self.root_order: self.root_order.append(r) # Render roots for root_idx in self.root_order: if not render_component_tree(root_idx): break # Handle tree drop after full render (allows drop on earlier items) if self._tree_drag_src is not None and imgui.is_mouse_released(0): src_idx = self._tree_drag_src self._tree_drag_src = None mx, my = imgui.get_mouse_pos() target_idx = _hit_test_tree_item(mx, my) if target_idx is None: target_idx = -1 if 0 <= src_idx < len(self.components): if target_idx == -1: self._set_parent_root(src_idx, keep_world=True) elif target_idx != src_idx: if target_idx in self._get_descendants(src_idx): print("Invalid drop: cannot parent to descendant") else: src_parent = self.components[src_idx].get('parent_index', -1) tgt_parent = self.components[target_idx].get('parent_index', -1) io = imgui.get_io() want_reorder = bool(getattr(io, 'key_shift', False)) if want_reorder and src_parent == tgt_parent: if src_parent is None or src_parent < 0: if not self.root_order: self.root_order = [i for i, c in enumerate(self.components) if c.get('parent_index') is None or c.get('parent_index') < 0] self.root_order = self._reorder_list(self.root_order, src_idx, target_idx) else: parent_data = self.components[src_parent] children = parent_data.get('children_indices', []) parent_data['children_indices'] = self._reorder_list(children, src_idx, target_idx) else: self._set_parent_child_relationship(src_idx, target_idx, keep_world=True) # Clear drag state if released outside any item if self._tree_drag_src is not None and imgui.is_mouse_released(0): self._tree_drag_src = None imgui.spacing() imgui.separator() # Property Editing if self.selected_index >= 0 and self.selected_index < len(self.components): imgui.text("属性编辑") imgui.separator() self.luiFunction._draw_component_properties(self,self.selected_index) else: imgui.text_disabled("未选中任何组件") # Anchor popup if self.anchor_popup_open: imgui.open_popup("选择锚点位置") if imgui.begin_popup("选择锚点位置"): imgui.text("选择锚点位置:") # 中文显示名称 anchor_names = { 'top-left': '左上', 'top-center': '中上', 'top-right': '右上', 'middle-left': '左中', 'center': '中心', 'middle-right': '右中', 'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下' } anchor_positions = [ ['top-left', 'top-center', 'top-right'], ['middle-left', 'center', 'middle-right'], ['bottom-left', 'bottom-center', 'bottom-right'] ] for row in anchor_positions: for i, pos in enumerate(row): button_text = anchor_names.get(pos, pos) if imgui.button(f"{button_text}##popup_{pos}", (50, 25)): # 使用新的锚点更新方法 if hasattr(self, '_temp_selected_index_for_anchor'): selected_idx = self._temp_selected_index_for_anchor self._update_component_anchor_position(selected_idx, pos) self.anchor_popup_open = False if hasattr(self, '_temp_selected_index_for_anchor'): delattr(self, '_temp_selected_index_for_anchor') imgui.close_current_popup() if i < len(row) - 1: imgui.same_line() imgui.spacing() if imgui.button("取消"): self.anchor_popup_open = False if hasattr(self, '_temp_selected_index_for_anchor'): delattr(self, '_temp_selected_index_for_anchor') imgui.close_current_popup() imgui.end_popup() def _create_component_by_type(self, type_index): """Create LUI component by index - 默认生成在Canvas中心""" # 1. 确定生成位置 (默认 Canvas 中心) canvas_relative_x = 100 canvas_relative_y = 100 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_panel = self.canvases[self.current_canvas_index]['panel'] canvas_width = canvas_panel.get_width() canvas_height = canvas_panel.get_height() # 根据组件类型预测尺寸,以便进行中心点对齐 # 注意:这些是 lui_function.py 中定义的默认值 comp_sizes = { 0: (100, 30), # Button 1: (80, 20), # Text 2: (120, 20), # Checkbox 3: (200, 24), # InputField 4: (200, 16), # Slider 5: (300, 200), # Frame 6: (100, 100), # Plane 7: (100, 100), # Image 8: (320, 240), # Video 9: (200, 30), # Progressbar 10: (200, 30), # Selectbox 11: (200, 200), # ScrollableRegion 12: (300, 200), # TabbedFrame 13: (300, 200), # VerticalLayout 14: (300, 200) # HorizontalLayout } w, h = comp_sizes.get(type_index, (100, 100)) # 计算中心位置 canvas_relative_x = (canvas_width - w) / 2 canvas_relative_y = (canvas_height - h) / 2 # 确保不超出合理范围 (防止 Canvas 太小时位置为负) canvas_relative_x = max(10, canvas_relative_x) canvas_relative_y = max(10, canvas_relative_y) try: if type_index == 0: # Button self.luiFunction.create_button(self, text="New Button", x=canvas_relative_x, y=canvas_relative_y) elif type_index == 1: # Text self.luiFunction.create_label(self, text="New Text", x=canvas_relative_x, y=canvas_relative_y) elif type_index == 2: # Checkbox self.luiFunction.create_checkbox(self, label="Check box", x=canvas_relative_x, y=canvas_relative_y) elif type_index == 3: # InputField self.luiFunction.create_input_field(self, text="", x=canvas_relative_x, y=canvas_relative_y) elif type_index == 4: # Slider self.luiFunction.create_slider(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 5: # Frame self.luiFunction.create_frame(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 6: # Plane self.luiFunction.create_plane(self, x=canvas_relative_x, y=canvas_relative_y, color=(0.8, 0.8, 0.8, 1)) elif type_index == 7: # Image self.luiFunction.create_image(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 8: # Video self.luiFunction.create_video(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 9: # Progressbar self.luiFunction.create_progressbar(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 10: # Selectbox self.luiFunction.create_selectbox(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 11: # ScrollableRegion self.luiFunction.create_scrollable_region(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 12: # TabbedFrame self.luiFunction.create_tabbed_frame(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 13: # VerticalLayout self.luiFunction.create_vertical_layout(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 14: # HorizontalLayout self.luiFunction.create_horizontal_layout(self, x=canvas_relative_x, y=canvas_relative_y) print(f"✓ 创建组件成功,已重定向至 Canvas 中心: ({canvas_relative_x:.1f}, {canvas_relative_y:.1f})") if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() except Exception as e: print(f"创建组件失败: {str(e)}") def set_component_anchor(self, comp_index, anchor_position): """设置组件锚点 - 保持向后兼容""" self._update_component_anchor_position(comp_index, anchor_position) def _update_anchored_children(self, parent_index): """Recursively update anchored/fill children.""" if parent_index < 0 or parent_index >= len(self.components): return print(f"[_update_anchored_children] Updating children of #{parent_index}") parent_data = self.components[parent_index] children = parent_data.get('children_indices', []) for child_idx in children: if child_idx < len(self.components): child_data = self.components[child_idx] # Fill layout has priority over anchored positioning if child_data.get('layout_mode') == 'fill': print(f" -> Child #{child_idx} is FILL mode") self._apply_fill_layout(child_idx) elif child_data.get('anchored_to_parent'): anchor_pos = child_data.get('anchor_position') manual_offset = ( child_data.get('anchor_manual_offset_x', 0.0), child_data.get('anchor_manual_offset_y', 0.0) ) if anchor_pos: self._update_component_anchor_position(child_idx, anchor_pos, manual_offset) # Recurse into children self._update_anchored_children(child_idx) def _apply_fill_layout(self, comp_index): """Apply fill layout to component: match parent/canvas size with margins.""" if comp_index < 0 or comp_index >= len(self.components): return comp_data = self.components[comp_index] comp_obj = comp_data.get('object') comp_type = comp_data.get('type', '') parent_width = None parent_height = None parent_index = comp_data.get('parent_index') if parent_index is not None and parent_index >= 0 and parent_index < len(self.components): parent_data = self.components[parent_index] parent_width = parent_data.get('width') parent_height = parent_data.get('height') parent_obj = parent_data.get('object') if parent_width is None and parent_obj is not None and hasattr(parent_obj, 'width'): parent_width = parent_obj.width if parent_height is None and parent_obj is not None and hasattr(parent_obj, 'height'): parent_height = parent_obj.height else: canvas_index = comp_data.get('canvas_index', self.current_canvas_index) if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases): canvas_panel = self.canvases[canvas_index].get('panel') if canvas_panel is not None: if hasattr(canvas_panel, 'width'): parent_width = canvas_panel.width elif hasattr(canvas_panel, 'get_width'): parent_width = canvas_panel.get_width() if hasattr(canvas_panel, 'height'): parent_height = canvas_panel.height elif hasattr(canvas_panel, 'get_height'): parent_height = canvas_panel.get_height() # DEBUG: Trace fill layout calculations # print(f"[_apply_fill_layout] Comp #{comp_index} Parent #{parent_index}: PW={parent_width} PH={parent_height}") if parent_width is None or parent_height is None: # print(f"[_apply_fill_layout] Failed to get parent dimensions for Comp #{comp_index}") return margin_left = float(comp_data.get('fill_margin_left', 0.0)) margin_right = float(comp_data.get('fill_margin_right', 0.0)) margin_top = float(comp_data.get('fill_margin_top', 0.0)) margin_bottom = float(comp_data.get('fill_margin_bottom', 0.0)) new_left = margin_left new_top = margin_top new_width = float(parent_width) - (margin_left + margin_right) new_height = float(parent_height) - (margin_top + margin_bottom) if new_width < 1.0: new_width = 1.0 if new_height < 1.0: new_height = 1.0 comp_data['left'] = new_left comp_data['top'] = new_top comp_data['width'] = new_width comp_data['height'] = new_height if comp_obj is not None: # Handle visual_parent_canvas case: convert local to absolute abs_left = new_left abs_top = new_top if parent_index is not None and parent_index >= 0 and comp_data.get('visual_parent_canvas'): p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index) abs_left = p_abs_x + new_left abs_top = p_abs_y + new_top if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(abs_left, abs_top) else: if hasattr(comp_obj, 'left'): comp_obj.left = abs_left if hasattr(comp_obj, 'top'): comp_obj.top = abs_top if hasattr(comp_obj, 'width'): comp_obj.width = new_width if hasattr(comp_obj, 'height'): comp_obj.height = new_height if comp_type in ['Plane', 'Image', 'Video'] and 'sprite' in comp_data: comp_data['sprite'].width = new_width comp_data['sprite'].height = new_height elif comp_type == 'Button': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) if hasattr(comp_obj, 'set_height'): comp_obj.set_height(new_height) # Force update internal layout for Button to ensure fill works try: layout = getattr(comp_obj, '_layout', None) if layout: if hasattr(layout, 'width'): layout.width = new_width if hasattr(layout, 'height'): layout.height = new_height # Inner layout inner = getattr(layout, '_layout', None) if inner: if hasattr(inner, 'width'): inner.width = new_width if hasattr(inner, 'height'): inner.height = new_height # Mid sprite usually stretches spr_mid = getattr(layout, '_sprite_mid', None) if spr_mid: spr_mid.width = new_width spr_mid.height = new_height except Exception: pass elif comp_type == 'InputField': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) elif comp_type == 'Slider': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) def _apply_layout_spacing(self, layout_obj, spacing): try: if hasattr(layout_obj, 'set_spacing'): layout_obj.set_spacing(spacing) elif hasattr(layout_obj, 'setSpacing'): layout_obj.setSpacing(spacing) elif hasattr(layout_obj, 'spacing'): layout_obj.spacing = spacing elif hasattr(layout_obj, '_spacing'): layout_obj._spacing = spacing except Exception: pass def _apply_layout_alignment(self, layout_obj, align): try: if hasattr(layout_obj, 'set_alignment'): layout_obj.set_alignment(align) elif hasattr(layout_obj, 'alignment'): layout_obj.alignment = align elif hasattr(layout_obj, 'align'): layout_obj.align = align except Exception: pass def _update_layout_inner(self, comp_index): if comp_index < 0 or comp_index >= len(self.components): return comp_data = self.components[comp_index] layout_obj = comp_data.get('layout_obj') if layout_obj is None: return width = float(comp_data.get('width', 0.0)) height = float(comp_data.get('height', 0.0)) 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)) inner_w = max(1.0, width - (pad_left + pad_right)) inner_h = max(1.0, height - (pad_top + pad_bottom)) try: if hasattr(layout_obj, 'left'): layout_obj.left = pad_left if hasattr(layout_obj, 'top'): layout_obj.top = pad_top if hasattr(layout_obj, 'width'): layout_obj.width = inner_w if hasattr(layout_obj, 'height'): layout_obj.height = inner_h if hasattr(layout_obj, 'set_size'): layout_obj.set_size(inner_w, inner_h) except Exception: pass spacing = float(comp_data.get('layout_spacing', 0.0)) self._apply_layout_spacing(layout_obj, spacing) align = comp_data.get('layout_align', None) if align: self._apply_layout_alignment(layout_obj, align) if comp_data.get('type') in ['HorizontalLayout', 'VerticalLayout']: self._apply_wrap_layout(comp_index) try: if hasattr(layout_obj, 'update'): layout_obj.update() except Exception: pass def _reparent_scrollable_children(self, parent_index): if parent_index < 0 or parent_index >= len(self.components): return parent_data = self.components[parent_index] if parent_data.get('type') != 'ScrollableRegion': return parent_obj = parent_data['object'] try: content_node = parent_obj.content_node except Exception: return for child_idx in parent_data.get('children_indices', []): if child_idx < 0 or child_idx >= len(self.components): continue child_data = self.components[child_idx] child_obj = child_data.get('object') if child_obj is None: continue try: if hasattr(child_obj, 'reparent_to'): child_obj.reparent_to(content_node) elif hasattr(child_obj, 'parent'): child_obj.parent = content_node except Exception: pass def _apply_wrap_layout(self, parent_index): if parent_index < 0 or parent_index >= len(self.components): return parent_data = self.components[parent_index] layout_type = parent_data.get('type') if layout_type not in ['HorizontalLayout', 'VerticalLayout']: return if not parent_data.get('layout_wrap', True): return # Get parent absolute position for coordinate conversion parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) width = float(parent_data.get('width', 0.0)) height = float(parent_data.get('height', 0.0)) pad_left = float(parent_data.get('layout_padding_left', 0.0)) pad_right = float(parent_data.get('layout_padding_right', 0.0)) pad_top = float(parent_data.get('layout_padding_top', 0.0)) pad_bottom = float(parent_data.get('layout_padding_bottom', 0.0)) spacing = float(parent_data.get('layout_spacing', 0.0)) line_spacing = float(parent_data.get('layout_line_spacing', 0.0)) inner_w = max(1.0, width - (pad_left + pad_right)) inner_h = max(1.0, height - (pad_top + pad_bottom)) if layout_type == 'HorizontalLayout': x = pad_left y = pad_top row_h = 0.0 for child_idx in parent_data.get('children_indices', []): if child_idx < 0 or child_idx >= len(self.components): continue child = self.components[child_idx] child_obj = child.get('object') cw = float(child.get('width', 0.0)) ch = float(child.get('height', 0.0)) # Allow layout even if size is 0, but usually skip # if cw <= 0 or ch <= 0: continue if x > pad_left and (x + cw) > (pad_left + inner_w): x = pad_left y += row_h + line_spacing row_h = 0.0 # Data stores relative position (to parent) child['left'] = x child['top'] = y # Update visual object try: child_parent = getattr(child_obj, 'parent', None) parent_obj = parent_data.get('object') if child_parent is not None and child_parent == parent_obj: # Child is parented to layout object: use relative coordinates if hasattr(child_obj, 'left'): child_obj.left = x if hasattr(child_obj, 'top'): child_obj.top = y if hasattr(child_obj, 'set_pos'): child_obj.set_pos(x, y) else: # Child is NOT parented to layout object (e.g. at root): use absolute coordinates abs_x = parent_abs_left + x abs_y = parent_abs_top + y if hasattr(child_obj, 'left'): child_obj.left = abs_x if hasattr(child_obj, 'top'): child_obj.top = abs_y if hasattr(child_obj, 'set_pos'): child_obj.set_pos(abs_x, abs_y) except Exception: pass # Recursively update child layout/visuals if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True): self._apply_wrap_layout(child_idx) elif child.get('children_indices'): self._sync_canvas_children(child_idx) row_h = max(row_h, ch) x += cw + spacing else: # Vertical wrap: flow top->bottom, then next column x = pad_left y = pad_top col_w = 0.0 for child_idx in parent_data.get('children_indices', []): if child_idx < 0 or child_idx >= len(self.components): continue child = self.components[child_idx] child_obj = child.get('object') cw = float(child.get('width', 0.0)) ch = float(child.get('height', 0.0)) if y > pad_top and (y + ch) > (pad_top + inner_h): y = pad_top x += col_w + line_spacing col_w = 0.0 child['left'] = x child['top'] = y try: child_parent = getattr(child_obj, 'parent', None) parent_obj = parent_data.get('object') if child_parent is not None and child_parent == parent_obj: # Child is parented to layout object: use relative coordinates if hasattr(child_obj, 'left'): child_obj.left = x if hasattr(child_obj, 'top'): child_obj.top = y if hasattr(child_obj, 'set_pos'): child_obj.set_pos(x, y) else: # Child is NOT parented to layout object (e.g. at root): use absolute coordinates abs_x = parent_abs_left + x abs_y = parent_abs_top + y if hasattr(child_obj, 'left'): child_obj.left = abs_x if hasattr(child_obj, 'top'): child_obj.top = abs_y if hasattr(child_obj, 'set_pos'): child_obj.set_pos(abs_x, abs_y) except Exception: pass # If child is also a layout, recursively update it if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True): self._apply_wrap_layout(child_idx) # If child is NOT a layout but has children (like a Frame with children), update their visual text elif child.get('children_indices'): self._sync_canvas_children(child_idx) col_w = max(col_w, cw) y += ch + spacing def get_selected_component(self): """获取当前选中的组件""" if self.selected_index >= 0 and self.selected_index < len(self.components): return self.components[self.selected_index] return None def select_component(self, index): """选中指定索引的组件""" if 0 <= index < len(self.components): self.selected_index = index print(f"✓ 选中组件 #{index}: {self.components[index]['type']}") else: self.selected_index = -1 def deselect_all(self): """取消选中所有组件""" if getattr(self, '_force_keep_selection_frames', 0) > 0: return self.selected_index = -1 print("✓ 已取消选中所有组件") def _change_image_texture(self, title = "选择文件",filetypes=None,initialdir=None): """更改图像组件的纹理""" try: import tkinter as tk from tkinter import filedialog root = tk.Tk() root.withdraw() root.attributes('-topmost',True) if filetypes is None: filetypes = [("所有文件","*.*")] file_path = filedialog.askopenfilename( title = title, filetypes = filetypes, initialdir = initialdir ) root.destroy() if file_path: return file_path return None except Exception as e: print(f"更改纹理失败: {e}") def _update_component_anchor_position(self, comp_index, anchor_position, manual_offset=(0.0, 0.0)): """更新组件的锚点位置""" if comp_index < 0 or comp_index >= len(self.components): return comp_data = self.components[comp_index] comp_obj = comp_data['object'] # Update data comp_data['anchor_position'] = anchor_position comp_data['anchored_to_parent'] = True comp_data['anchor_manual_offset_x'] = manual_offset[0] comp_data['anchor_manual_offset_y'] = manual_offset[1] # Determine strict parent reference parent_index = comp_data.get('parent_index') parent_width = 0 parent_height = 0 if parent_index is not None and parent_index >= 0: parent_data = self.components[parent_index] parent_width = parent_data.get('width', 100) parent_height = parent_data.get('height', 30) # Try to get actual object size if available p_obj = parent_data.get('object') if p_obj: if hasattr(p_obj, 'width') and p_obj.width > 0: parent_width = p_obj.width if hasattr(p_obj, 'height') and p_obj.height > 0: parent_height = p_obj.height elif self.current_canvas_index >= 0: # Canvas anchor canvas = self.canvases[self.current_canvas_index]['panel'] if hasattr(canvas, 'get_width'): parent_width = canvas.get_width() parent_height = canvas.get_height() else: parent_width = canvas.width parent_height = canvas.height # Calculate anchor point in parent local space anchor_ratios = { 'top-left': (0, 0), 'top-center': (0.5, 0), 'top-right': (1, 0), 'middle-left': (0, 0.5), 'center': (0.5, 0.5), 'middle-right': (1, 0.5), 'bottom-left': (0, 1), 'bottom-center': (0.5, 1), 'bottom-right': (1, 1) } rx, ry = anchor_ratios.get(anchor_position, (0, 0)) anchor_x = parent_width * rx anchor_y = parent_height * ry child_width = comp_data.get('width', 0) child_height = comp_data.get('height', 0) # Try to get actual object size if hasattr(comp_obj, 'width') and comp_obj.width > 0: child_width = comp_obj.width if hasattr(comp_obj, 'height') and comp_obj.height > 0: child_height = comp_obj.height # Center child on anchor (pivot logic) child_left = anchor_x - (child_width * rx) + manual_offset[0] child_top = anchor_y - (child_height * ry) + manual_offset[1] # Update logical position data comp_data['left'] = child_left comp_data['top'] = child_top # Update visual position if parent_index is not None and parent_index >= 0: # Check if visually parented to canvas (absolute positioning needed) if comp_data.get('visual_parent_canvas'): p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index) abs_left = p_abs_x + child_left abs_top = p_abs_y + child_top # Prefer left/top properties for LUI objects if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): comp_obj.left = abs_left comp_obj.top = abs_top elif hasattr(comp_obj, 'set_pos'): try: comp_obj.set_pos(abs_left, abs_top) except TypeError: # Fallback for NodePath-like objects requiring 3 args comp_obj.set_pos(abs_left, abs_top, 0) else: # Standard local parenting if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): comp_obj.left = child_left comp_obj.top = child_top elif hasattr(comp_obj, 'set_pos'): try: comp_obj.set_pos(child_left, child_top) except TypeError: comp_obj.set_pos(child_left, child_top, 0) else: # Canvas root if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): comp_obj.left = child_left comp_obj.top = child_top elif hasattr(comp_obj, 'set_pos'): try: comp_obj.set_pos(child_left, child_top) except TypeError: comp_obj.set_pos(child_left, child_top, 0) print(f"✓ 已更新锚点位置: {anchor_position} -> ({child_left:.1f}, {child_top:.1f})") # Update children positions (for visual_parent_canvas or anchored children) self._sync_canvas_children(comp_index) self._update_anchored_children(comp_index) def _handle_anchor_popup(self): """Handle component anchor selection popup""" if imgui.begin_popup("选择锚点位置"): imgui.text("选择锚点位置:") anchor_names = { 'top-left': '左上', 'top-center': '中上', 'top-right': '右上', 'middle-left': '左中', 'center': '中心', 'middle-right': '右中', 'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下' } anchor_positions = [ ['top-left', 'top-center', 'top-right'], ['middle-left', 'center', 'middle-right'], ['bottom-left', 'bottom-center', 'bottom-right'] ] for row in anchor_positions: for i, pos in enumerate(row): button_text = anchor_names.get(pos, pos) if imgui.button(f"{button_text}##popup_{pos}", (50, 25)): if hasattr(self, '_temp_selected_index_for_anchor'): selected_idx = self._temp_selected_index_for_anchor # Reset manual offset by passing (0,0) explicitly to match original behavior logic self._update_component_anchor_position(selected_idx, pos, manual_offset=(0.0, 0.0)) # Clean up temp attribute delattr(self, '_temp_selected_index_for_anchor') # Reset flag if used elsewhere self.anchor_popup_open = False imgui.close_current_popup() if i < len(row) - 1: imgui.same_line() imgui.spacing() if imgui.button("取消", (160, 25)): self.anchor_popup_open = False if hasattr(self, '_temp_selected_index_for_anchor'): delattr(self, '_temp_selected_index_for_anchor') imgui.close_current_popup() imgui.end_popup()