fix matching time
This commit is contained in:
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user