diff --git a/croppa/main.py b/croppa/main.py index 42cb87b..215462f 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1201,14 +1201,18 @@ class VideoEditor: def _map_original_to_screen(self, ox, oy): """Map a point in original frame coords to canvas screen coords.""" - cx, cy, cw, ch = self._get_effective_crop_rect_for_frame(getattr(self, 'current_frame', 0)) + frame_number = getattr(self, 'current_frame', 0) + cx, cy, cw, ch = self._get_effective_crop_rect_for_frame(frame_number) + # Relative to effective crop px = ox - cx py = oy - cy angle = self.rotation_angle + # Dimensions after rotation if angle in (90, 270): rotated_w, rotated_h = ch, cw else: rotated_w, rotated_h = cw, ch + # Forward rotation mapping if angle == 90: rx, ry = py, rotated_w - px elif angle == 180: @@ -1217,51 +1221,68 @@ class VideoEditor: rx, ry = rotated_h - py, px else: rx, ry = px, py + # Zoom zx = rx * self.zoom_factor zy = ry * self.zoom_factor - base_w, base_h = rotated_w, rotated_h - disp_w = int(base_w * self.zoom_factor) - disp_h = int(base_h * self.zoom_factor) + # Apply display offset cropping in zoomed space + new_w = int(rotated_w * self.zoom_factor) + new_h = int(rotated_h * self.zoom_factor) + offx_max = max(0, new_w - self.window_width) + offy_max = max(0, new_h - self.window_height) + offx = max(0, min(int(self.display_offset[0]), offx_max)) + offy = max(0, min(int(self.display_offset[1]), offy_max)) + inframe_x = zx - offx + inframe_y = zy - offy + # Size of processed_frame from apply_crop_zoom_and_rotation + base_w = new_w if new_w <= self.window_width else self.window_width + base_h = new_h if new_h <= self.window_height else self.window_height + # Final scale and canvas placement available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) - scale = min(self.window_width / max(1, disp_w), available_height / max(1, disp_h)) - if scale < 1.0: - final_w = int(disp_w * scale) - final_h = int(disp_h * scale) - else: - final_w = disp_w - final_h = disp_h - scale = 1.0 - start_x = (self.window_width - final_w) // 2 - start_y = (available_height - final_h) // 2 - sx = int(round(start_x + zx * scale)) - sy = int(round(start_y + zy * scale)) + scale = min(self.window_width / max(1, base_w), available_height / max(1, base_h)) + final_w = int(base_w * scale) + final_h = int(base_h * scale) + start_x_canvas = (self.window_width - final_w) // 2 + start_y_canvas = (available_height - final_h) // 2 + sx = int(round(start_x_canvas + inframe_x * scale)) + sy = int(round(start_y_canvas + inframe_y * scale)) return sx, sy def _map_screen_to_original(self, sx, sy): """Map a point on canvas screen coords back to original frame coords.""" - cx, cy, cw, ch = self._get_effective_crop_rect_for_frame(getattr(self, 'current_frame', 0)) + frame_number = getattr(self, 'current_frame', 0) + cx, cy, cw, ch = self._get_effective_crop_rect_for_frame(frame_number) angle = self.rotation_angle + # Dimensions after rotation if angle in (90, 270): rotated_w, rotated_h = ch, cw else: rotated_w, rotated_h = cw, ch - disp_w = int(rotated_w * self.zoom_factor) - disp_h = int(rotated_h * self.zoom_factor) + # Zoomed dimensions and base processed dimensions (after window cropping) + new_w = int(rotated_w * self.zoom_factor) + new_h = int(rotated_h * self.zoom_factor) + base_w = new_w if new_w <= self.window_width else self.window_width + base_h = new_h if new_h <= self.window_height else self.window_height + # Final scaling used in display available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) - scale = min(self.window_width / max(1, disp_w), available_height / max(1, disp_h)) - if scale < 1.0: - final_w = int(disp_w * scale) - final_h = int(disp_h * scale) - else: - final_w = disp_w - final_h = disp_h - scale = 1.0 - start_x = (self.window_width - final_w) // 2 - start_y = (available_height - final_h) // 2 - zx = (sx - start_x) / max(1e-6, scale) - zy = (sy - start_y) / max(1e-6, scale) + scale = min(self.window_width / max(1, base_w), available_height / max(1, base_h)) + final_w = int(base_w * scale) + final_h = int(base_h * scale) + start_x_canvas = (self.window_width - final_w) // 2 + start_y_canvas = (available_height - final_h) // 2 + # Back to processed (zoomed+cropped) space + zx = (sx - start_x_canvas) / max(1e-6, scale) + zy = (sy - start_y_canvas) / max(1e-6, scale) + # Add display offset in zoomed space + offx_max = max(0, new_w - self.window_width) + offy_max = max(0, new_h - self.window_height) + offx = max(0, min(int(self.display_offset[0]), offx_max)) + offy = max(0, min(int(self.display_offset[1]), offy_max)) + zx += offx + zy += offy + # Reverse zoom rx = zx / max(1e-6, self.zoom_factor) ry = zy / max(1e-6, self.zoom_factor) + # Reverse rotation if angle == 90: px, py = rotated_w - ry, rx elif angle == 180: @@ -1270,6 +1291,7 @@ class VideoEditor: px, py = ry, rotated_h - rx else: px, py = rx, ry + # Back to original frame ox = px + cx oy = py + cy ox = max(0, min(int(round(ox)), self.frame_width - 1))