From f0d38fd9c6749ab7b6e565a8a2db9d225aec9f8f Mon Sep 17 00:00:00 2001 From: JJ Date: Sun, 15 Mar 2026 01:30:29 +0000 Subject: [PATCH] fix --- .gitignore | 1 + nodes/draw_image.py | 38 +++++++++---------- nodes/power_spline_editor.py | 27 +++++++++++-- utility/rgthree_api/utils_info.py | 19 +++------- .../canvas/canvas_box_timeline.js | 6 +-- web/power_spline_editor/layer_type_box.js | 29 ++++++++++++++ .../power_spline_editor.js | 38 ++++++++++++++++++- 7 files changed, 116 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index 42cc213..ff37a74 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ text_embed_cache/ web/power_spline_editor/bg/ref_image_0.png web/power_spline_editor/ref/ /web/power_spline_editor/bg +utility/matanyone2/checkpoints/matanyone2.pth diff --git a/nodes/draw_image.py b/nodes/draw_image.py index 7a85d5c..279c859 100644 --- a/nodes/draw_image.py +++ b/nodes/draw_image.py @@ -8,7 +8,7 @@ import torch from PIL import Image from ..utility.utility import pil2tensor, tensor2pil -from ..config.constants import BOX_BASE_SIZE +from ..config.constants import BOX_BASE_RADIUS class DrawImageOnPath: @@ -179,25 +179,24 @@ Draws an input image along a coordinate path for each frame, returning the rende def _compute_target_size(self, base_w, base_h, scale_factor, frame_width, frame_height, use_box_scale_size, editor_scale=1.0): if use_box_scale_size: - # Apply editor_scale to match what was shown in the Power Spline Editor canvas - # The editor displays boxes at: BOX_BASE_RADIUS * scale * editor_scale - # So the final size should be: BOX_BASE_SIZE * scale * editor_scale + # Match the editor's formula: box_radius = BOX_BASE_RADIUS * pointScale * canvasScale + # BOX_BASE_RADIUS = 200, so diameter = 400 * pointScale * canvasScale + # We map this from canvas space to video space using editor_scale adjusted_scale = scale_factor * editor_scale - target_short = BOX_BASE_SIZE * adjusted_scale - ratio = target_short / max(1.0, min(base_w, base_h)) - new_w = int(round(base_w * ratio)) - new_h = int(round(base_h * ratio)) + box_diameter = (BOX_BASE_RADIUS * 2) * adjusted_scale + # Calculate the scale factor to fit the image within the box diameter + # This matches the editor's: scale = Math.min(boxSize / imgW, boxSize / imgH) + scale_w = box_diameter / max(1.0, base_w) + scale_h = box_diameter / max(1.0, base_h) + fit_scale = min(scale_w, scale_h) + new_w = int(round(base_w * fit_scale)) + new_h = int(round(base_h * fit_scale)) else: new_w = int(round(base_w * scale_factor)) new_h = int(round(base_h * scale_factor)) - max_size_fraction = 1.0 - max_w = int(frame_width * max_size_fraction) - max_h = int(frame_height * max_size_fraction) - if new_w > max_w or new_h > max_h: - clamp_ratio = min(max_w / max(1, new_w), max_h / max(1, new_h)) - new_w = max(1, int(new_w * clamp_ratio)) - new_h = max(1, int(new_h * clamp_ratio)) + # REMOVED: max_size_fraction clamp that was preventing scale variation + # The clamp was forcing all large images to the same size, making scale changes invisible new_w = max(1, new_w) new_h = max(1, new_h) @@ -380,7 +379,6 @@ Draws an input image along a coordinate path for each frame, returning the rende pos_x, pos_y = self._scale_point(point, frame_width, frame_height, coord_width, coord_height) scale_factor = self._get_scale(point, fallback_scale) - new_w, new_h = self._compute_target_size( base_w, base_h, scale_factor, frame_width, frame_height, use_box_scale_size, editor_scale ) @@ -439,9 +437,9 @@ Draws an input image along a coordinate path for each frame, returning the rende rotation_deg = -math.degrees(rotation_rad) if abs(rotation_deg) > 1e-4: - ref_img = ref_img.rotate(rotation_deg, expand=True) + ref_img = ref_img.rotate(rotation_deg, resample=Image.Resampling.BICUBIC, expand=True) if mask_img is not None: - mask_img = mask_img.rotate(rotation_deg, expand=True) + mask_img = mask_img.rotate(rotation_deg, resample=Image.Resampling.BICUBIC, expand=True) paste_x = int(round(pos_x - ref_img.width / 2)) paste_y = int(round(pos_y - ref_img.height / 2)) @@ -548,9 +546,9 @@ Draws an input image along a coordinate path for each frame, returning the rende rotation_deg = -math.degrees(rotation_rad) if abs(rotation_deg) > 1e-4: - ref_img = ref_img.rotate(rotation_deg, expand=True) + ref_img = ref_img.rotate(rotation_deg, resample=Image.Resampling.BICUBIC, expand=True) if mask_img is not None: - mask_img = mask_img.rotate(rotation_deg, expand=True) + mask_img = mask_img.rotate(rotation_deg, resample=Image.Resampling.BICUBIC, expand=True) paste_x = int(round(pos_x - ref_img.width / 2)) paste_y = int(round(pos_y - ref_img.height / 2)) diff --git a/nodes/power_spline_editor.py b/nodes/power_spline_editor.py index 1735099..16edadc 100644 --- a/nodes/power_spline_editor.py +++ b/nodes/power_spline_editor.py @@ -27,8 +27,8 @@ class PowerSplineEditor: } } - RETURN_TYPES = ("IMAGE", "STRING", "INT",) - RETURN_NAMES = ("bg_image", "coord_out", "frames",) + RETURN_TYPES = ("IMAGE", "STRING", "INT", "STRING",) + RETURN_NAMES = ("bg_image", "coord_out", "frames", "debug_scale",) FUNCTION = "splinedata" CATEGORY = "WanVideoWrapper_QQ" DESCRIPTION = """WIP""" @@ -1038,7 +1038,28 @@ class PowerSplineEditor: # Create blank image tensor in BHWC format (Batch, Height, Width, Channels) result_image = torch.zeros((1, int(bg_h), int(bg_w), 3), dtype=torch.float32) - result = (result_image, coord_out, frames) + # Create debug_scale output to show keyframe scale values + debug_scale_list = [] + if all_coord_paths: + # Extract scale values from coordinates for debugging + for coord_path in all_coord_paths: + if isinstance(coord_path, list) and len(coord_path) > 0: + frame_scales = [] + for point in coord_path: + if isinstance(point, dict): + frame_num = point.get('frame', '?') + scale_val = point.get('scale', point.get('boxScale', point.get('pointScale', 1.0))) + frame_scales.append(f"F{frame_num}:{scale_val:.4f}") + if frame_scales: + # Show all frames, but limit display to first 20 frames for readability + display_scales = frame_scales[:20] if len(frame_scales) > 20 else frame_scales + if len(frame_scales) > 20: + display_scales.append(f"... (+{len(frame_scales) - 20} more)") + debug_scale_list.append(", ".join(display_scales)) + + debug_scale = " | ".join(debug_scale_list) if debug_scale_list else "No scale data found" + + result = (result_image, coord_out, frames, debug_scale) # Add video metadata to ui_out if video was created if video_metadata: diff --git a/utility/rgthree_api/utils_info.py b/utility/rgthree_api/utils_info.py index f604984..9196a40 100644 --- a/utility/rgthree_api/utils_info.py +++ b/utility/rgthree_api/utils_info.py @@ -59,21 +59,14 @@ def get_info_file(file_path: str, model_type: str, force=False): # Create a mirrored folder structure in loras/_power_preview power_preview_dir = os.path.join(base_dir, '_power_preview') + relative_path = os.path.relpath(file_path, base_dir) # Get the filename without extension and change to .json - filename_without_ext = os.path.splitext(os.path.basename(file_path))[0] + filename_without_ext = os.path.splitext(os.path.basename(relative_path))[0] + relative_dir = os.path.dirname(relative_path) - # Handle cross-drive paths (Windows) - os.path.relpath fails across different drives - try: - relative_path = os.path.relpath(file_path, base_dir) - relative_dir = os.path.dirname(relative_path) - if relative_dir: - info_path = os.path.join(power_preview_dir, relative_dir, f'{filename_without_ext}.json') - else: - info_path = os.path.join(power_preview_dir, f'{filename_without_ext}.json') - except ValueError: - # Cross-drive path on Windows - use the file's own directory for _power_preview - file_dir = os.path.dirname(file_path) - power_preview_dir = os.path.join(file_dir, '_power_preview') + if relative_dir: + info_path = os.path.join(power_preview_dir, relative_dir, f'{filename_without_ext}.json') + else: info_path = os.path.join(power_preview_dir, f'{filename_without_ext}.json') # Create the directory if it doesn't exist diff --git a/web/power_spline_editor/canvas/canvas_box_timeline.js b/web/power_spline_editor/canvas/canvas_box_timeline.js index ec841bb..6b1fe25 100644 --- a/web/power_spline_editor/canvas/canvas_box_timeline.js +++ b/web/power_spline_editor/canvas/canvas_box_timeline.js @@ -288,10 +288,6 @@ export function attachBoxTimelineHelpers(editor) { const currentBoxPositionNormalized = editor._computeBoxLayerPosition(activeWidget, frame); // This is the 0-1 normalized position const currentBoxPositionDenorm = editor.denormalizePoints([{ x: currentBoxPositionNormalized.x, y: currentBoxPositionNormalized.y }])[0]; const currentBoxPositionCanvas = transformVideoToCanvasSpace(editor, currentBoxPositionDenorm.x, currentBoxPositionDenorm.y); - - console.log(`[DEBUG] New Key Point Creation (S+Click):`); - console.log(` Clicked Canvas Location: x=${clickCanvasCoords.x.toFixed(2)}, y=${clickCanvasCoords.y.toFixed(2)}`); - console.log(` Created Box Center (Canvas): x=${currentBoxPositionCanvas.x.toFixed(2)}, y=${currentBoxPositionCanvas.y.toFixed(2)}`); } return true; @@ -307,6 +303,7 @@ export function attachBoxTimelineHelpers(editor) { if (!editor._isBoxLayerWidget(widget)) return false; const keys = editor._ensureBoxLayerData(widget) || []; const stored = editor._getBoxLayerStoredPoint(widget); + const normalized = (() => { // Stored point (from _getBoxLayerStoredPoint) is already normalized (0-1 range). // Always use this. No re-normalization needed here. @@ -319,6 +316,7 @@ export function attachBoxTimelineHelpers(editor) { rotation: (typeof stored?.rotation === 'number' && !Number.isNaN(stored.rotation)) ? stored.rotation : 0, }; })(); + const targetFrame = Math.max(1, Math.min(editor._getMaxFrames(), Math.round(frame || widget.value.box_timeline_point || 1))); const payload = { frame: targetFrame, diff --git a/web/power_spline_editor/layer_type_box.js b/web/power_spline_editor/layer_type_box.js index f687a00..ab3e1f7 100644 --- a/web/power_spline_editor/layer_type_box.js +++ b/web/power_spline_editor/layer_type_box.js @@ -554,11 +554,13 @@ export class BoxLayerWidget extends RgthreeBaseWidget { onScaleDec() { this.value.scale = Math.max(0.01, parseFloat((this.value.scale || 1.00) - 0.01).toFixed(2)); + this._updatePointsStoreWithCurrentScale(); this.parent.setDirtyCanvas(true, true); return true; } onScaleInc() { this.value.scale = Math.min(8.00, parseFloat((this.value.scale || 1.00) + 0.01).toFixed(2)); + this._updatePointsStoreWithCurrentScale(); this.parent.setDirtyCanvas(true, true); return true; } @@ -566,6 +568,7 @@ export class BoxLayerWidget extends RgthreeBaseWidget { const canvas = app.canvas; canvas.prompt('Scale', this.value.scale || 1.00, (v) => { this.value.scale = Math.max(0.01, Math.min(8.00, parseFloat(Number(v).toFixed(2)))); + this._updatePointsStoreWithCurrentScale(); this.parent.setDirtyCanvas(true, true); }); return true; @@ -574,10 +577,36 @@ export class BoxLayerWidget extends RgthreeBaseWidget { if (event.deltaX) { this.haveMouseMovedValue = true; this.value.scale = Math.max(0.01, Math.min(8.00, parseFloat(((this.value.scale || 1.00) + (event.deltaX / 1000)).toFixed(2)))); + this._updatePointsStoreWithCurrentScale(); node.setDirtyCanvas(true, true); } } + _updatePointsStoreWithCurrentScale() { + // Update points_store with the current scale value so keyframes use the latest scale + try { + let points = []; + try { + points = JSON.parse(this.value.points_store || '[]'); + } catch { + points = []; + } + + if (points && points.length > 0) { + // Update all scale fields in points_store with current scale value + const currentScale = parseFloat(this.value.scale || 1.00); + points.forEach(point => { + point.scale = currentScale; + point.boxScale = currentScale; + point.pointScale = currentScale; + }); + this.value.points_store = JSON.stringify(points); + } + } catch (e) { + console.warn('Failed to update points_store with current scale:', e); + } + } + onKeyClick(event, pos, node) { this._triggerButtonPress('key', node); this._activateLayer(node); diff --git a/web/power_spline_editor/power_spline_editor.js b/web/power_spline_editor/power_spline_editor.js index 2aea0d0..9d42edc 100644 --- a/web/power_spline_editor/power_spline_editor.js +++ b/web/power_spline_editor/power_spline_editor.js @@ -791,8 +791,6 @@ app.registerExtension({ let coord_in = message['coord_in']; let ref_image_dims = message['ref_image_dims']; - console.log('[PowerSplineEditor onExecuted] Received message:', message); - // ============================================================ // == HANDLE VIDEO // ============================================================ @@ -881,6 +879,37 @@ app.registerExtension({ this.editor.drawPreviousSpline(coord_in_str); } + // ✅ Update box widgets with ref_image if available + if (ref_image && this.editor?.refImageManager) { + const dataUrl = ref_image.startsWith('data:') ? ref_image : `data:image/png;base64,${ref_image}`; + const dims = await new Promise(resolve => { + const img = new Image(); + img.onload = () => resolve({ width: img.width, height: img.height }); + img.onerror = () => resolve({ width: 1, height: 1 }); + img.src = dataUrl; + }); + + const attachment = { + base64: ref_image.startsWith('data:') ? ref_image.split(',')[1] : ref_image, + type: 'image/png', + width: dims.width, + height: dims.height + }; + + // Update all box widgets with the new ref_attachment + const boxWidgets = this.layerManager?.getSplineWidgets?.().filter(w => w?.value?.type === 'box_layer') || []; + boxWidgets.forEach(widget => { + widget.value.ref_attachment = { entries: [attachment] }; + // Set selection to ref_1 if it's currently no_ref + if (!widget.value.ref_selection || widget.value.ref_selection === 'no_ref') { + widget.value.ref_selection = 'ref_1'; + } + }); + + // Clear cache and render + this.editor.layerRenderer?.clearRefImageCache?.(); + } + // ✅ WAIT for video scale to be set before loading coordinates if (message.bg_video?.length > 0 && this.editor?.videoMetadata) { console.log('[PowerSplineEditor] Video detected, waiting for scale to be set...'); @@ -913,6 +942,11 @@ app.registerExtension({ } else if (this.editor) { this.editor?.vis?.render(); } + + // ✅ Force render layer renderer to show ref images + if (this.editor?.layerRenderer) { + this.editor.layerRenderer.render(); + } }; // Call the async function