diff --git a/ui/LUI/lui_manager_interaction.py b/ui/LUI/lui_manager_interaction.py index 5b87a734..f341f9b3 100644 --- a/ui/LUI/lui_manager_interaction.py +++ b/ui/LUI/lui_manager_interaction.py @@ -310,258 +310,242 @@ class LUIManagerInteractionMixin: 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 + def _clear_pending_drag_state(self): + """清空 pending 拖拽状态。""" + 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 - # 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 + def _has_mouse_for_drag(self): + """检查是否存在可用鼠标输入。""" + return hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse() - 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 - - # 检查鼠标左键是否释放 + def _is_left_mouse_down(self): + """检查鼠标左键是否按下。""" 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 + return self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()) - 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 - - # 获取鼠标位置 + def _get_mouse_pixel_data(self): + """获取鼠标像素坐标及窗口尺寸。""" 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信息 + return pixel_x, pixel_y, win_x, win_y + + def _find_canvas_data_by_panel(self, canvas_panel): + """按 panel 对象查找 canvas 配置。""" + for c in self.canvases: + if c['panel'] == canvas_panel: + return c + return None + + def _process_canvas_drag(self): + """处理 Canvas 拖动,返回是否已处理该帧。""" + if not (hasattr(self, 'dragging_canvas') and self.dragging_canvas): + return False + + canvas_data = self._find_canvas_data_by_panel(self.dragging_canvas) + if canvas_data and canvas_data.get('fixed', False): + return True + + if not self._has_mouse_for_drag(): + return True + + if not self._is_left_mouse_down(): + self.dragging_canvas = None + return True + + pixel_x, pixel_y, _, _ = self._get_mouse_pixel_data() + 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 True + + def _try_start_pending_component_drag(self): + """处理 pending 拖拽转正,返回是否需要本帧提前结束。""" + if self.dragging_comp is not None: + return False + + if self.pending_drag_comp is None: + return True + + if not self._has_mouse_for_drag(): + return True + + if not self._is_left_mouse_down(): + # click without drag + self._clear_pending_drag_state() + return True + + pixel_x, pixel_y, _, _ = self._get_mouse_pixel_data() + + if self.pending_drag_start_mouse is None: + self.pending_drag_start_mouse = (pixel_x, pixel_y) + return True + + 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 True + + # 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 Exception: + 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) + + self._clear_pending_drag_state() + return False + + def _finish_component_drag_if_released(self): + """在鼠标释放时完成拖拽收尾,返回是否已处理该帧。""" + if not self._is_left_mouse_down(): + 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 True + return False + + def _resolve_canvas_metrics_for_drag(self, win_x, win_y): + """获取当前 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: + except Exception: # 如果获取失败,使用默认值 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'): @@ -572,7 +556,18 @@ class LUIManagerInteractionMixin: 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) - + + return canvas_abs_left, canvas_abs_top, canvas_width, canvas_height + + def _update_active_component_drag(self, pixel_x, pixel_y, win_x, win_y): + """更新已开始拖拽的组件位置。""" + # 计算新的绝对位置(减去偏移量) + new_abs_left = pixel_x - self.drag_offset[0] + new_abs_top = pixel_y - self.drag_offset[1] + + # 获取Canvas信息 + canvas_abs_left, canvas_abs_top, canvas_width, canvas_height = self._resolve_canvas_metrics_for_drag(win_x, win_y) + # 转换为Canvas相对坐标 (Global Canvas Pos) canvas_relative_left = new_abs_left - canvas_abs_left canvas_relative_top = new_abs_top - canvas_abs_top @@ -584,7 +579,7 @@ class LUIManagerInteractionMixin: # 获取组件宽高 (用于边界限制) 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 @@ -595,35 +590,33 @@ class LUIManagerInteractionMixin: # 确保左边界不小于0 if canvas_relative_left < 0: canvas_relative_left = 0 - - # 确保上边界不小于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'): @@ -648,7 +641,7 @@ class LUIManagerInteractionMixin: 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 @@ -656,7 +649,27 @@ class LUIManagerInteractionMixin: # 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) - + + def _update_drag(self, task): + """每帧更新拖动状态 - 支持Canvas边界约束和局部坐标""" + if getattr(self, 'play_mode', False): + # Disable editor drag logic while in play mode + return task.cont + + if self._process_canvas_drag(): + return task.cont + + if self._try_start_pending_component_drag(): + return task.cont + + if not self._has_mouse_for_drag(): + return task.cont + + if self._finish_component_drag_if_released(): + return task.cont + + pixel_x, pixel_y, win_x, win_y = self._get_mouse_pixel_data() + self._update_active_component_drag(pixel_x, pixel_y, win_x, win_y) return task.cont def _create_resize_handles(self):