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