Files
siraxe-ComfyUI-WanVideoWrap…/utility/driver_utils.py
T
siraxe 04b8a1b9cc driver/driven update 1
w1
smoothing works
2025-10-10 14:39:56 +01:00

261 lines
7.4 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 by scaling the path relative to its first point.
Args:
path: List of coordinate dicts with 'x' and 'y' keys
smooth_factor: Smoothing factor from 0.0 (no effect) to 1.0 (path collapsed to first point)
Returns:
Smoothed path (new list of dicts)
"""
if not path or len(path) == 0 or smooth_factor is None:
return [p.copy() for p in path]
# Clamp smooth factor
smooth_factor = max(0.0, min(1.0, smooth_factor))
if smooth_factor == 0.0:
return [p.copy() for p in path]
scale_factor = 1.0 - smooth_factor
pivot_point = path[0]
pivot_x = float(pivot_point['x'])
pivot_y = float(pivot_point['y'])
smoothed = []
for point in path:
px = float(point['x'])
py = float(point['y'])
# Vector from pivot to point
vec_x = px - pivot_x
vec_y = py - pivot_y
# Scale the vector
scaled_vec_x = vec_x * scale_factor
scaled_vec_y = vec_y * scale_factor
# New point position
new_x = pivot_x + scaled_vec_x
new_y = pivot_y + scaled_vec_y
new_point = {
'x': new_x,
'y': new_y
}
# Preserve other properties
for key in point:
if key not in ('x', 'y'):
new_point[key] = point[key]
smoothed.append(new_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