/** * Dimension Manager Module * Handles canvas dimensions, frame scaling, and keyframe management */ import { safeSetSessionItem } from './image-cache.js'; import { recenterBackgroundVideo } from '../canvas/canvas_video_background.js'; /** * Create dimension manager instance * @param {Object} node - The PowerSplineEditor node instance * @returns {Object} - Manager with methods for handling dimensions */ export function createDimensionManager(node) { return { /** * Handle frames refresh and keyframe scaling */ async handleFramesRefresh() { try { // Step 1: Find all box type layers const boxLayers = this._getBoxLayers(); if (boxLayers.length === 0) { return; } // Step 2: Check which box layers have keyframes const boxLayersWithKeys = boxLayers.filter(layer => { return ( layer.value.box_keys && Array.isArray(layer.value.box_keys) && layer.value.box_keys.length > 0 ); }); // Step 3: Determine target frames const targetFrames = this._getTargetFrames(); if (!targetFrames) { return; } // Step 4: Get current max frames from timeline try { const module = await import('../canvas/canvas_constants.js'); const currentMaxFrames = node.editor?._getMaxFrames?.() ?? module.BOX_TIMELINE_MAX_POINTS ?? 50; // Step 5: Decide scaling behavior await this._scaleFrames( boxLayersWithKeys, targetFrames, currentMaxFrames ); // Step 6: Refresh editor if (node.editor?.redraw) { node.editor.redraw(); } node.setDirtyCanvas(true, true); } catch (error) { console.error('[DimensionManager] Error importing canvas_constants:', error); } } catch (error) { console.error('[DimensionManager] Error handling frames refresh:', error); } }, /** * Get all box layers from node widgets */ _getBoxLayers() { const boxLayers = []; if (node.widgets) { for (const widget of node.widgets) { if (widget.value && widget.value.type === 'box_layer') { boxLayers.push(widget); } } } return boxLayers; }, /** * Get target frames from input or property */ _getTargetFrames() { const framesInputValue = this.getInputOrProperty('frames'); const hasFramesInput = typeof framesInputValue === 'number' && !Number.isNaN(framesInputValue) && framesInputValue > 0; if (!hasFramesInput) { return null; } return Math.max(1, Math.round(framesInputValue)); }, /** * Scale frames based on target frames */ async _scaleFrames(boxLayersWithKeys, targetFrames, currentMaxFrames) { const persistMaxFrames = (frames) => { if (node.editor) { node.editor._maxFrames = frames; } node.properties = node.properties || {}; node.properties.box_max_frames = frames; if (node.uuid) { safeSetSessionItem(`spline-editor-maxframes-${node.uuid}`, String(frames)); } }; // Determine last keyframe const lastKeyFrame = boxLayersWithKeys.length > 0 ? Math.max( ...boxLayersWithKeys.map(layer => Math.max( ...layer.value.box_keys.map(k => Math.max(1, Number(k.frame) || 1) ) ) ) ) : 0; if (targetFrames > currentMaxFrames) { // Expand canvas persistMaxFrames(targetFrames); } else { // Contract or maintain canvas if (lastKeyFrame <= targetFrames) { // Can safely set to target frames persistMaxFrames(targetFrames); } else { // Need to scale keyframes const scaleRatio = targetFrames / currentMaxFrames; if (boxLayersWithKeys.length > 0) { for (const layer of boxLayersWithKeys) { const originalKeys = layer.value.box_keys; const scaledKeys = originalKeys.map(key => { const newFrame = Math.max( 1, Math.min(targetFrames, Math.round(key.frame * scaleRatio)) ); return { ...key, frame: newFrame }; }); // Remove duplicate frames (keep last one for each frame) const keysByFrame = new Map(); for (const key of scaledKeys) { keysByFrame.set(key.frame, key); } layer.value.box_keys = Array.from(keysByFrame.values()).sort( (a, b) => a.frame - b.frame ); } } persistMaxFrames(targetFrames); } } }, /** * Get input value or property value */ getInputOrProperty(name) { const coerceNumber = val => { if (val === null || val === undefined) return val; const num = Number(val); return Number.isNaN(num) ? val : num; }; // Try to get from connected input if (node.inputs) { const inputIndex = node.inputs.findIndex(i => i.name === name); if (inputIndex >= 0 && node.inputs[inputIndex].link != null) { const link = app.graph.links.get(node.inputs[inputIndex].link); // Prefer explicit link data if (link?.data !== undefined) { return coerceNumber(link.data); } if (link) { // Get source node const sourceNode = app.graph._nodes.find( n => n.id === link.origin_id ); if (sourceNode?.outputs?.[link.origin_slot]) { const outputName = sourceNode.outputs[link.origin_slot].name; const widget = sourceNode.widgets?.find( w => w.name === outputName || w.name === name ); if (widget) { return coerceNumber(widget.value); } // Fallback to output value const outputVal = sourceNode.outputs[link.origin_slot].value; if (outputVal !== undefined) { return coerceNumber(outputVal); } // Fallback to first numeric widget const firstNumericWidget = sourceNode.widgets?.find(w => { const num = coerceNumber(w.value); return typeof num === 'number' && !Number.isNaN(num); }); if (firstNumericWidget) { return coerceNumber(firstNumericWidget.value); } // Fallback to first numeric property if (sourceNode.properties) { for (const val of Object.values(sourceNode.properties)) { const num = coerceNumber(val); if (typeof num === 'number' && !Number.isNaN(num)) { return num; } } } } } // Fallback to direct input value const directInputVal = node.inputs[inputIndex].value; if (directInputVal !== undefined) { return coerceNumber(directInputVal); } } } // Fallback to property if (node.properties?.[name] !== undefined) { return coerceNumber(node.properties[name]); } // Fallback to widget const widget = node.widgets?.find(w => w.name === name); if (widget) { return coerceNumber(widget.value); } return null; }, /** * Update canvas dimensions */ updateCanvasDimensions(width, height) { if (!node.editor) { console.warn('[DimensionManager] Editor not initialized'); return; } // Update editor dimensions node.editor.width = width; node.editor.height = height; // Update visualization if (node.editor.vis) { node.editor.vis.width(width); node.editor.vis.height(height); node.editor.vis.render(); } // Update video position if video is loaded if (node.editor.videoElement && node.editor.videoMetadata) { recenterBackgroundVideo(node.editor); } // Refresh layers if (node.editor.layerRenderer) { node.editor.layerRenderer.render(); } }, /** * Scale spline points based on dimension change */ scaleSplinePoints(oldWidth, oldHeight, newWidth, newHeight) { const scaleX = newWidth / oldWidth; const scaleY = newHeight / oldHeight; const splineWidgets = node.widgets?.filter(w => { const PowerSplineWidget = window.PowerSplineWidget; const HandDrawLayerWidget = window.HandDrawLayerWidget; const BoxLayerWidget = window.BoxLayerWidget; return ( w instanceof PowerSplineWidget || w instanceof HandDrawLayerWidget || w instanceof BoxLayerWidget ); }); if (!splineWidgets?.length) { return; } splineWidgets.forEach(widget => { if (widget.value?.points_store) { try { let points = JSON.parse(widget.value.points_store); const transformedPoints = points.map(point => ({ ...point, x: point.x * scaleX, y: point.y * scaleY })); widget.value.points_store = JSON.stringify(transformedPoints); } catch (e) { console.error( '[DimensionManager] Error updating spline points:', e ); } } }); }, /** * Get current canvas dimensions */ getCanvasDimensions() { const widthWidget = node.widgets?.find(w => w.name === 'mask_width'); const heightWidget = node.widgets?.find(w => w.name === 'mask_height'); return { width: widthWidget?.value || 640, height: heightWidget?.value || 480 }; }, /** * Restore dimensions from session storage */ async restoreDimensionsFromSession() { try { if (!node.uuid) return null; const savedDims = sessionStorage.getItem( `spline-editor-dims-${node.uuid}` ); if (savedDims) { const dims = JSON.parse(savedDims); return dims; } } catch (error) { console.error('[DimensionManager] Error restoring dimensions:', error); } return null; }, /** * Save dimensions to session storage */ saveDimensionsToSession(width, height) { try { if (!node.uuid) return; safeSetSessionItem( `spline-editor-dims-${node.uuid}`, JSON.stringify({ width, height }) ); } catch (error) { console.error('[DimensionManager] Error saving dimensions:', error); } }, /** * Check if user has manually adjusted dimensions */ hasUserAdjustedDimensions() { return node.properties?.userAdjustedDims || false; }, /** * Mark dimensions as user-adjusted */ markDimensionsAsUserAdjusted(width, height) { node.properties = node.properties || {}; node.properties.userAdjustedDims = true; node.properties.bgImageDims = { width, height }; try { const userDims = { width: Number(width), height: Number(height) }; if (node.uuid) { safeSetSessionItem( `spline-editor-user-dims-${node.uuid}`, JSON.stringify(userDims) ); } } catch {} } }; }