/** * Reference Image Manager Module * Handles reference image loading, attachment, and box layer ref management */ import { getReferenceImageFromConnectedNode, getReferenceImagesFromConnectedNode, findConnectedSourceNode, extractImagesFromSourceNode } from '../graph_query.js'; import { saveRefImageToCache, safeSetSessionItem } from './image-cache.js'; import { app } from '../../../../scripts/app.js'; /** * Create ref image manager instance for a node * @param {Object} node - The PowerSplineEditor node instance * @returns {Object} - Manager with methods for handling reference images */ export function createRefImageManager(node) { return { /** * Update reference image from connected node (bg_image input) * Handles both static images and video frames */ async updateReferenceImageFromConnectedNode(forceUpdate = false) { console.log( 'Attempting to update reference image from connected node...', forceUpdate ? '(forced)' : '' ); // First, check what's connected to bg_image input const sourceNode = findConnectedSourceNode(node, 'bg_image'); let isVideoSource = false; if (sourceNode?.node) { isVideoSource = this._isNodeVideoOrVideoProcessing(sourceNode.node); } // Check if we have video loaded - if so, skip (unless forced) if (!forceUpdate && node.editor?.videoMetadata) { return; } // Check if videoData exists (video might be pending) - skip unless forced if (!forceUpdate && node.videoData) { return; } // Determine if we need to clear the existing video background const shouldClearExistingVideo = node.editor?.videoMetadata && (!isVideoSource || !sourceNode); if (shouldClearExistingVideo) { if (node.editor) { const { clearBackgroundVideo } = await import( '../canvas/canvas_video_background.js' ); if (clearBackgroundVideo) { clearBackgroundVideo(node.editor); } } node.videoData = null; } try { // Check if we're connected to PrepareRefs node const isConnectedToPrepareRefs = this.checkIfConnectedToPrepareRefs(); console.log('[DEBUG] Connected to PrepareRefs:', isConnectedToPrepareRefs); if (isConnectedToPrepareRefs) { return await this._handlePrepareRefsConnection(); } // --- Handle video from direct or indirect video loaders --- let videoFrames = null; let videoFilename = null; let isSourceNodeDirectVideoLoader = false; if (sourceNode) { isSourceNodeDirectVideoLoader = sourceNode.node.type === 'LoadVideo' || sourceNode.node.type === 'VHS_LoadVideo'; if (isSourceNodeDirectVideoLoader) { console.log('[Canvas] Detected direct LoadVideo node'); const videoWidget = sourceNode.node.widgets?.find( w => w.name === 'video' ); if (videoWidget?.value) { videoFilename = videoWidget.value; console.log('[Canvas] Found video filename:', videoFilename); } } else if (isVideoSource) { const images = await extractImagesFromSourceNode(sourceNode, false); if (images?.length > 1) { videoFrames = images; console.log(`[Canvas] Got ${images.length} frames from bg_image`); } } } // Process video filename if found if (videoFilename) { return await this._processVideoFile(videoFilename); } // Process video frames if found if (videoFrames?.length > 1) { return await this._processVideoFrames(videoFrames); } // --- Fallback for static image --- const base64Image = await getReferenceImageFromConnectedNode( node, 'bg_image' ); if (!base64Image) { console.log('Could not retrieve reference image from connected node'); return; } // Store the fetched image node.originalRefImageData = { name: 'ref_image_from_connection.jpg', base64: base64Image.split(',')[1], type: 'image/jpeg' }; // Cache the ref image await saveRefImageToCache(node.originalRefImageData.base64, 'bg_image.png'); // Clear session storage cache if (node.uuid) { sessionStorage.removeItem(`spline-editor-img-${node.uuid}`); } // Update background with current opacity if (node.bgImageManager) { await node.bgImageManager.updateBackgroundImage(); } // Wait for processing await new Promise(resolve => setTimeout(resolve, 200)); } catch (error) { console.error('Error updating reference image from connected node:', error); alert('Error updating reference image: ' + error.message); } }, /** * Handle PrepareRefs connection */ async _handlePrepareRefsConnection() { try { const timestamp = Date.now(); const refImageUrl = new URL( `../ref/bg_image_cl.png?t=${timestamp}`, import.meta.url ).href; const response = await fetch(refImageUrl); if (!response.ok) { console.error('Failed to load bg_image_cl.png from ref folder'); alert('Could not load bg_image_cl.png from ref folder.'); return; } const blob = await response.blob(); const base64Data = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result.split(',')[1]); reader.onerror = reject; reader.readAsDataURL(blob); }); node.originalRefImageData = { name: 'bg_image_cl.png', base64: base64Data, type: 'image/png' }; // Clear session storage cache if (node.uuid) { sessionStorage.removeItem(`spline-editor-img-${node.uuid}`); } // Update background with current opacity if (node.bgImageManager) { await node.bgImageManager.updateBackgroundImage(); } await new Promise(resolve => setTimeout(resolve, 200)); } catch (error) { console.error('Error loading bg_image_cl.png:', error); alert('Error loading bg_image_cl.png: ' + error.message); } }, /** * Process video file from LoadVideo node */ async _processVideoFile(videoFilename) { console.log('[Canvas] Processing LoadVideo file:', videoFilename); const payload = { video_filename: videoFilename, mask_width: node.widgets?.find(w => w.name === 'mask_width')?.value || 640, mask_height: node.widgets?.find(w => w.name === 'mask_height')?.value || 480 }; try { console.log('[DEBUG] Sending request to /wanvideowrapper_qq/process_video_file'); const response = await fetch('/wanvideowrapper_qq/process_video_file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); console.log('[DEBUG] Response status:', response.status); const result = await response.json(); console.log('[DEBUG] Response result:', result); if (result.success && result.paths?.bg_video) { console.log('[Canvas] Video processed successfully:', result.paths.bg_video); if (node.videoManager) { await node.videoManager.loadVideo(result.paths.bg_video); } return; } else { throw new Error(result.error || 'Unknown error'); } } catch (error) { console.error('[Canvas] Error processing video file:', error); alert('Failed to process video file: ' + error.message); } }, /** * Process video frames */ async _processVideoFrames(videoFrames) { console.log('[Canvas] Processing video frames through backend...'); const payload = { bg_image: videoFrames, ref_layer_data: [], mask_width: node.widgets?.find(w => w.name === 'mask_width')?.value || 640, mask_height: node.widgets?.find(w => w.name === 'mask_height')?.value || 480 }; try { const response = await fetch('/wanvideowrapper_qq/trigger_prepare_refs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); if (result.success && result.paths?.bg_video) { console.log('[Canvas] Video created successfully:', result.paths.bg_video); if (node.videoManager) { await node.videoManager.loadVideo(result.paths.bg_video); } return; } else { throw new Error(result.error || 'Unknown error'); } } catch (error) { console.error('[Canvas] Error processing video:', error); } }, /** * Attach reference images to active box layer */ async attachRefImageToActiveBoxLayer(desiredSelection = 'ref_1') { const activeWidget = node.layerManager?.getActiveWidget?.(); if (!activeWidget || activeWidget.value?.type !== 'box_layer') { alert('Activate a box layer first to attach a ref image.'); return; } const isConnectedToPrepareRefs = this.checkIfConnectedToPrepareRefs(); if (isConnectedToPrepareRefs) { await this.loadRefImagesFromRefFolder(activeWidget, desiredSelection); return; } // Fetch images from ref_images input const images = await getReferenceImagesFromConnectedNode(node); if (!images || images.length === 0) { alert('No ref_images input found. Connect an IMAGE or IMAGE batch to ref_images.'); return; } const attachments = await this._processImageAttachments(images); if (attachments.length === 0) { console.warn('No attachments created'); return; } // Set attachments on widget activeWidget.value.ref_attachment = { entries: attachments }; // Set selection const availableOptions = activeWidget._getRefOptions ? activeWidget._getRefOptions() : ['no_ref', 'ref_1', 'ref_2', 'ref_3', 'ref_4', 'ref_5']; const desiredIdx = desiredSelection && availableOptions.includes(desiredSelection) ? Math.max(0, (parseInt(desiredSelection.split('_')[1], 10) || 1) - 1) : 0; if (attachments.length === 0 || desiredIdx >= attachments.length) { activeWidget.value.ref_selection = 'no_ref'; } else { const clampedIndex = Math.min(attachments.length - 1, desiredIdx); activeWidget.value.ref_selection = `ref_${clampedIndex + 1}`; } // Persist to session this._persistBoxRefToSession(activeWidget); // Clear global ref cache try { sessionStorage.removeItem('spline-editor-cached-ref-image'); } catch {} // Save all frames to disk for (let i = 0; i < images.length; i++) { const imgData = images[i]; const b64 = imgData.startsWith('data:') ? imgData.split(',')[1] : imgData; await saveRefImageToCache(b64, `ref_image_${i}.png`, { skipSessionCache: true }); } // Refresh canvas node.setDirtyCanvas(true, true); node.editor?.refreshBackgroundImage?.(); }, /** * Process image attachments */ async _processImageAttachments(images) { const attachments = []; const maxRefs = 5; for (let i = 0; i < Math.min(images.length, maxRefs); i++) { const imgData = images[i]; const base64Data = imgData.startsWith('data:') ? imgData.split(',')[1] : imgData; const dataUrl = imgData.startsWith('data:') ? imgData : `data:image/png;base64,${base64Data}`; // Get dimensions 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; }); // Save to cache const filename = `ref_image_${i}.png`; await saveRefImageToCache(base64Data, filename); attachments.push({ path: `power_spline_editor/bg/${filename}`, type: 'image/png', width: dims.width, height: dims.height, name: filename }); } return attachments; }, /** * Clear reference image from active box layer */ clearRefImageFromActiveBoxLayer() { const activeWidget = node.layerManager?.getActiveWidget?.(); if (activeWidget && activeWidget.value?.type === 'box_layer') { activeWidget.value.ref_attachment = null; activeWidget.value.ref_selection = 'no_ref'; try { const keyId = node.id ?? node.uuid; const key = keyId ? `spline-editor-boxref-${keyId}-${ activeWidget.value.name || activeWidget.name || 'box' }` : null; if (key) sessionStorage.removeItem(key); } catch {} node.setDirtyCanvas(true, true); } }, /** * Update reference images for all box layers */ async updateAllBoxLayerRefs() { const widgets = node.layerManager?.getSplineWidgets?.() || []; const boxWidgets = widgets.filter(w => w?.value?.type === 'box_layer'); if (!boxWidgets.length) return; // Clear ref image cache if (node.layerRenderer?.clearRefImageCache) { node.layerRenderer.clearRefImageCache(); } const isConnectedToPrepareRefs = this.checkIfConnectedToPrepareRefs(); if (isConnectedToPrepareRefs) { for (const boxWidget of boxWidgets) { await this.loadRefImagesFromRefFolder( boxWidget, boxWidget.value.ref_selection || 'ref_1' ); } return; } // Fetch images from ref_images input const images = await getReferenceImagesFromConnectedNode(node); if (!images || images.length === 0) { console.warn('No ref_images input found for updating box layer refs.'); return; } const attachments = await this._processImageAttachments(images); // Update all box widgets boxWidgets.forEach(boxWidget => { const currentSelection = boxWidget.value.ref_selection || 'no_ref'; boxWidget.value.ref_attachment = { entries: attachments }; // Keep current selection if valid if (currentSelection !== 'no_ref') { const parts = currentSelection.split('_'); const idx = parts.length > 1 ? parseInt(parts[1], 10) : 1; const arrayIndex = Number.isFinite(idx) ? Math.max(0, idx - 1) : 0; if (arrayIndex >= attachments.length) { boxWidget.value.ref_selection = 'no_ref'; } } // Persist to session this._persistBoxRefToSession(boxWidget); }); // Clear global ref cache try { sessionStorage.removeItem('spline-editor-cached-ref-image'); } catch {} // Save all frames to disk for (let i = 0; i < images.length; i++) { const imgData = images[i]; const b64 = imgData.startsWith('data:') ? imgData.split(',')[1] : imgData; await saveRefImageToCache(b64, `ref_image_${i}.png`, { skipSessionCache: true }); } // Clear cache again and refresh node.editor?.layerRenderer?.clearRefImageCache?.(); node.setDirtyCanvas(true, true); }, /** * Persist box ref attachment to session storage */ _persistBoxRefToSession(boxWidget) { try { const keyId = node.id ?? node.uuid; const key = keyId ? `spline-editor-boxref-${keyId}-${ boxWidget.value.name || boxWidget.name || 'box' }` : null; if (key) { safeSetSessionItem( key, JSON.stringify({ attachment: boxWidget.value.ref_attachment, selection: boxWidget.value.ref_selection }) ); } } catch (e) { console.warn('Failed to persist box ref attachment to session:', e); } }, /** * Load reference images from ref folder */ async loadRefImagesFromRefFolder(boxWidget, desiredSelection = 'ref_1') { try { const attachments = []; const maxRefs = 5; const timestamp = Date.now(); for (let i = 1; i <= maxRefs; i++) { try { const refImageUrl = new URL( `../ref/ref_${i}.png?t=${timestamp}`, import.meta.url ).href; const response = await fetch(refImageUrl); if (!response.ok) { continue; } const blob = await response.blob(); const base64Data = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result.split(',')[1]); reader.onerror = reject; reader.readAsDataURL(blob); }); // Get dimensions 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 = `data:image/png;base64,${base64Data}`; }); // Save to cache const filename = `ref_${i}.png`; await saveRefImageToCache(base64Data, filename); attachments.push({ path: `ref/${filename}`, type: 'image/png', width: dims.width, height: dims.height, name: filename }); } catch (error) { console.error(`Error loading ref_${i}.png:`, error); } } if (attachments.length === 0) { console.error('[loadRefImagesFromRefFolder] No ref images found in ref folder'); return; } // Set attachments boxWidget.value.ref_attachment = { entries: attachments }; // Set selection const availableOptions = boxWidget._getRefOptions ? boxWidget._getRefOptions() : ['no_ref', 'ref_1', 'ref_2', 'ref_3', 'ref_4', 'ref_5']; const desiredIdx = desiredSelection && availableOptions.includes(desiredSelection) ? Math.max(0, (parseInt(desiredSelection.split('_')[1], 10) || 1) - 1) : 0; if (desiredIdx >= attachments.length) { boxWidget.value.ref_selection = 'no_ref'; } else { const clampedIndex = Math.min(attachments.length - 1, desiredIdx); boxWidget.value.ref_selection = `ref_${clampedIndex + 1}`; } // Persist to session this._persistBoxRefToSession(boxWidget); // Clear cache and refresh if (node.layerRenderer?.clearRefImageCache) { node.layerRenderer.clearRefImageCache(); } } catch (error) { console.error('Error loading ref images from ref folder:', error); } }, /** * Check if connected to PrepareRefs node */ checkIfConnectedToPrepareRefs() { const graph = app.graph; if (!graph || !graph.links || !node.inputs) { return false; } for (let i = 0; i < node.inputs.length; i++) { const input = node.inputs[i]; if (!input || !input.name) continue; let link = null; if (graph.links instanceof Map) { for (const [linkId, linkObj] of graph.links) { if ( linkObj && linkObj.target_id === node.id && linkObj.target_slot === i ) { link = linkObj; break; } } } else if (Array.isArray(graph.links)) { link = graph.links.find( linkObj => linkObj && linkObj.target_id === node.id && linkObj.target_slot === i ); } if (link) { const sourceNode = graph._nodes?.find(n => n.id === link.origin_id); if (sourceNode && sourceNode.type === 'PrepareRefs') { return true; } } } return false; }, /** * Check if node is a video source or video processing node */ _isNodeVideoOrVideoProcessing(nodeToCheck) { if (!nodeToCheck) return false; // Direct video loaders if ( nodeToCheck.type === 'LoadVideo' || nodeToCheck.type === 'VHS_LoadVideo' ) { return true; } // Video processing nodes that output multiple frames const videoProcessingTypes = [ 'ImageResizeKJv2', 'ImageScale', 'ImageUpscaleWithModel', 'VideoCapture' ]; if (videoProcessingTypes.includes(nodeToCheck.type)) { return true; } return false; } }; }