diff --git a/croppa/main.py b/croppa/main.py index 64f1172..fe03f66 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1195,6 +1195,38 @@ class VideoEditor: return (x1 + t * (x2 - x1), y1 + t * (y2 - y1)) return None + def _get_display_params(self): + """Unified display transform parameters for current frame in rotated space.""" + eff_x, eff_y, eff_w, eff_h = self._get_effective_crop_rect_for_frame(getattr(self, 'current_frame', 0)) + new_w = int(eff_w * self.zoom_factor) + new_h = int(eff_h * self.zoom_factor) + cropped_due_to_zoom = (self.zoom_factor != 1.0) and (new_w > self.window_width or new_h > self.window_height) + if cropped_due_to_zoom: + 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)) + visible_w = self.window_width + visible_h = self.window_height + else: + offx = 0 + offy = 0 + visible_w = new_w + visible_h = new_h + available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) + scale_raw = min(self.window_width / max(1, visible_w), available_height / max(1, visible_h)) + scale = scale_raw if scale_raw < 1.0 else 1.0 + final_w = int(visible_w * scale) + final_h = int(visible_h * scale) + start_x = (self.window_width - final_w) // 2 + start_y = (available_height - final_h) // 2 + return { + 'eff_x': eff_x, 'eff_y': eff_y, 'eff_w': eff_w, 'eff_h': eff_h, + 'offx': offx, 'offy': offy, + 'scale': scale, + 'start_x': start_x, 'start_y': start_y + } + def _map_original_to_screen(self, ox, oy): """Map a point in original frame coords to canvas screen coords.""" frame_number = getattr(self, 'current_frame', 0) @@ -1208,46 +1240,16 @@ class VideoEditor: rx, ry = self.frame_height - 1 - oy, ox else: rx, ry = ox, oy - # Now account for crop in rotated space - cx, cy, cw, ch = self._get_effective_crop_rect_for_frame(frame_number) - rx -= cx - ry -= cy - # Dimensions after rotation - if angle in (90, 270): - rotated_w, rotated_h = ch, cw - else: - rotated_w, rotated_h = cw, ch - # Zoom + # Now account for crop/zoom/offset using unified params + params = self._get_display_params() + rx -= params['eff_x'] + ry -= params['eff_y'] zx = rx * self.zoom_factor zy = ry * self.zoom_factor - # Zoomed dimensions - new_w = int(rotated_w * self.zoom_factor) - new_h = int(rotated_h * self.zoom_factor) - # Whether apply_crop_zoom_and_rotation cropped due to zoom - cropped_due_to_zoom = (self.zoom_factor != 1.0) and (new_w > self.window_width or new_h > self.window_height) - # Display offset in zoomed space only applies if cropped_due_to_zoom - if cropped_due_to_zoom: - 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)) - else: - offx = 0 - offy = 0 - inframe_x = zx - offx - inframe_y = zy - offy - # Visible dimensions before canvas scaling (what display_current_frame receives) - visible_w = new_w if not cropped_due_to_zoom else min(new_w, self.window_width) - visible_h = new_h if not cropped_due_to_zoom else min(new_h, self.window_height) - # Canvas scale and placement (matches display_current_frame downscale and centering) - available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) - scale_canvas = min(self.window_width / max(1, visible_w), available_height / max(1, visible_h)) - final_w = int(visible_w * scale_canvas) - final_h = int(visible_h * scale_canvas) - 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_canvas)) - sy = int(round(start_y_canvas + inframe_y * scale_canvas)) + inframe_x = zx - params['offx'] + inframe_y = zy - params['offy'] + sx = int(round(params['start_x'] + inframe_x * params['scale'])) + sy = int(round(params['start_y'] + inframe_y * params['scale'])) return sx, sy def _map_screen_to_original(self, sx, sy): @@ -1269,7 +1271,8 @@ class VideoEditor: visible_h = new_h if not cropped_due_to_zoom else min(new_h, self.window_height) # Canvas scale and placement available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) - scale_canvas = min(self.window_width / max(1, visible_w), available_height / max(1, visible_h)) + scale_raw = min(self.window_width / max(1, visible_w), available_height / max(1, visible_h)) + scale_canvas = scale_raw if scale_raw < 1.0 else 1.0 final_w = int(visible_w * scale_canvas) final_h = int(visible_h * scale_canvas) start_x_canvas = (self.window_width - final_w) // 2 @@ -1345,38 +1348,19 @@ class VideoEditor: """Map a point on canvas screen coords back to ROTATED frame coords (pre-crop).""" frame_number = getattr(self, 'current_frame', 0) angle = self.rotation_angle - # Use EFFECTIVE crop dims as the displayed base after rotation - cx, cy, cw, ch = self._get_effective_crop_rect_for_frame(frame_number) - new_w = int(cw * self.zoom_factor) - new_h = int(ch * self.zoom_factor) - cropped_due_to_zoom = (self.zoom_factor != 1.0) and (new_w > self.window_width or new_h > self.window_height) - visible_w = new_w if not cropped_due_to_zoom else min(new_w, self.window_width) - visible_h = new_h if not cropped_due_to_zoom else min(new_h, self.window_height) - available_height = self.window_height - (0 if self.is_image_mode else self.TIMELINE_HEIGHT) - scale_canvas = min(self.window_width / max(1, visible_w), available_height / max(1, visible_h)) - final_w = int(visible_w * scale_canvas) - final_h = int(visible_h * scale_canvas) - start_x_canvas = (self.window_width - final_w) // 2 - start_y_canvas = (available_height - final_h) // 2 + # Use unified display params + params = self._get_display_params() # Back to processed (zoomed+cropped) space - zx = (sx - start_x_canvas) / max(1e-6, scale_canvas) - zy = (sy - start_y_canvas) / max(1e-6, scale_canvas) - if cropped_due_to_zoom: - 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)) - else: - offx = 0 - offy = 0 - zx += offx - zy += offy + zx = (sx - params['start_x']) / max(1e-6, params['scale']) + zy = (sy - params['start_y']) / max(1e-6, params['scale']) + zx += params['offx'] + zy += params['offy'] # Reverse zoom rx = zx / max(1e-6, self.zoom_factor) ry = zy / max(1e-6, self.zoom_factor) # Unapply current EFFECTIVE crop to get PRE-crop rotated coords - rx = rx + cx - ry = ry + cy + rx = rx + params['eff_x'] + ry = ry + params['eff_y'] return int(round(rx)), int(round(ry)) def clear_transformation_cache(self):