From 99fbfa32012545faf581a293d104e016c48b7fa8 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Tue, 23 Dec 2025 09:02:06 +0100 Subject: [PATCH] Add crop border dragging functionality to VideoEditor class Implement mouse event handling for dragging crop borders, allowing users to adjust crop dimensions interactively. Introduce methods to detect border proximity and update crop rectangle based on mouse movements, ensuring proper clamping to frame bounds. This enhances the user experience by providing more precise control over cropping in video editing. --- croppa/main.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/croppa/main.py b/croppa/main.py index e111d4c..a842a9f 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -806,6 +806,10 @@ class VideoEditor: self.crop_start_point = None self.crop_preview_rect = None self.crop_history = [] # For undo + self.crop_border_dragging = False + self.crop_border_drag_edge = None # 'left', 'right', 'top', 'bottom' + self.crop_border_drag_start_pos = None # (screen_x, screen_y) when drag started + self.crop_border_drag_start_rect = None # (x, y, w, h) in rotated coords when drag started # Zoom settings self.zoom_factor = 1.0 @@ -3767,6 +3771,85 @@ class VideoEditor: self.mouse_dragging = False return + # Handle crop border dragging (only when Shift is NOT pressed) + if not (flags & cv2.EVENT_FLAG_SHIFTKEY) and self.crop_rect: + border_threshold = 10 # pixels + + # Get effective crop in rotated coords and map to screen + eff_x, eff_y, eff_w, eff_h = self._get_effective_crop_rect_for_frame(getattr(self, 'current_frame', 0)) + sx1, sy1 = self._map_rotated_to_screen(eff_x, eff_y) + sx2, sy2 = self._map_rotated_to_screen(eff_x + eff_w, eff_y + eff_h) + + # Detect which border is near + def detect_border(): + if abs(x - sx1) < border_threshold and sy1 <= y <= sy2: + return 'left' + elif abs(x - sx2) < border_threshold and sy1 <= y <= sy2: + return 'right' + elif abs(y - sy1) < border_threshold and sx1 <= x <= sx2: + return 'top' + elif abs(y - sy2) < border_threshold and sx1 <= x <= sx2: + return 'bottom' + return None + + if event == cv2.EVENT_LBUTTONDOWN: + edge = detect_border() + if edge: + self.crop_border_dragging = True + self.crop_border_drag_edge = edge + self.crop_border_drag_start_pos = (x, y) + self.crop_border_drag_start_rect = (eff_x, eff_y, eff_w, eff_h) + elif event == cv2.EVENT_MOUSEMOVE and self.crop_border_dragging: + if self.crop_border_drag_edge and self.crop_border_drag_start_pos and self.crop_border_drag_start_rect: + # Convert mouse movement from screen to rotated coords + start_sx, start_sy = self.crop_border_drag_start_pos + start_rx, start_ry = self._map_screen_to_rotated(start_sx, start_sy) + curr_rx, curr_ry = self._map_screen_to_rotated(x, y) + + dx_r = curr_rx - start_rx + dy_r = curr_ry - start_ry + + # Adjust the appropriate border + new_x, new_y, new_w, new_h = self.crop_border_drag_start_rect + + # Get rotated frame dimensions + if self.rotation_angle in (90, 270): + rot_w, rot_h = self.frame_height, self.frame_width + else: + rot_w, rot_h = self.frame_width, self.frame_height + + if self.crop_border_drag_edge == 'left': + new_x = max(0, new_x + dx_r) + new_w = new_w - dx_r + if new_w < 10: + new_w = 10 + new_x = self.crop_border_drag_start_rect[0] + self.crop_border_drag_start_rect[2] - 10 + elif self.crop_border_drag_edge == 'right': + new_w = max(10, new_w + dx_r) + if new_x + new_w > rot_w: + new_w = rot_w - new_x + elif self.crop_border_drag_edge == 'top': + new_y = max(0, new_y + dy_r) + new_h = new_h - dy_r + if new_h < 10: + new_h = 10 + new_y = self.crop_border_drag_start_rect[1] + self.crop_border_drag_start_rect[3] - 10 + elif self.crop_border_drag_edge == 'bottom': + new_h = max(10, new_h + dy_r) + if new_y + new_h > rot_h: + new_h = rot_h - new_y + + # Convert back from rotated to original frame coords + self._set_crop_from_rotated_rect((new_x, new_y, new_w, new_h)) + self.clear_transformation_cache() + self.display_current_frame() + elif event == cv2.EVENT_LBUTTONUP and self.crop_border_dragging: + self.crop_border_dragging = False + self.crop_border_drag_edge = None + self.crop_border_drag_start_pos = None + self.crop_border_drag_start_rect = None + self.save_state() + # Handle crop selection (Shift + click and drag) if flags & cv2.EVENT_FLAG_SHIFTKEY: @@ -4044,6 +4127,32 @@ class VideoEditor: direction = 1 if flags > 0 else -1 self.seek_video_exact_frame(direction) + def _set_crop_from_rotated_rect(self, rotated_rect): + """Set crop_rect from a rectangle in rotated frame coordinates""" + rx, ry, rw, rh = rotated_rect + + # Convert from rotated coords to original frame coords + # Rotation is applied clockwise: 90° means ROTATE_90_CLOCKWISE + if self.rotation_angle == 0: + self.crop_rect = (rx, ry, rw, rh) + elif self.rotation_angle == 90: + # 90° clockwise: (rx, ry, rw, rh) rotated -> (ry, frame_height - rx - rw, rh, rw) original + self.crop_rect = (ry, self.frame_height - rx - rw, rh, rw) + elif self.rotation_angle == 180: + # 180°: (rx, ry, rw, rh) rotated -> (frame_width - rx - rw, frame_height - ry - rh, rw, rh) original + self.crop_rect = (self.frame_width - rx - rw, self.frame_height - ry - rh, rw, rh) + elif self.rotation_angle == 270: + # 270° (90° counterclockwise): (rx, ry, rw, rh) rotated -> (frame_width - ry - rh, rx, rh, rw) original + self.crop_rect = (self.frame_width - ry - rh, rx, rh, rw) + + # Clamp to frame bounds + x, y, w, h = self.crop_rect + x = max(0, min(x, self.frame_width - 1)) + y = max(0, min(y, self.frame_height - 1)) + w = min(w, self.frame_width - x) + h = min(h, self.frame_height - y) + self.crop_rect = (x, y, w, h) + def set_crop_from_screen_coords(self, screen_rect): """Convert screen coordinates to video frame coordinates and set crop""" x, y, w, h = screen_rect