This commit is contained in:
JJ
2026-03-15 01:30:29 +00:00
parent f4abfbe2e4
commit f0d38fd9c6
7 changed files with 116 additions and 42 deletions
+1
View File
@@ -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
+18 -20
View File
@@ -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))
+24 -3
View File
@@ -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:
+6 -13
View File
@@ -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
@@ -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,
+29
View File
@@ -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);
+36 -2
View File
@@ -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