From e124dd3811d2fb73ee4e26fbde0b2f5cddb6e921 Mon Sep 17 00:00:00 2001 From: siraxe Date: Mon, 13 Oct 2025 12:24:54 +0100 Subject: [PATCH] accel + alternate mode --- .claude/settings.local.json | 9 - .gitignore | 3 +- CLAUDE.md | 170 ------------------ README.md | 2 +- nodes/draw_shapes.py | 12 +- nodes/power_spline_editor.py | 11 +- utility/draw_utils.py | 128 ++++++++++++- web/power_lora_loader_js/power_lora_loader.js | 17 -- web/power_spline_editor/context_menu.js | 8 +- web/power_spline_editor/persistence.js | 4 +- 10 files changed, 156 insertions(+), 208 deletions(-) delete mode 100644 .claude/settings.local.json delete mode 100644 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 9e2c6c8..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(python:*)" - ], - "deny": [], - "ask": [] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index e29b3b0..afc867b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ nodes/__pycache__/ .env2 .env 22 wanvideo_cached_samples.pt -ref_image.jpg \ No newline at end of file +ref_image.jpg +QWEN.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index e2b9f73..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,170 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Overview - -ComfyUI-WanVideoWrapper_QQ is a custom node collection for ComfyUI that provides utilities and nodes for WanVideo video generation workflows. It includes video manipulation, LoRA loading, path animation, image encoding, and visual drawing tools. - -## Architecture - -### Node Registration System - -The node system uses **dynamic class discovery** via `__init__.py`: -- Classes are imported from `/nodes/*.py` modules -- Any class with `INPUT_TYPES`, `FUNCTION`, and `CATEGORY` attributes is automatically registered -- Display names are auto-generated from class names (e.g., `WanVideoSpeed` → "Wan Video Speed (QQ)") -- Classes containing "Math" in their name are explicitly excluded (line 41 in `__init__.py:41`) -- Manual override via `DISPLAY_NAME` class attribute if custom naming is needed - -The registration creates two dictionaries: -- `NODE_CLASS_MAPPINGS`: Maps node class names to Python classes -- `NODE_DISPLAY_NAME_MAPPINGS`: Maps node class names to human-readable display names - -### Frontend Integration - -The repository includes browser-based UI components in `/web/`: -- **Power LoRA Loader**: Custom widget system (`/web/power_lora_loader_js/`) with rgthree-style UI -- **Spline Editors**: Canvas-based path drawing (`/web/spline_editor/`, `/web/power_spline_editor/`) -- **Static Assets**: External libraries in `/kjweb_async/` (served via aiohttp route at `/kjweb_async`) - -The `__init__.py` sets up an aiohttp static route to serve `/kjweb_async` content to the web interface. - -### Data Flow Patterns - -**1. Video Tensor Format** -- ComfyUI uses BHWC format: (Batch, Height, Width, Channels) -- Internal processing often converts to CHW or NTCHW for PyTorch operations -- Always verify tensor dimensions when working with video nodes - -**2. Coordinate System** -- Path coordinates are stored in JSON format with metadata: `{"coordinates": [...], "start_p_frames": N, "end_p_frames": M}` -- Two coordinate types: - - `coordinates`: Animated paths (interpolated over frames) - - `p_coordinates`: Static points (rendered on all frames, optionally driven by driver path) -- Supports scaling via `coord_width`/`coord_height` metadata when canvas size differs from output size - -**3. LoRA Loading Architecture** -- `WanVideoPowerLoraLoader` (nodes/power_lora.py) uses `FlexibleOptionalInputType` for unlimited dynamic inputs -- Returns `WANVIDLORA` type containing list of LoRA configurations with paths, strengths, and block selections -- Supports "High/Low" LoRA pair detection: automatically identifies matching low-res variants (e.g., `foo-High.safetensors` → `foo-Low.safetensors`) -- JavaScript widget manages UI state; Python backend processes the serialized widget values -- Properties like `low_mem_load` and `merge_loras` are passed via `OptionsWidget` in the workflow JSON - -## Key Node Categories - -### Video Manipulation Nodes (nodes/nodes.py) - -- **WanFrames**: Adjusts frame counts (divisible by 4) -- **WanVideoMerge**: Blends 2-3 videos with customizable transition frames -- **WanVideoSpeed**: Adjusts playback speed by resampling frames -- **WanVideoExtractFrame**: Extracts frames from video tensors -- **WanReplaceFirstFrame**: Replaces first frame with provided image - -### Image-to-Video Encoding (nodes/image_to_video.py) - -Contains the TAEHV (Tiny AutoEncoder for Hunyuan Video) implementation: -- **Memory Management**: Supports parallel (fast, high memory) and sequential (slow, low memory) processing modes -- **MemBlocks**: Temporal memory blocks that reference previous frames during encoding/decoding -- **TPool/TGrow**: Temporal pooling and growth layers for frame compression/expansion -- **WanVideoImageToVideoEncode_v2**: Main encoding node with support for start/mid/end image conditioning - -Key features: -- Temporal feathering for smooth frame transitions (cosine interpolation) -- Configurable latent strength multipliers for start/mid/end frames -- Support for dual end frames (one at custom position, one at final frame) -- Noise augmentation for improved motion in I2V tasks - -### Path Animation & Drawing (nodes/draw_shapes.py) - -- **PathFrameConfig**: Defines easing functions, frame counts, and timing for animations -- **DrawShapeOnPath**: Renders shapes (circle/square/triangle) along coordinate paths - - Supports multiple paths with per-path pause frames - - Interpolation modes: linear, cardinal (Catmull-Rom), basis (B-spline), points (no interpolation) - - Driver offset system: Apply movement from one path to transform another path - - Handles "highlighted" control points for hard corners in spline interpolation - -### Spline Editor Nodes (nodes/spline_editor.py, nodes/power_spline_editor.py) - -- SplineEditor2: Single-path editor with driver offset, rotation, and smoothing -- PowerSplineEditor: Multi-layer path editor with per-layer controls -- Both output coordinate metadata in JSON format for downstream nodes -- Frontend canvas editors in `/web/` directories - -### Utility Functions (utility/) - -- `utility.py`: Tensor/PIL/numpy conversion utilities (pil2tensor, tensor2pil, tensor2np, np2tensor) -- `draw_utils.py`: Path interpolation, easing functions, and drawing helpers -- `driver_utils.py`: Driver path offset calculations -- `numerical.py`, `magictex.py`, `fluid.py`: Mathematical/visual effects utilities - -## Common Development Tasks - -### Adding a New Node - -1. Create a new class in an appropriate file under `/nodes/` or create a new file -2. Define required class attributes: - ```python - class MyNewNode: - @classmethod - def INPUT_TYPES(cls): - return {"required": {...}, "optional": {...}} - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("output",) - FUNCTION = "process" - CATEGORY = "WanVideoWrapper_QQ" - DESCRIPTION = "..." - - def process(self, ...): - return (result,) - ``` -3. The node will auto-register on ComfyUI reload (no manual registration needed) -4. Add to NODE_CLASS_MAPPINGS/NODE_DISPLAY_NAME_MAPPINGS at end of file if creating a new module - -### Modifying Video Processing - -- Always clone tensors before modification to avoid in-place operations: `output_video = video.clone()` -- Use `torch.clamp(tensor, 0.0, 1.0)` to ensure valid pixel values -- For frame-by-frame processing, consider using `concurrent.futures.ThreadPoolExecutor` (see draw_shapes.py) -- Ensure tensor devices match before operations: `tensor.to(device=target.device, dtype=target.dtype)` - -### Working with Coordinates - -- Coordinate JSON format: `{"coordinates": [[{x, y}, ...]], "start_p_frames": {...}, "end_p_frames": {...}}` -- Pause frames can be: - - Single value: applied to all paths - - List: per-path values - - Dict with "p" and "c" keys: separate for p_coordinates and coordinates -- Interpolation helpers in `draw_utils.py`: - - `interpolate_points()`: Spline interpolation (cardinal/basis) - - `InterpMath.interpolate_or_downsample_path()`: Resampling to target frame count with easing - -### Frontend Widget Development - -- Custom widgets extend `RgthreeBaseWidget` (see power_lora_loader.js) -- Widgets must implement: - - `draw(ctx, node, width, posY, height)`: Render the widget - - `serializeValue(node, index)`: Save widget state to workflow JSON - - `mouse(event, pos, node)`: Handle mouse interactions -- Use hit areas for clickable regions: `this.hitAreas.myButton = {bounds: [x, width], onClick: this.onButtonClick}` -- Widget state is stored in `widget.value` and must be serializable to JSON - -### Testing Workflow - -1. Load ComfyUI with this custom node installed -2. Press `R` to refresh node definitions after code changes -3. Test with example workflows (if available in repository) -4. For LoRA nodes: Ensure LoRA files are in ComfyUI's `models/loras/` directory -5. For video nodes: Test with batch IMAGE tensors (BHWC format) - -## Important Notes - -- This is a **ComfyUI custom node package**, not a standalone application -- Must be installed in `ComfyUI/custom_nodes/` directory -- Requires ComfyUI's server and frontend to function -- Python nodes run in ComfyUI's backend process -- JavaScript files are loaded by ComfyUI's frontend -- The "(QQ)" suffix in node names distinguishes these nodes from similar nodes by other authors -- VAE operations expect specific stride values: `VAE_STRIDE = (4, 8, 8)` for temporal/spatial compression -- Image-to-video encoding uses 4-frame temporal grouping (num_frames must satisfy `(n-1) % 4 == 0`) diff --git a/README.md b/README.md index 2f237e6..3896088 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ https://github.com/user-attachments/assets/5689c666-924a-4362-9788-d8f662c0d925 - (EXPERIMENTAL) Maybe there's native ways to do this , but it seems to work - **Multi-frame I2V**: Start/mid/end image conditioning with position control - **Temporal feathering**: Cosine interpolation for smooth frame transitions -- **Latent strength**: Per-frame multipliers (start/mid/end) for motion control +- **Latent strength**: Per-frame multipliers (start/mid/end) for motion control (default setting work but can be adjusted) diff --git a/nodes/draw_shapes.py b/nodes/draw_shapes.py index eb59611..3d39893 100644 --- a/nodes/draw_shapes.py +++ b/nodes/draw_shapes.py @@ -435,7 +435,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in coordinates_data = json.dumps(parsed["coordinates"]) if "p_coordinates" in parsed: p_coordinates_data = json.dumps(parsed["p_coordinates"]) - for k in ("start_p_frames", "end_p_frames", "offsets", "interpolations", "easing_functions", "easing_paths", "easing_strengths", "scales", "drivers", "p_coordinates_use_driver", "p_driver_path", "p_driver_smooth", "coord_width", "coord_height"): + for k in ("start_p_frames", "end_p_frames", "offsets", "interpolations", "easing_functions", "easing_paths", "easing_strengths", "accelerations", "scales", "drivers", "p_coordinates_use_driver", "p_driver_path", "p_driver_smooth", "coord_width", "coord_height"): if k in parsed: metadata[k] = parsed[k] else: @@ -693,7 +693,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in start_p_frames_meta, end_p_frames_meta, offsets_meta, interpolations_meta, drivers_meta, easing_functions_meta, easing_paths_meta, easing_strengths_meta, - scales_meta, + scales_meta, accelerations_meta=None, coord_width: Optional[float] = None, coord_height: Optional[float] = None, frame_width: int = 512, frame_height: int = 512) -> Tuple[List[Path], List[Tuple[int, int]], List[Optional[Dict[str, Any]]], List[float]]: """ @@ -714,6 +714,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in easing_functions_list = self._normalize_easing_lists(num_paths, easing_functions_meta, "easing_function") easing_paths_list = self._normalize_easing_lists(num_paths, easing_paths_meta, "easing_path") easing_strengths_list = self._normalize_easing_lists(num_paths, easing_strengths_meta, "easing_strength") + accelerations_list = self._normalize_easing_lists(num_paths, accelerations_meta, 0.00) scales_list = self._normalize_easing_lists(num_paths, scales_meta, 1.0) processed_coords_list: List[Path] = [] @@ -769,6 +770,11 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in processed_path = draw_utils.InterpMath.interpolate_or_downsample_path( interpolated_path, path_animation_frames, path_easing_function, effective_easing_path, bounce_between=0.0, easing_strength=path_easing_strength, interpolation=path_interpolation ) + + # Apply acceleration remapping if acceleration is not zero + path_acceleration = float(accelerations_list[i]) if i < len(accelerations_list) else 0.00 + if abs(path_acceleration) > 0.001: # Only apply if acceleration is not close to zero + processed_path = draw_utils.InterpMath.apply_acceleration_remapping(processed_path, path_acceleration) # Prepare per-path driver interpolation (for per-frame offsets) driver_info_for_frame = None @@ -1007,6 +1013,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in easing_functions_meta = meta.get("easing_functions", "in_out") easing_paths_meta = meta.get("easing_paths", "full") easing_strengths_meta = meta.get("easing_strengths", 1.0) + accelerations_meta = meta.get("accelerations", 0.00) processed_coords_list, path_pause_frames, coords_driver_info_list, scales_list = self._build_interpolated_paths( coords_list_raw, total_frames, @@ -1017,6 +1024,7 @@ Locations are center locations. Allows coordinates outside the frame for 'fly-in easing_paths_meta, easing_strengths_meta, meta.get("scales", 1.0), + accelerations_meta, coord_width, coord_height, frame_width, frame_height ) diff --git a/nodes/power_spline_editor.py b/nodes/power_spline_editor.py index 6bcb712..5c4246e 100644 --- a/nodes/power_spline_editor.py +++ b/nodes/power_spline_editor.py @@ -401,6 +401,8 @@ class PowerSplineEditor: all_coord_easing_paths = [] # Initialize list for coordinates easing paths all_p_easing_strengths = [] # Initialize list for p_coordinates easing strengths all_coord_easing_strengths = [] # Initialize list for coordinates easing strengths + all_p_accelerations = [] # Initialize list for p_coordinates accelerations + all_coord_accelerations = [] # Initialize list for coordinates accelerations all_p_scales = [] # Initialize list for p_coordinates scales all_coord_scales = [] # Initialize list for coordinates scales all_p_drivers = [] # Driver info for p_coordinates @@ -456,9 +458,10 @@ class PowerSplineEditor: # Get easing parameters easing_function = spline_data.get('easing', 'in_out') # Get easing function using simple name - easing_config = spline_data.get('easingConfig', {'path': 'full', 'strength': 1.0}) # Get easing config + easing_config = spline_data.get('easingConfig', {'path': 'full', 'strength': 1.0, 'acceleration': 0.00}) # Get easing config easing_path = easing_config.get('path', 'full') # Get easing path ('each' or 'full') easing_strength = easing_config.get('strength', 1.0) # Get easing strength + acceleration = easing_config.get('acceleration', 0.00) # Get acceleration value # Get scale parameter scale = spline_data.get('scale', 1.00) # Get scale value @@ -553,6 +556,7 @@ class PowerSplineEditor: all_p_easing_functions.append(easing_function) # Collect easing function all_p_easing_paths.append(easing_path) # Collect easing path all_p_easing_strengths.append(easing_strength) # Collect easing strength + all_p_accelerations.append(acceleration) # Collect acceleration value all_p_scales.append(scale) # Collect scale value all_p_drivers.append(driver_info_for_layer) # Collect driver info (None if no driver) else: @@ -564,6 +568,7 @@ class PowerSplineEditor: all_coord_easing_functions.append(easing_function) # Collect easing function all_coord_easing_paths.append(easing_path) # Collect easing path all_coord_easing_strengths.append(easing_strength) # Collect easing strength + all_coord_accelerations.append(acceleration) # Collect acceleration value all_coord_scales.append(scale) # Collect scale value all_coord_drivers.append(driver_info_for_layer) # Collect driver info (None if no driver) @@ -622,6 +627,7 @@ class PowerSplineEditor: coord_out_data["easing_functions"] = {"p": all_p_easing_functions, "c": all_coord_easing_functions} coord_out_data["easing_paths"] = {"p": all_p_easing_paths, "c": all_coord_easing_paths} coord_out_data["easing_strengths"] = {"p": all_p_easing_strengths, "c": all_coord_easing_strengths} + coord_out_data["accelerations"] = {"p": all_p_accelerations, "c": all_coord_accelerations} coord_out_data["scales"] = {"p": all_p_scales, "c": all_coord_scales} coord_out_data["drivers"] = {"p": all_p_drivers, "c": all_coord_drivers} elif all_p_paths: @@ -633,6 +639,7 @@ class PowerSplineEditor: coord_out_data["easing_functions"] = {"p": all_p_easing_functions, "c": []} coord_out_data["easing_paths"] = {"p": all_p_easing_paths, "c": []} coord_out_data["easing_strengths"] = {"p": all_p_easing_strengths, "c": []} + coord_out_data["accelerations"] = {"p": all_p_accelerations, "c": []} coord_out_data["scales"] = {"p": all_p_scales, "c": []} coord_out_data["drivers"] = {"p": all_p_drivers, "c": []} elif all_coord_paths: @@ -644,6 +651,7 @@ class PowerSplineEditor: coord_out_data["easing_functions"] = {"p": [], "c": all_coord_easing_functions} coord_out_data["easing_paths"] = {"p": [], "c": all_coord_easing_paths} coord_out_data["easing_strengths"] = {"p": [], "c": all_coord_easing_strengths} + coord_out_data["accelerations"] = {"p": [], "c": all_coord_accelerations} coord_out_data["scales"] = {"p": [], "c": all_coord_scales} coord_out_data["drivers"] = {"p": [], "c": all_coord_drivers} else: @@ -655,6 +663,7 @@ class PowerSplineEditor: coord_out_data["easing_functions"] = [] # Default empty list for easing functions coord_out_data["easing_paths"] = [] # Default empty list for easing paths coord_out_data["easing_strengths"] = [] # Default empty list for easing strengths + coord_out_data["accelerations"] = [] # Default empty list for accelerations coord_out_data["scales"] = [] # Default empty list for scales coord_out_data["drivers"] = [] # Default empty list print("Warning: No paths to output") diff --git a/utility/draw_utils.py b/utility/draw_utils.py index bd2626b..5a0d167 100644 --- a/utility/draw_utils.py +++ b/utility/draw_utils.py @@ -48,6 +48,16 @@ class InterpMath: } apply_easing = easing_map.get(method, lambda t, s=1.0: t) + # Create inverted easing functions for "alternate" mode + inverted_easing_map = { + "linear": lambda t, s=1.0: t, + "in": lambda t, s=easing_strength: InterpMath._ease_out(t, s), + "out": lambda t, s=easing_strength: InterpMath._ease_in(t, s), + "in_out": lambda t, s=easing_strength: InterpMath._ease_out_in(t, s), + "out_in": lambda t, s=easing_strength: InterpMath._ease_in_out(t, s) + } + apply_inverted_easing = inverted_easing_map.get(method, lambda t, s=1.0: t) + total_path_length = 0 segment_lengths = [] for i in range(n_points - 1): @@ -81,6 +91,17 @@ class InterpMath: elif easing_path == 'full': if interpolation != 'linear': control_point_indices = [i for i, p in enumerate(points) if p.get('highlighted')] + elif easing_path == 'alternate': + # For alternate mode, use the same segment boundaries as 'each' mode + if interpolation == 'basis': + control_point_indices = [i for i, p in enumerate(points) if p.get('highlighted')] + # If no highlighted points for basis+each, we let control_point_indices be empty, + # which causes a fallback to 'full' path easing later. + else: + control_point_indices = [i for i, p in enumerate(points) if p.get('is_control')] + if not control_point_indices: + # Fallback for non-basis interpolations + control_point_indices = list(range(n_points)) if len(control_point_indices) > 1: if control_point_indices[0] != 0: @@ -119,7 +140,8 @@ class InterpMath: segment_frame_ranges.append({ 'segment': seg, 'start_frame': frames_allocated, - 'frame_count': num_frames + 'frame_count': num_frames, + 'segment_idx': idx # Track the segment index for alternating easing }) frames_allocated += num_frames @@ -145,7 +167,16 @@ class InterpMath: else: t_segment_local = 0.0 - t_segment_eased = apply_easing(t_segment_local, easing_strength) + # Apply normal easing or inverted easing based on the segment index for 'alternate' mode + if easing_path == 'alternate': + # Alternate between normal and inverted easing based on segment index + if target_segment_info['segment_idx'] % 2 == 0: # Even index (0, 2, 4, ...) + t_segment_eased = apply_easing(t_segment_local, easing_strength) + else: # Odd index (1, 3, 5, ...) + t_segment_eased = apply_inverted_easing(t_segment_local, easing_strength) + else: + # For 'each' and 'full' modes, use the normal easing + t_segment_eased = apply_easing(t_segment_local, easing_strength) dist_before_segment = 0 for seg in major_segments: @@ -185,6 +216,99 @@ class InterpMath: return output_points + @staticmethod + def apply_acceleration_remapping(points, acceleration): + """ + Apply acceleration-based timing remapping to a sequence of points. + + For positive acceleration (> 0.00): + - Movement is faster at the end by squishing end coordinates and stretching starting ones + + For negative acceleration (< 0.00): + - Movement is slower at the end by stretching end coordinates (and compressing starting ones) + + For zero acceleration (0.00): + - Skip the function (return original points) + + Args: + points: List of points with x, y coordinates + acceleration: Acceleration value from -1.0 to 1.0 + + Returns: + List of remapped points + """ + if abs(acceleration) < 0.001: # Close to zero, skip remapping + return points + + if len(points) <= 2: + return points + + n_points = len(points) + output_points = [] + + for i in range(n_points): + # Calculate normalized position along the path (0 to 1) + t = i / (n_points - 1) if n_points > 1 else 0 + + # Apply acceleration remapping with clamped values to prevent math errors + # Convert acceleration to proper easing behavior: + # Positive acceleration should create ease-out (start fast, end slow) + # Negative acceleration should create ease-in (start slow, end fast) + clamped_acceleration = max(-0.99, min(0.99, acceleration)) + + if clamped_acceleration > 0: + # For positive acceleration, create ease-out behavior (start fast, end slow) + # Use exponent < 1 to make time progress faster at the start and slower at the end + exponent = max(0.01, 1.0 - clamped_acceleration) + else: + # For negative acceleration, create ease-in behavior (start slow, end fast) + # Use exponent > 1 to make time progress slower at the start and faster at the end + # abs(negative_acceleration) is positive, so 1.0 + positive_value > 1 + exponent = 1.0 + abs(clamped_acceleration) + + # Handle edge cases to avoid math errors + if t == 0.0: + remap_t = 0.0 + elif t == 1.0: + remap_t = 1.0 + else: + remap_t = pow(t, exponent) + + # Ensure remap_t stays within bounds + remap_t = max(0.0, min(1.0, remap_t)) + + # Map remap_t back to original point index + original_index = remap_t * (n_points - 1) + index_low = int(original_index) + index_high = min(index_low + 1, n_points - 1) + t_local = original_index - index_low + + # Linear interpolation between the two nearest points + if index_low == index_high: + output_points.append(points[index_low].copy()) + else: + p1 = points[index_low] + p2 = points[index_high] + + x = p1['x'] * (1.0 - t_local) + p2['x'] * t_local + y = p1['y'] * (1.0 - t_local) + p2['y'] * t_local + + new_point = {'x': x, 'y': y} + + # Preserve any additional properties from the original points + for key in p1: + if key not in ['x', 'y']: + new_point[key] = p1[key] + + output_points.append(new_point) + + # Ensure the first and last points match the original to maintain path integrity + if output_points and len(output_points) > 1: + output_points[0] = points[0].copy() + output_points[-1] = points[-1].copy() + + return output_points + def interpolate_points(points, interpolation, easing_path='full', steps_per_segment=3): if not points or len(points) < 2: return points diff --git a/web/power_lora_loader_js/power_lora_loader.js b/web/power_lora_loader_js/power_lora_loader.js index 13fa7d8..ecfcf14 100644 --- a/web/power_lora_loader_js/power_lora_loader.js +++ b/web/power_lora_loader_js/power_lora_loader.js @@ -472,7 +472,6 @@ class OptionsWidget extends RgthreeBaseWidget { merge_loras: node.properties['merge_loras'] !== false ? true : false, overwrite_duplicates: node.properties['overwrite_duplicates'] || false }; - console.log(`[JS] OptionsWidget serializing: `, value); return value; } @@ -988,22 +987,6 @@ class PowerLoraLoaderWidget extends RgthreeBaseWidget { v.low_strength = 1; } - // Keep the original selected LoRA - let Python backend handle the outputs - if (v.is_low && v.low_variant_name) { - console.log(`[JS] High LoRA '${v.lora}' has low variant '${v.low_variant_name}' - sending both to backend`); - console.log(`[JS] High strength: ${v.strength}, Low strength: ${v.low_strength}`); - } - - // Enhanced debug logging - console.log(`[JS] serializeValue for ${this.name}:`, { - final_lora: v.lora, - final_strength: v.strength, - original_low_strength: v.low_strength, - is_low: v.is_low, - low_variant_name: v.low_variant_name, - full_object: v - }); - return v; } diff --git a/web/power_spline_editor/context_menu.js b/web/power_spline_editor/context_menu.js index a3d5518..e056599 100644 --- a/web/power_spline_editor/context_menu.js +++ b/web/power_spline_editor/context_menu.js @@ -507,11 +507,11 @@ export function showCustomEasingMenu(event, widget, position) { font-size: 11px !important; `; - // Create grid container (2 columns: Path | Strength) + // Create grid container (3 columns: Path | Strength | Accel) const container = document.createElement('div'); container.style.cssText = ` display: grid !important; - grid-template-columns: 1fr 1fr !important; + grid-template-columns: 1fr 1fr 1fr !important; grid-template-rows: auto auto !important; gap: 2px !important; width: 100% !important; @@ -535,10 +535,12 @@ export function showCustomEasingMenu(event, widget, position) { // Row 1: Headers container.appendChild(createHeader('Path:')); container.appendChild(createHeader('Strength:')); + container.appendChild(createHeader('Accel:')); // Row 2: Controls - container.appendChild(createDropdown(['each', 'full'], configObj.path, widget, 'path', updateEasingConfigValue)); + container.appendChild(createDropdown(['each', 'full', 'alternate'], configObj.path, widget, 'path', updateEasingConfigValue)); container.appendChild(createEasingNumberInput(widget, configObj, 'strength', 0.0, 2.0, 0.1, 1)); + container.appendChild(createEasingNumberInput(widget, configObj, 'acceleration', -1.0, 1.0, 0.01, 2)); menu.appendChild(container); document.body.appendChild(menu); diff --git a/web/power_spline_editor/persistence.js b/web/power_spline_editor/persistence.js index 4e68e8e..c47e824 100644 --- a/web/power_spline_editor/persistence.js +++ b/web/power_spline_editor/persistence.js @@ -2,7 +2,7 @@ import { app } from '../../../scripts/app.js'; // == DEFAULTS --- const DRIVEN_DEFAULTS = { driver: "", rotate: 0, d_scale: 1.0 }; -const EASING_DEFAULTS = { path: "each", strength: 1.0 }; +const EASING_DEFAULTS = { path: "each", strength: 1.0, acceleration: 0.00 }; const SCALE_DEFAULT = 1.00; @@ -128,7 +128,7 @@ export function initializeEasingConfig(widgetValue, loadedValue = null) { // Ensure the easingConfig object exists and apply defaults over the loaded data. // Handle case where easingConfig might not exist at all (new layers) if (!widgetValue.easingConfig || typeof widgetValue.easingConfig !== 'object') { - widgetValue.easingConfig = { path: "each", strength: 1.0 }; + widgetValue.easingConfig = { path: "each", strength: 1.0, acceleration: 0.00 }; } const loadedConfig = widgetValue.easingConfig;