diff --git a/croppa/main.py b/croppa/main.py index 1c53349..7804abe 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1132,26 +1132,31 @@ class VideoEditor: next_frame = min(next_frames) return next_frame, self.tracking_points[next_frame] - def _point_to_line_distance(self, px, py, x1, y1, x2, y2): - """Calculate distance from point (px, py) to line segment (x1, y1) to (x2, y2)""" - # Vector from line start to end - line_dx = x2 - x1 - line_dy = y2 - y1 - line_length_sq = line_dx * line_dx + line_dy * line_dy + def _point_to_line_distance_and_foot(self, px, py, x1, y1, x2, y2): + """Calculate distance from point (px, py) to infinite line (x1, y1) to (x2, y2) and return foot of perpendicular""" + # Convert line to general form: Ax + By + C = 0 + # (y2 - y1)(x - x1) - (x2 - x1)(y - y1) = 0 + A = y2 - y1 + B = -(x2 - x1) # Note the negative sign + C = -(A * x1 + B * y1) - if line_length_sq == 0: + # Calculate distance: d = |Ax + By + C| / sqrt(A^2 + B^2) + denominator = (A * A + B * B) ** 0.5 + if denominator == 0: # Line is actually a point - return ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5 + distance = ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5 + return distance, (x1, y1) - # Parameter t for closest point on line (0 = start, 1 = end) - t = max(0, min(1, ((px - x1) * line_dx + (py - y1) * line_dy) / line_length_sq)) + distance = abs(A * px + B * py + C) / denominator - # Closest point on line segment - closest_x = x1 + t * line_dx - closest_y = y1 + t * line_dy + # Calculate foot of perpendicular: (xf, yf) + # xf = xu - A(Axu + Byu + C)/(A^2 + B^2) + # yf = yu - B(Axu + Byu + C)/(A^2 + B^2) + numerator = A * px + B * py + C + xf = px - A * numerator / (A * A + B * B) + yf = py - B * numerator / (A * A + B * B) - # Distance to closest point - return ((px - closest_x) ** 2 + (py - closest_y) ** 2) ** 0.5 + return distance, (xf, yf) def advance_frame(self) -> bool: """Advance to next frame - handles playback speed and marker looping""" @@ -2183,7 +2188,7 @@ class VideoEditor: best_snap_point = None # Check all tracking points from all frames for point snapping - for _, points in self.tracking_points.items(): + for frame_num, points in self.tracking_points.items(): for (px, py) in points: sxp, syp = self._map_rotated_to_screen(px, py) distance = ((sxp - x) ** 2 + (syp - y) ** 2) ** 0.5 @@ -2191,50 +2196,76 @@ class VideoEditor: best_snap_distance = distance best_snap_point = (int(px), int(py)) - # Check for line snapping between consecutive tracking points - tracking_frames = sorted(self.tracking_points.keys()) - for i in range(len(tracking_frames) - 1): - frame1 = tracking_frames[i] - frame2 = tracking_frames[i + 1] - points1 = self.tracking_points[frame1] - points2 = self.tracking_points[frame2] + # Check for line snapping between previous and next tracking points (the actual cyan arrows) + prev_result = self._get_previous_tracking_point() + next_result = self._get_next_tracking_point() + + print(f"DEBUG: Line snapping - prev_result: {prev_result}, next_result: {next_result}") + + if prev_result and next_result: + prev_frame, prev_pts = prev_result + next_frame, next_pts = next_result - # Check each corresponding pair of points - for j in range(min(len(points1), len(points2))): - px1, py1 = points1[j] - px2, py2 = points2[j] + print(f"DEBUG: Checking line between prev frame {prev_frame} and next frame {next_frame}") + + # Check each corresponding pair of points between previous and next + for j in range(min(len(prev_pts), len(next_pts))): + px1, py1 = prev_pts[j] + px2, py2 = next_pts[j] # Convert to screen coordinates sx1, sy1 = self._map_rotated_to_screen(px1, py1) sx2, sy2 = self._map_rotated_to_screen(px2, py2) - # Calculate distance to line segment - line_distance = self._point_to_line_distance(x, y, sx1, sy1, sx2, sy2) + # Calculate distance to infinite line and foot of perpendicular + line_distance, (foot_x, foot_y) = self._point_to_line_distance_and_foot(x, y, sx1, sy1, sx2, sy2) + + print(f"DEBUG: Line {j}: ({sx1},{sy1}) to ({sx2},{sy2}), distance to click ({x},{y}) = {line_distance:.2f}, foot = ({foot_x:.1f}, {foot_y:.1f})") if line_distance <= threshold and line_distance < best_snap_distance: - # Find the closest point on the line segment + print(f"DEBUG: Line snap found! Distance {line_distance:.2f} <= threshold {threshold}") + + # Clamp the foot to the line segment + # Calculate parameter t for the foot point line_dx = sx2 - sx1 line_dy = sy2 - sy1 line_length_sq = line_dx * line_dx + line_dy * line_dy if line_length_sq > 0: - t = max(0, min(1, ((x - sx1) * line_dx + (y - sy1) * line_dy) / line_length_sq)) - closest_sx = sx1 + t * line_dx - closest_sy = sy1 + t * line_dy + t = ((foot_x - sx1) * line_dx + (foot_y - sy1) * line_dy) / line_length_sq + t = max(0, min(1, t)) # Clamp to [0, 1] - # Convert back to rotated coordinates - closest_rx, closest_ry = self._map_screen_to_rotated(int(closest_sx), int(closest_sy)) + # Calculate clamped foot point + clamped_foot_x = sx1 + t * line_dx + clamped_foot_y = sy1 + t * line_dy + + print(f"DEBUG: Clamped foot from ({foot_x:.1f}, {foot_y:.1f}) to ({clamped_foot_x:.1f}, {clamped_foot_y:.1f})") + + # Convert clamped foot back to rotated coordinates + closest_rx, closest_ry = self._map_screen_to_rotated(int(clamped_foot_x), int(clamped_foot_y)) best_snap_distance = line_distance best_snap_point = (int(closest_rx), int(closest_ry)) + print(f"DEBUG: Best line snap point: ({closest_rx}, {closest_ry})") + else: + print(f"DEBUG: No prev/next points found for line snapping") # Apply the best snap if found if best_snap_point: + print(f"DEBUG: Final best_snap_point: {best_snap_point} (distance: {best_snap_distance:.2f})") self.tracking_points.setdefault(self.current_frame, []).append(best_snap_point) snapped = True + else: + print(f"DEBUG: No snap found, adding new point at: ({rx}, {ry})") # If no snapping, add new point at clicked location if not snapped: + print(f"DEBUG: No snap found, adding new point at: ({rx}, {ry})") + print(f"DEBUG: Click was at screen coords: ({x}, {y})") + print(f"DEBUG: Converted to rotated coords: ({rx}, {ry})") + # Verify the conversion + verify_sx, verify_sy = self._map_rotated_to_screen(rx, ry) + print(f"DEBUG: Verification - rotated ({rx}, {ry}) -> screen ({verify_sx}, {verify_sy})") self.tracking_points.setdefault(self.current_frame, []).append((int(rx), int(ry))) # self.show_feedback_message("Tracking point added")