diff --git a/croppa/main.py b/croppa/main.py index 18933b1..a1cb2d7 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -72,7 +72,7 @@ class VideoEditor: # Crop adjustment settings CROP_SIZE_STEP = 5 # pixels to expand/contract crop CROP_MIN_SIZE = 10 # minimum crop width/height in pixels - CROP_DRAG_MIN_DISTANCE = 10 # pixels - minimum drag distance before applying crop adjustment + CROP_DRAG_MIN_DISTANCE = 1 # pixels - minimum drag distance before applying crop adjustment # Motion tracking settings TRACKING_POINT_THRESHOLD = 10 # pixels for delete/snap radius @@ -156,7 +156,10 @@ class VideoEditor: self.crop_border_dragging = False 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 - self.crop_border_drag_inside = None # True if drag started inside crop area, False if outside + self.crop_drag_edge = None # 'left', 'right', 'top', 'bottom' + self.crop_drag_mode = None # 'expand' or 'contract' + self.mouse_left_down = False + self.mouse_right_down = False # Zoom settings self.zoom_factor = 1.0 @@ -3116,125 +3119,126 @@ class VideoEditor: self.mouse_dragging = False return - # Handle crop border dragging (only when Shift and Ctrl are NOT pressed) + # Track mouse button state (used for crop drag mode) + if event == cv2.EVENT_LBUTTONDOWN: + self.mouse_left_down = True + elif event == cv2.EVENT_LBUTTONUP: + self.mouse_left_down = False + elif event == cv2.EVENT_RBUTTONDOWN: + self.mouse_right_down = True + elif event == cv2.EVENT_RBUTTONUP: + self.mouse_right_down = False + + # Handle crop resizing with drag direction (no Shift/Ctrl) + # Left drag: expand in drag direction + # Left + right drag: contract from opposite edge if not (flags & cv2.EVENT_FLAG_SHIFTKEY) and not (flags & cv2.EVENT_FLAG_CTRLKEY) and self.crop_rect: - # 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) - - # Check if cursor is inside crop area - inside_crop = sx1 <= x <= sx2 and sy1 <= y <= sy2 - if event == cv2.EVENT_LBUTTONDOWN: - # Start dragging - record position and whether we're inside/outside + eff_x, eff_y, eff_w, eff_h = self._get_effective_crop_rect_for_frame(getattr(self, 'current_frame', 0)) self.crop_border_dragging = True self.crop_border_drag_start_pos = (x, y) self.crop_border_drag_start_rect = (eff_x, eff_y, eff_w, eff_h) - self.crop_border_drag_inside = inside_crop - elif event == cv2.EVENT_MOUSEMOVE and self.crop_border_dragging: - if self.crop_border_drag_start_pos and self.crop_border_drag_start_rect and self.crop_border_drag_inside is not None: - # 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 - - # Check minimum drag distance - drag_distance = (dx_r ** 2 + dy_r ** 2) ** 0.5 - if drag_distance < self.CROP_DRAG_MIN_DISTANCE: - return - - # Determine primary direction (horizontal vs vertical) - abs_dx = abs(dx_r) - abs_dy = abs(dy_r) - - # Adjust the appropriate border based on direction and inside/outside - 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 - - # Determine which border to adjust based on movement direction - if abs_dx > abs_dy: - # Horizontal movement - if self.crop_border_drag_inside: - # Inside crop: drag left -> adjust left border, drag right -> adjust right border - if dx_r < 0: - # Dragging left -> move left border left (expand left) - new_x = max(0, new_x + dx_r) - new_w = new_w - dx_r - if new_w < self.CROP_MIN_SIZE: - new_w = self.CROP_MIN_SIZE - new_x = self.crop_border_drag_start_rect[0] + self.crop_border_drag_start_rect[2] - self.CROP_MIN_SIZE - else: - # Dragging right -> move right border right (expand right) - new_w = max(self.CROP_MIN_SIZE, new_w + dx_r) - if new_x + new_w > rot_w: - new_w = rot_w - new_x + self.crop_drag_mode = 'contract' if self.mouse_right_down else 'expand' + + # If right button is pressed while dragging, switch to contract mode + if event == cv2.EVENT_RBUTTONDOWN and self.crop_border_dragging: + self.crop_drag_mode = 'contract' + + if ( + event == cv2.EVENT_MOUSEMOVE + and self.crop_border_dragging + and self.crop_border_drag_start_pos + and self.crop_border_drag_start_rect + and self.crop_drag_mode + ): + 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 + + # Ignore tiny movements + if abs(dx_r) < self.CROP_DRAG_MIN_DISTANCE and abs(dy_r) < self.CROP_DRAG_MIN_DISTANCE: + return + + start_x, start_y, start_w, start_h = self.crop_border_drag_start_rect + 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 + + mode = self.crop_drag_mode # 'expand' or 'contract' + new_x, new_y, new_w, new_h = start_x, start_y, start_w, start_h + + abs_dx = abs(dx_r) + abs_dy = abs(dy_r) + + # Horizontal drag + if abs_dx >= abs_dy: + if mode == 'expand': + if dx_r < 0: + # Drag left -> extend left edge left + move = min(abs_dx, start_x) + new_x = start_x - move + new_w = start_w + move else: - # Outside crop: always contract based on drag direction - # Drag left -> contract right (move right border left) - # Drag right -> contract left (move left border right) - if dx_r < 0: - # Dragging left -> move right border left (contract right) - new_w = max(self.CROP_MIN_SIZE, new_w + dx_r) - if new_w < self.CROP_MIN_SIZE: - new_w = self.CROP_MIN_SIZE - else: - # Dragging right -> move left border right (contract left) - new_x = max(0, new_x + dx_r) - new_w = new_w - dx_r - if new_w < self.CROP_MIN_SIZE: - new_w = self.CROP_MIN_SIZE - new_x = self.crop_border_drag_start_rect[0] + self.crop_border_drag_start_rect[2] - self.CROP_MIN_SIZE + # Drag right -> extend right edge right + max_move = max(0, rot_w - (start_x + start_w)) + move = min(dx_r, max_move) + new_w = start_w + move else: - # Vertical movement - if self.crop_border_drag_inside: - # Inside crop: drag up -> adjust top border, drag down -> adjust bottom border - if dy_r < 0: - # Dragging up -> move top border up (expand up) - new_y = max(0, new_y + dy_r) - new_h = new_h - dy_r - if new_h < self.CROP_MIN_SIZE: - new_h = self.CROP_MIN_SIZE - new_y = self.crop_border_drag_start_rect[1] + self.crop_border_drag_start_rect[3] - self.CROP_MIN_SIZE - else: - # Dragging down -> move bottom border down (expand down) - new_h = max(self.CROP_MIN_SIZE, new_h + dy_r) - if new_y + new_h > rot_h: - new_h = rot_h - new_y + max_shrink = max(0, start_w - self.CROP_MIN_SIZE) + if dx_r < 0: + # Left+right drag left -> contract right edge + move = min(abs_dx, max_shrink) + new_w = start_w - move else: - # Outside crop: always contract based on drag direction - # Drag up -> contract bottom (move bottom border up) - # Drag down -> contract top (move top border down) - if dy_r < 0: - # Dragging up -> move bottom border up (contract bottom) - new_h = max(self.CROP_MIN_SIZE, new_h + dy_r) - if new_h < self.CROP_MIN_SIZE: - new_h = self.CROP_MIN_SIZE - else: - # Dragging down -> move top border down (contract top) - new_y = max(0, new_y + dy_r) - new_h = new_h - dy_r - if new_h < self.CROP_MIN_SIZE: - new_h = self.CROP_MIN_SIZE - new_y = self.crop_border_drag_start_rect[1] + self.crop_border_drag_start_rect[3] - self.CROP_MIN_SIZE - - # 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: + # Left+right drag right -> contract left edge + move = min(dx_r, max_shrink) + new_x = start_x + move + new_w = start_w - move + # Vertical drag + else: + if mode == 'expand': + if dy_r < 0: + # Drag up -> extend top edge up + move = min(abs_dy, start_y) + new_y = start_y - move + new_h = start_h + move + else: + # Drag down -> extend bottom edge down + max_move = max(0, rot_h - (start_y + start_h)) + move = min(dy_r, max_move) + new_h = start_h + move + else: + max_shrink = max(0, start_h - self.CROP_MIN_SIZE) + if dy_r < 0: + # Left+right drag up -> contract bottom edge + move = min(abs_dy, max_shrink) + new_h = start_h - move + else: + # Left+right drag down -> contract top edge + move = min(dy_r, max_shrink) + new_y = start_y + move + new_h = start_h - move + + # Clamp to bounds + new_x = max(0, min(new_x, rot_w - self.CROP_MIN_SIZE)) + new_y = max(0, min(new_y, rot_h - self.CROP_MIN_SIZE)) + new_w = max(self.CROP_MIN_SIZE, min(new_w, rot_w - new_x)) + new_h = max(self.CROP_MIN_SIZE, min(new_h, rot_h - new_y)) + + self._set_crop_from_rotated_rect((new_x, new_y, new_w, new_h)) + self.clear_transformation_cache() + self.display_current_frame() + + if event == cv2.EVENT_LBUTTONUP and self.crop_border_dragging: self.crop_border_dragging = False self.crop_border_drag_start_pos = None self.crop_border_drag_start_rect = None - self.crop_border_drag_inside = None + self.crop_drag_edge = None + self.crop_drag_mode = None self.save_state() # Handle crop selection (Shift + click and drag) @@ -3267,7 +3271,13 @@ class VideoEditor: self.zoom_center = (x, y) # Handle shift+right-click for placing tracking point at previous tracking point position - if event == cv2.EVENT_RBUTTONDOWN and (flags & cv2.EVENT_FLAG_SHIFTKEY) and not (flags & cv2.EVENT_FLAG_CTRLKEY): + # Do not trigger while left button is held (used for crop resizing) + if ( + event == cv2.EVENT_RBUTTONDOWN + and (flags & cv2.EVENT_FLAG_SHIFTKEY) + and not (flags & cv2.EVENT_FLAG_CTRLKEY) + and not self.mouse_left_down + ): if not self.is_image_mode: # Get previous tracking point position prev_result = self._get_previous_tracking_point() @@ -3346,7 +3356,12 @@ class VideoEditor: # Handle right-click for selective feature extraction when mode is active - if event == cv2.EVENT_RBUTTONDOWN and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)): + # Do not trigger while left button is held (used for crop resizing) + if ( + event == cv2.EVENT_RBUTTONDOWN + and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)) + and not self.mouse_left_down + ): if not self.is_image_mode and hasattr(self, 'selective_feature_extraction_mode') and self.selective_feature_extraction_mode: # Start selective feature extraction self.selective_feature_extraction_start = (x, y) @@ -3370,7 +3385,12 @@ class VideoEditor: self.display_needs_update = True # Handle right-click for tracking points (no modifiers) - if event == cv2.EVENT_RBUTTONDOWN and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)): + # Do not trigger while left button is held (used for crop resizing) + if ( + event == cv2.EVENT_RBUTTONDOWN + and not (flags & (cv2.EVENT_FLAG_CTRLKEY | cv2.EVENT_FLAG_SHIFTKEY)) + and not self.mouse_left_down + ): if not self.is_image_mode: # First check for template removal (like motion tracking points) if self.templates: