Files
siraxe-ComfyUI-WanVideoWrap…/utility/driver_utils.py
T
2025-10-09 19:27:37 +01:00

262 lines
7.6 KiB
Python

"""
Driver utilities for spline coordinate transformation.
Used by PowerSplineEditor to apply coordinate driving between layers.
"""
import math
def rotate_path(path, angle_degrees, pivot=None):
"""
Rotate a path of coordinates around a pivot point.
Args:
path: List of coordinate dicts with 'x' and 'y' keys
angle_degrees: Rotation angle in degrees (positive = counterclockwise)
pivot: Pivot point dict with 'x' and 'y' keys. If None, uses first point of path.
Returns:
Rotated path (new list of dicts)
"""
if not path or len(path) == 0:
return []
if angle_degrees == 0:
return [p.copy() for p in path]
# Use first point as pivot if not specified
if pivot is None:
pivot = path[0]
pivot_x = float(pivot['x'])
pivot_y = float(pivot['y'])
angle_rad = math.radians(angle_degrees)
cos_a = math.cos(angle_rad)
sin_a = math.sin(angle_rad)
rotated = []
for point in path:
px = float(point['x'])
py = float(point['y'])
# Translate to origin
rel_x = px - pivot_x
rel_y = py - pivot_y
# Rotate
new_rel_x = rel_x * cos_a - rel_y * sin_a
new_rel_y = rel_x * sin_a + rel_y * cos_a
# Translate back
rotated_point = {
'x': new_rel_x + pivot_x,
'y': new_rel_y + pivot_y
}
# Preserve other properties
for key in point:
if key not in ('x', 'y'):
rotated_point[key] = point[key]
rotated.append(rotated_point)
return rotated
def smooth_path(path, smooth_factor):
"""
Apply smoothing to a path using simple moving average.
Args:
path: List of coordinate dicts with 'x' and 'y' keys
smooth_factor: Smoothing factor from 0.0 (no smoothing) to 1.0 (maximum smoothing)
Returns:
Smoothed path (new list of dicts)
"""
if not path or len(path) <= 2 or smooth_factor <= 0.0:
return [p.copy() for p in path]
# Clamp smooth factor
smooth_factor = max(0.0, min(1.0, smooth_factor))
# Calculate window size based on smooth factor
# smooth_factor 0.0 → window size 1 (no smoothing)
# smooth_factor 1.0 → window size ~20% of path length
max_window = max(3, int(len(path) * 0.2))
window_size = max(1, int(1 + (max_window - 1) * smooth_factor))
if window_size <= 1:
return [p.copy() for p in path]
smoothed = []
half_window = window_size // 2
for i in range(len(path)):
# Calculate window bounds
start_idx = max(0, i - half_window)
end_idx = min(len(path), i + half_window + 1)
# Average coordinates in window
sum_x = 0.0
sum_y = 0.0
count = 0
for j in range(start_idx, end_idx):
sum_x += float(path[j]['x'])
sum_y += float(path[j]['y'])
count += 1
smoothed_point = {
'x': sum_x / count,
'y': sum_y / count
}
# Preserve other properties from original point
for key in path[i]:
if key not in ('x', 'y'):
smoothed_point[key] = path[i][key]
smoothed.append(smoothed_point)
return smoothed
def interpolate_path(path, target_frames):
"""
Linearly interpolate a path to a target number of frames.
Args:
path: List of coordinate dicts with 'x' and 'y' keys
target_frames: Desired number of frames
Returns:
Interpolated path with target_frames points
"""
n_coords = len(path)
if target_frames <= 0:
print("driver_utils Warning: target_frames is 0 or negative. Returning original path.")
return [p.copy() for p in path]
if n_coords == target_frames:
return [p.copy() for p in path]
if n_coords == 0:
print("driver_utils Warning: Cannot interpolate empty path.")
return []
if n_coords == 1:
# Single point - duplicate it
return [path[0].copy() for _ in range(target_frames)]
if target_frames == 1:
# Return first point only
return [path[0].copy()]
interpolated = []
# Ensure coordinates are floats
float_coords = []
try:
for p in path:
float_coords.append({'x': float(p['x']), 'y': float(p['y'])})
except (KeyError, ValueError) as e:
print(f"driver_utils Error: Invalid coordinate format - {e}")
return [p.copy() for p in path]
for i in range(target_frames):
# Calculate position in original path
pos = i * (n_coords - 1) / (target_frames - 1)
idx1 = math.floor(pos)
idx2 = math.ceil(pos)
if idx1 == idx2:
interpolated.append(float_coords[idx1].copy())
else:
# Linear interpolation
t = pos - idx1
p1 = float_coords[idx1]
p2 = float_coords[idx2]
new_x = p1['x'] * (1.0 - t) + p2['x'] * t
new_y = p1['y'] * (1.0 - t) + p2['y'] * t
interpolated.append({'x': new_x, 'y': new_y})
return interpolated
def apply_driver_offset(driven_coords, driver_coords, rotate=0, smooth=0.0):
"""
Apply driver coordinate transformation to driven coordinates.
This is the main function that orchestrates the driver logic:
1. Rotate driver path if rotate != 0
2. Smooth driver path if smooth > 0.0
3. Interpolate driver path to match driven length
4. Calculate offset from driver's first point
5. Apply offset to each driven coordinate
Args:
driven_coords: List of coordinate dicts to be driven
driver_coords: List of coordinate dicts from the driver layer
rotate: Rotation angle in degrees
smooth: Smoothing factor (0.0 to 1.0)
Returns:
New list of driven coordinates with driver offset applied
"""
if not driven_coords or len(driven_coords) == 0:
return []
if not driver_coords or len(driver_coords) == 0:
print("driver_utils Warning: No driver coordinates provided. Returning original driven coords.")
return [c.copy() for c in driven_coords]
# Step 1: Apply rotation to driver path
transformed_driver = driver_coords
if rotate != 0:
transformed_driver = rotate_path(transformed_driver, rotate)
# Step 2: Apply smoothing to driver path
if smooth > 0.0:
transformed_driver = smooth_path(transformed_driver, smooth)
# Step 3: Interpolate driver to match driven length
driven_length = len(driven_coords)
interpolated_driver = interpolate_path(transformed_driver, driven_length)
if not interpolated_driver or len(interpolated_driver) == 0:
print("driver_utils Warning: Driver interpolation failed. Returning original driven coords.")
return [c.copy() for c in driven_coords]
# Step 4: Calculate reference offset from driver's first point
ref_offset_x = float(interpolated_driver[0]['x'])
ref_offset_y = float(interpolated_driver[0]['y'])
# Step 5: Apply offset to each driven coordinate
result = []
for i in range(driven_length):
driven_point = driven_coords[i]
driver_point = interpolated_driver[i]
# Calculate offset
offset_x = float(driver_point['x']) - ref_offset_x
offset_y = float(driver_point['y']) - ref_offset_y
# Apply offset to driven
new_point = {
'x': float(driven_point['x']) + offset_x,
'y': float(driven_point['y']) + offset_y
}
# Preserve other properties from driven point
for key in driven_point:
if key not in ('x', 'y'):
new_point[key] = driven_point[key]
result.append(new_point)
return result