fix matching time

This commit is contained in:
JJ
2026-03-16 14:48:50 +00:00
parent 16725ee39d
commit 95c2a9d5b9
10 changed files with 216 additions and 20 deletions
+1 -1
View File
@@ -7,5 +7,5 @@ This is the single source of truth for all constant values.
BOX_BASE_RADIUS = 200
BOX_BASE_SIZE = BOX_BASE_RADIUS * 2 # Side length of box (400)
POINT_BASE_RADIUS = 8
BOX_TIMELINE_MAX_POINTS = 50
BOX_TIMELINE_MAX_POINTS = 250
BOX_BORDER_BAND = 20
+91 -9
View File
@@ -14,7 +14,6 @@ from ..config.constants import BOX_BASE_RADIUS
# Supersampling factor for smooth scaling
SUPERSAMPLE = 4
class DrawImageOnPath:
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image", "mask",)
@@ -249,6 +248,80 @@ Draws an input image along a coordinate path for each frame, returning the rende
padded.append(dict(last))
return padded
def _get_interpolated_point(self, coords, target_frame):
"""
Get interpolated coordinates for a target frame using the 'frame' property.
This matches the Power Spline Editor's box timeline interpolation logic.
"""
if not coords:
return None
# Filter out coords without a frame property
valid_coords = [c for c in coords if isinstance(c.get("frame"), (int, float))]
if not valid_coords:
# Fall back to first coordinate
return coords[0] if coords else None
# Sort by frame number
sorted_coords = sorted(valid_coords, key=lambda c: float(c.get("frame", 0)))
first = sorted_coords[0]
last = sorted_coords[-1]
first_frame = float(first.get("frame", 1))
last_frame = float(last.get("frame", 1))
# Clamp target frame to keyframe range
if target_frame <= first_frame:
return first
if target_frame >= last_frame:
return last
# Find the two keyframes that bracket our target frame
for i in range(len(sorted_coords) - 1):
curr = sorted_coords[i]
next_c = sorted_coords[i + 1]
curr_frame = float(curr.get("frame", 0))
next_frame = float(next_c.get("frame", 0))
if curr_frame <= target_frame <= next_frame:
# Exact match on current frame
if target_frame == curr_frame:
return curr
# Interpolate between curr and next_c
if next_frame == curr_frame:
return curr
t = (target_frame - curr_frame) / (next_frame - curr_frame)
# Interpolate all numeric properties
result = {}
# Copy non-numeric properties as-is from curr
for key, value in curr.items():
if key not in ("x", "y", "scale", "pointScale", "boxScale", "rotation", "boxR", "frame"):
result[key] = value
# Interpolate numeric properties
for prop in ("x", "y", "scale", "pointScale", "boxScale", "rotation", "boxR"):
curr_val = curr.get(prop)
next_val = next_c.get(prop)
if isinstance(curr_val, (int, float)) and isinstance(next_val, (int, float)):
result[prop] = curr_val + (next_val - curr_val) * t
elif isinstance(curr_val, (int, float)):
result[prop] = curr_val
elif isinstance(next_val, (int, float)):
result[prop] = next_val
# Copy frame from target
result["frame"] = target_frame
return result
# Shouldn't reach here, but return last as fallback
return last
def create(self, bg_image, ref_images, coordinates, ref_masks=None, use_box_rotation=True, use_box_scale_size=True,
fallback_scale=1.0, overlay_opacity=1.0, frames=0, add_shadows=False, mask_fill=0.0, use_gpu=True, gpu_batch=8):
try:
@@ -376,10 +449,14 @@ Draws an input image along a coordinate path for each frame, returning the rende
# Process each layer for this frame (reversed so top layers in list draw on top)
for reversed_idx, layer_coords in enumerate(reversed(coords)):
if frame_idx >= len(layer_coords):
if not layer_coords:
continue
point = layer_coords[frame_idx]
# Use interpolation to get the correct point for this frame
# Coordinates already have per-frame data, so use frame index directly
point = self._get_interpolated_point(layer_coords, frame_idx + 1)
if point is None:
continue
# Get original layer index (before reversal) for ref_selections
layer_idx = len(coords) - 1 - reversed_idx
@@ -624,7 +701,7 @@ Draws an input image along a coordinate path for each frame, returning the rende
return output_tensor, mask_tensor
def _create_gpu(self, bg_image, ref_images, coordinates, ref_masks, use_box_rotation, use_box_scale_size,
fallback_scale, overlay_opacity, frames, add_shadows, mask_fill, gpu_batch=8):
fallback_scale, overlay_opacity, frames, add_shadows=False, mask_fill=0.0, gpu_batch=8):
"""GPU-accelerated rendering using torch operations with batched processing to avoid OOM."""
device = torch.device('cuda')
@@ -751,13 +828,18 @@ Draws an input image along a coordinate path for each frame, returning the rende
bg_batch = bg_cpu[batch_start:batch_end].to(device)
# Extract coordinate batch for each layer
# Use interpolation to get correct coordinates for each frame in the batch
coords_batch = []
for layer_coords in coords:
batch_coords = layer_coords[batch_start:batch_end]
# Extend if needed to match batch size
if len(batch_coords) < current_batch_size and layer_coords:
last_coord = dict(layer_coords[-1])
batch_coords = list(batch_coords) + [dict(last_coord)] * (current_batch_size - len(batch_coords))
batch_coords = []
for i in range(current_batch_size):
actual_frame = batch_start + i + 1 # Frames are 1-indexed
# Coordinates already have per-frame data, so use frame directly
point = self._get_interpolated_point(layer_coords, actual_frame)
if point is None:
# Create a default point if interpolation fails
point = {"x": 0.5, "y": 0.5, "scale": 1, "frame": actual_frame}
batch_coords.append(point)
coords_batch.append(batch_coords)
# Render this batch using the existing GPU method
+1 -1
View File
@@ -416,7 +416,7 @@ class PowerSplineEditor:
'scale': round(snap['scale'], 4),
'boxScale': round(snap['scale'], 4),
'pointScale': round(snap['scale'], 4),
'frame': idx + 1,
'frame': round(timeline_frame),
'boxR': round(snap.get('boxR', 0.0), 4),
})
return samples
@@ -4,9 +4,31 @@ import { applyBoxTimelineFrame } from './canvas_scrub.js';
import { transformMouseToVideoSpace, transformVideoToCanvasSpace } from '../spline_utils.js'; // Added transformVideoToCanvasSpace
export function attachBoxTimelineHelpers(editor) {
// Helper to get max frames dynamically (can be overridden by editor._maxFrames)
// Helper to get max frames dynamically
// Returns the highest of: explicitly set _maxFrames, highest keyframe across all box widgets, or default
editor._getMaxFrames = () => {
return editor._maxFrames || BOX_TIMELINE_MAX_POINTS;
if (editor._maxFrames) {
return editor._maxFrames;
}
// Dynamically calculate based on existing keyframes in all box layer widgets
const layerManager = editor.node?.layerManager;
if (layerManager) {
const widgets = layerManager.getSplineWidgets();
let maxKeyframeFrame = 0;
for (const widget of widgets) {
if (editor._isBoxLayerWidget(widget) && Array.isArray(widget.value.box_keys)) {
for (const key of widget.value.box_keys) {
if (key && typeof key.frame === 'number') {
maxKeyframeFrame = Math.max(maxKeyframeFrame, key.frame);
}
}
}
}
if (maxKeyframeFrame > 0) {
return Math.max(BOX_TIMELINE_MAX_POINTS, maxKeyframeFrame);
}
}
return BOX_TIMELINE_MAX_POINTS;
};
editor._isBoxLayerWidget = (widget) => widget?.value?.type === 'box_layer';
@@ -27,8 +49,9 @@ export function attachBoxTimelineHelpers(editor) {
const normY = Math.abs(rawY) >= 10 ? rawY / height : rawY;
const scaleVal = (typeof key.scale === 'number' && !Number.isNaN(key.scale)) ? key.scale : 1;
const rotationVal = (typeof key.rotation === 'number' && !Number.isNaN(key.rotation)) ? key.rotation : 0;
// Don't clamp keyframe frames - allow them to extend beyond default max
return {
frame: Math.max(1, Math.min(editor._getMaxFrames(), Math.round(key.frame || 1))),
frame: Math.max(1, Math.round(key.frame || 1)),
x: normX,
y: normY,
scale: editor.clampScaleValue ? editor.clampScaleValue(scaleVal) : Math.max(0.2, Math.min(6, scaleVal)),
@@ -38,7 +61,9 @@ export function attachBoxTimelineHelpers(editor) {
.filter(Boolean)
.sort((a, b) => (a.frame || 0) - (b.frame || 0));
const current = Number(widget.value.box_timeline_point) || 1;
widget.value.box_timeline_point = Math.max(1, Math.min(editor._getMaxFrames(), Math.round(current)));
// Clamp current timeline point but allow it to extend if keyframes go further
const maxFrames = editor._getMaxFrames();
widget.value.box_timeline_point = Math.max(1, Math.min(maxFrames, Math.round(current)));
return widget.value.box_keys;
};
@@ -1 +1 @@
export const BOX_TIMELINE_MAX_POINTS = 50;
export const BOX_TIMELINE_MAX_POINTS = 250;
@@ -224,7 +224,7 @@ export function attachPathHelpers(editor) {
if (isBoxLayer) {
const normalizedKeys = Array.isArray(widget.value.box_keys)
? widget.value.box_keys.map(k => ({
frame: Math.max(1, Math.min(editor._getMaxFrames(), Math.round(k.frame || 1))),
frame: Math.max(1, Math.round(k.frame || 1)),
x: typeof k.x === 'number' ? k.x : 0.5,
y: typeof k.y === 'number' ? k.y : 0.5,
scale: editor.clampScaleValue(k.scale ?? 1),
+13 -1
View File
@@ -424,6 +424,15 @@ export async function handleCanvasRefresh(node) {
if (!node.editor) {
const SplineEditor2 = (await import('./canvas/canvas_main.js')).default;
node.editor = new SplineEditor2(node);
// Restore max frames from session storage or properties
if (node.dimensionManager) {
const restoredFrames = node.dimensionManager.restoreMaxFrames();
// If no saved frames, initialize from frames input
if (!restoredFrames) {
node.dimensionManager.initializeMaxFramesFromInput();
}
}
}
// Refresh the background image to trigger canvas render
@@ -570,10 +579,13 @@ export class TopRowWidget extends RgthreeBaseWidget {
// Draw Refresh Frames button
if (this.visibility.refreshFramesButton) {
// Get current max frames for display
const currentMaxFrames = node.editor?._getMaxFrames?.() ?? node.properties?.box_max_frames ?? '?';
const framesLabel = `🕞 ${currentMaxFrames}`;
drawWidgetButton(
ctx,
{ size: [refreshFramesWidth, height], pos: [posX, posY] },
"🕞 Frames",
framesLabel,
this.framesButtonMouseDown
);
}
+2 -2
View File
@@ -3,7 +3,7 @@ import { initializeDrivenConfig, initializeEasingConfig, initializeScaleConfig,
import { showCustomEasingMenu, showCustomDrivenToggleMenu, showInterpolationMenu, showRefSelectionMenu } from './context_menu.js';
import { app } from '../../../scripts/app.js';
const BOX_TIMELINE_MAX_POINTS = 50;
const BOX_TIMELINE_MAX_POINTS = 250;
export class BoxLayerWidget extends RgthreeBaseWidget {
constructor(name) {
@@ -1064,7 +1064,7 @@ export class BoxLayerWidget extends RgthreeBaseWidget {
this.stopBoxPlayback(node);
}
const editor = node?.editor;
const fps = 16;
const fps = 25;
const intervalMs = Math.max(10, Math.round(1000 / fps));
const advanceFrame = () => {
const max = this._getMaxFrames(node);
@@ -406,6 +406,65 @@ export function createDimensionManager(node) {
);
}
} catch {}
},
/**
* Restore max frames from session storage or properties
*/
restoreMaxFrames() {
try {
// First check node properties
if (node.properties?.box_max_frames) {
const savedFrames = Number(node.properties.box_max_frames);
if (!Number.isNaN(savedFrames) && savedFrames > 0 && node.editor) {
node.editor._maxFrames = savedFrames;
console.log(`[DimensionManager] Restored max frames from properties: ${savedFrames}`);
return savedFrames;
}
}
// Then check session storage
if (node.uuid) {
const sessionValue = sessionStorage.getItem(`spline-editor-maxframes-${node.uuid}`);
if (sessionValue) {
const savedFrames = Number(sessionValue);
if (!Number.isNaN(savedFrames) && savedFrames > 0 && node.editor) {
node.editor._maxFrames = savedFrames;
// Also update properties for persistence
node.properties = node.properties || {};
node.properties.box_max_frames = savedFrames;
console.log(`[DimensionManager] Restored max frames from session: ${savedFrames}`);
return savedFrames;
}
}
}
} catch (error) {
console.warn('[DimensionManager] Error restoring max frames:', error);
}
return null;
},
/**
* Initialize max frames from the frames input/property
* This sets the initial max frames based on what's connected to the frames input
*/
initializeMaxFramesFromInput() {
try {
const targetFrames = this._getTargetFrames();
if (targetFrames && node.editor) {
node.editor._maxFrames = targetFrames;
node.properties = node.properties || {};
node.properties.box_max_frames = targetFrames;
if (node.uuid) {
safeSetSessionItem(`spline-editor-maxframes-${node.uuid}`, String(targetFrames));
}
console.log(`[DimensionManager] Initialized max frames from input: ${targetFrames}`);
return targetFrames;
}
} catch (error) {
console.warn('[DimensionManager] Error initializing max frames:', error);
}
return null;
}
};
}
@@ -751,6 +751,15 @@ app.registerExtension({
this.editor = new SplineEditor2(this);
}
// Restore max frames from session storage or properties
if (this.dimensionManager) {
const restoredFrames = this.dimensionManager.restoreMaxFrames();
// If no saved frames, initialize from frames input
if (!restoredFrames) {
this.dimensionManager.initializeMaxFramesFromInput();
}
}
// Restore video background
if (this.videoData && !this.imgData && this.editor) {
console.log('[PowerSplineEditor onConfigure] Restoring video background');
@@ -871,6 +880,15 @@ app.registerExtension({
this.editor = new SplineEditor2(this, false);
}
// Restore max frames from session storage or properties
if (this.dimensionManager) {
const restoredFrames = this.dimensionManager.restoreMaxFrames();
// If no saved frames, initialize from frames input
if (!restoredFrames) {
this.dimensionManager.initializeMaxFramesFromInput();
}
}
const finishEditorSetup = async () => {
if (coord_in) {
const coord_in_str = Array.isArray(coord_in)