Enhance snapping functionality in VideoEditor to include line snapping

This commit improves the snapping feature in the VideoEditor class by adding the ability to snap new tracking points to the closest point on the line segment between consecutive tracking points, in addition to existing point snapping. The distance calculation logic has been refined, and the specification has been updated to reflect the new snapping behavior, which now operates within a 10px radius for both point and line snapping, enhancing the precision of motion tracking.
This commit is contained in:
2025-09-17 15:12:35 +02:00
parent 498a1911b1
commit 68a1cc3e7d
2 changed files with 75 additions and 11 deletions

View File

@@ -1132,6 +1132,27 @@ class VideoEditor:
next_frame = min(next_frames) next_frame = min(next_frames)
return next_frame, self.tracking_points[next_frame] 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
if line_length_sq == 0:
# Line is actually a point
return ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5
# 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))
# Closest point on line segment
closest_x = x1 + t * line_dx
closest_y = y1 + t * line_dy
# Distance to closest point
return ((px - closest_x) ** 2 + (py - closest_y) ** 2) ** 0.5
def advance_frame(self) -> bool: def advance_frame(self) -> bool:
"""Advance to next frame - handles playback speed and marker looping""" """Advance to next frame - handles playback speed and marker looping"""
if not self.is_playing: if not self.is_playing:
@@ -2155,20 +2176,62 @@ class VideoEditor:
removed = True removed = True
break break
# If not removed, check for snapping to nearby points from other frames # If not removed, check for snapping to nearby points or lines from other frames
if not removed: if not removed:
snapped = False snapped = False
# Check all tracking points from all frames for snapping best_snap_distance = float('inf')
best_snap_point = None
# Check all tracking points from all frames for point snapping
for _, points in self.tracking_points.items(): for _, points in self.tracking_points.items():
for (px, py) in points: for (px, py) in points:
sxp, syp = self._map_rotated_to_screen(px, py) sxp, syp = self._map_rotated_to_screen(px, py)
if (sxp - x) ** 2 + (syp - y) ** 2 <= threshold ** 2: distance = ((sxp - x) ** 2 + (syp - y) ** 2) ** 0.5
# Snap to this existing point if distance <= threshold and distance < best_snap_distance:
self.tracking_points.setdefault(self.current_frame, []).append((int(px), int(py))) best_snap_distance = distance
snapped = True best_snap_point = (int(px), int(py))
break
if snapped: # Check for line snapping between consecutive tracking points
break 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 each corresponding pair of points
for j in range(min(len(points1), len(points2))):
px1, py1 = points1[j]
px2, py2 = points2[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)
if line_distance <= threshold and line_distance < best_snap_distance:
# Find the closest point on the line segment
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
# Convert back to rotated coordinates
closest_rx, closest_ry = self._map_screen_to_rotated(int(closest_sx), int(closest_sy))
best_snap_distance = line_distance
best_snap_point = (int(closest_rx), int(closest_ry))
# Apply the best snap if found
if best_snap_point:
self.tracking_points.setdefault(self.current_frame, []).append(best_snap_point)
snapped = True
# If no snapping, add new point at clicked location # If no snapping, add new point at clicked location
if not snapped: if not snapped:

View File

@@ -57,8 +57,9 @@ Be careful to save and load settings when navigating this way
### Motion Tracking ### Motion Tracking
- **Right-click**: Add tracking point (green circle with white border) - **Right-click**: Add tracking point (green circle with white border)
- **Right-click existing point**: Remove tracking point (within 50px) - **Right-click existing point**: Remove tracking point (within 10px)
- **Right-click near existing point**: Snap to existing point from any frame (within 50px radius) - **Right-click near existing point**: Snap to existing point from any frame (within 10px radius)
- **Right-click near motion path**: Snap to closest point on yellow arrow line between tracking points (within 10px radius)
- **v**: Toggle motion tracking on/off - **v**: Toggle motion tracking on/off
- **V**: Clear all tracking points - **V**: Clear all tracking points
- **Blue cross**: Shows computed tracking position - **Blue cross**: Shows computed tracking position