864 lines
32 KiB
JavaScript
864 lines
32 KiB
JavaScript
import { app } from '../../../scripts/app.js';
|
||
import { makeUUID, NodeSizeManager } from './spline_utils.js';
|
||
import { TopRowWidget, handlePrepareRefsRefresh } from './canvas_top_row.js';
|
||
import { chainCallback, hideWidgetForGood } from './general_utils.js';
|
||
import { getReferenceImageFromConnectedNode } from './graph_query.js';
|
||
import { RefLayerWidget } from './layer_type_ref.js';
|
||
import { attachLassoHelpers } from './canvas_lasso.js';
|
||
import { attachSam2Helpers } from './canvas/canvas_sam2.js';
|
||
import RefCanvas from './canvas_main_ref.js';
|
||
|
||
// ===================================================================
|
||
// Session Storage Helpers
|
||
// ===================================================================
|
||
function safeSetSessionItem(key, value) {
|
||
try { sessionStorage.setItem(key, value); } catch (e) { /* ignore */ }
|
||
}
|
||
|
||
function safeGetSessionItem(key) {
|
||
try { return sessionStorage.getItem(key); } catch (e) { return null; }
|
||
}
|
||
|
||
// ===================================================================
|
||
// Add SAM2 Magic Wand Button Styles
|
||
// ===================================================================
|
||
const sam2Style = document.createElement('style');
|
||
sam2Style.textContent = `
|
||
.magic-wand-btn.active {
|
||
background: #4CAF50 !important;
|
||
color: white !important;
|
||
}
|
||
.magic-wand-btn:hover {
|
||
background: #555 !important;
|
||
}
|
||
`;
|
||
document.head.appendChild(sam2Style);
|
||
|
||
// ===================================================================
|
||
// Image Loading Utilities
|
||
// ===================================================================
|
||
async function loadImageFromUrl(url, canvasEditor) {
|
||
const timestampedUrl = `${url}${url.includes('?') ? '&' : '?'}t=${Date.now()}`;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
canvasEditor.loadBackgroundImage(img);
|
||
resolve(img);
|
||
};
|
||
img.onerror = (err) => {
|
||
console.error('[loadImageFromUrl] Failed to load image:', url, err);
|
||
reject(err);
|
||
};
|
||
img.src = timestampedUrl;
|
||
});
|
||
}
|
||
|
||
async function loadRefImagesFromPaths(paths, node) {
|
||
if (!Array.isArray(paths) || paths.length === 0) return [];
|
||
|
||
const images = [];
|
||
for (let i = 0; i < paths.length; i++) {
|
||
const path = paths[i];
|
||
try {
|
||
const url = new URL(path, import.meta.url).href + `?t=${Date.now()}`;
|
||
const img = await new Promise((resolve, reject) => {
|
||
const image = new Image();
|
||
image.onload = () => resolve(image);
|
||
image.onerror = reject;
|
||
image.src = url;
|
||
});
|
||
images.push(img);
|
||
} catch (err) {
|
||
console.error(`[loadRefImagesFromPaths] Failed to load ref image ${i}:`, path, err);
|
||
}
|
||
}
|
||
return images;
|
||
}
|
||
|
||
// ===================================================================
|
||
// Node Lifecycle: PrepareRefs
|
||
// ===================================================================
|
||
app.registerExtension({
|
||
name: 'WanVideoWrapper_QQ.PrepareRefs',
|
||
|
||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||
if (nodeData?.name !== 'PrepareRefs') return;
|
||
|
||
// =================================================================
|
||
// onNodeCreated – Main Setup
|
||
// =================================================================
|
||
chainCallback(nodeType.prototype, 'onNodeCreated', function () {
|
||
this.serialize_widgets = true;
|
||
this.resizable = false;
|
||
this.properties = this.properties || {};
|
||
this.properties.userAdjustedDims = this.properties.userAdjustedDims || false;
|
||
|
||
// Restore UUID from properties if available (for persistence), otherwise create new one
|
||
this.uuid = this.properties.uuid || this.uuid || makeUUID();
|
||
|
||
// Hide widgets that shouldn't be visible in UI
|
||
const widthWidget = this.widgets.find(w => w.name === 'mask_width');
|
||
const heightWidget = this.widgets.find(w => w.name === 'mask_height');
|
||
const internalStateWidget = this.widgets.find(w => w.name === 'internal_state');
|
||
const exportFilenameWidget = this.widgets.find(w => w.name === 'export_filename');
|
||
const refLayerDataWidget = this.widgets.find(w => w.name === 'ref_layer_data');
|
||
|
||
const markUserAdjusted = () => { this.properties.userAdjustedDims = true; };
|
||
|
||
[widthWidget, heightWidget].forEach(w => {
|
||
if (w) {
|
||
const orig = w.callback;
|
||
w.callback = (v) => { orig?.call(w, v); markUserAdjusted(); this.updateCanvasSizeFromWidgets?.(); };
|
||
hideWidgetForGood(this, w);
|
||
}
|
||
});
|
||
|
||
// Hide the persistence widgets (they're managed by the node, not user-editable)
|
||
[internalStateWidget, exportFilenameWidget, refLayerDataWidget].forEach(w => {
|
||
if (w) hideWidgetForGood(this, w);
|
||
});
|
||
|
||
// Size manager for consistent canvas/node sizing
|
||
this.sizeManager = new NodeSizeManager(this, {
|
||
spacingAfterCanvas: 36,
|
||
canvasWidth: widthWidget?.value ?? 640,
|
||
canvasHeight: heightWidget?.value ?? 480,
|
||
minNodeWidth: 640,
|
||
minNodeHeight: 480,
|
||
});
|
||
|
||
this.onResize = (size) => {
|
||
const constrained = this.sizeManager.onNodeResized(size);
|
||
size[0] = constrained[0];
|
||
size[1] = constrained[1];
|
||
};
|
||
|
||
// Top row controls
|
||
this.addCustomWidget(new TopRowWidget('prepare_refs_top_row', {
|
||
refreshCanvasButton: true,
|
||
refreshFramesButton: false,
|
||
bgImgControl: false,
|
||
animToggleButton: false,
|
||
}, {
|
||
onRefreshCanvas: handlePrepareRefsRefresh
|
||
}));
|
||
|
||
this.setupUI();
|
||
this.setupCanvas();
|
||
this.setupRefLayers();
|
||
this.restoreSessionImages();
|
||
});
|
||
|
||
// =================================================================
|
||
// UI Construction
|
||
// =================================================================
|
||
nodeType.prototype.setupUI = function () {
|
||
const container = this.createMainContainer();
|
||
this.domWidget = this.addDOMWidget('PrepareRefs', 'PrepareRefsCanvas', container, {
|
||
serialize: false,
|
||
hideOnZoom: false,
|
||
});
|
||
|
||
this.domWidget.computeSize = () => {
|
||
const h = this.widgets.find(w => w.name === 'mask_height')?.value ?? 480;
|
||
return [0, h];
|
||
};
|
||
};
|
||
|
||
nodeType.prototype.createMainContainer = function () {
|
||
const container = document.createElement('div');
|
||
container.id = `prepare-refs-${this.uuid}`;
|
||
container.style.cssText = `
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin: 0; padding: 0; gap: 0;
|
||
`;
|
||
|
||
const row = document.createElement('div');
|
||
row.style.cssText = `
|
||
display: flex; gap: 8px; align-items: stretch;
|
||
`;
|
||
container.appendChild(row);
|
||
|
||
this.sidebar = this.createSidebar();
|
||
row.appendChild(this.sidebar);
|
||
|
||
this.canvasWrap = this.createCanvasWrapper();
|
||
row.appendChild(this.canvasWrap);
|
||
|
||
return container;
|
||
};
|
||
|
||
nodeType.prototype.createSidebar = function () {
|
||
const sidebar = document.createElement('div');
|
||
sidebar.style.cssText = `
|
||
width: 120px; flex: 0 0 120px; display: flex; flex-direction: column;
|
||
gap: 8px; padding: 8px; background: transparent; border: 1px solid #3a3a3a;
|
||
color: #b5b5b5; font-size: 12px; box-sizing: border-box;
|
||
`;
|
||
|
||
const addBtn = this.createAddRefButton();
|
||
sidebar.appendChild(addBtn);
|
||
|
||
this.refLayersContainer = document.createElement('div');
|
||
this.refLayersContainer.style.cssText = `
|
||
flex: 1; display: flex; flex-direction: column; gap: 4px;
|
||
overflow-y: auto; margin-top: 4px;
|
||
`;
|
||
sidebar.appendChild(this.refLayersContainer);
|
||
|
||
return sidebar;
|
||
};
|
||
|
||
nodeType.prototype.createAddRefButton = function () {
|
||
const btn = document.createElement('button');
|
||
btn.textContent = '+ Add ref';
|
||
btn.style.cssText = `
|
||
padding: 8px 12px; background: #1e1e1e; color: #ddd; border: 0.75px solid #00000044;
|
||
border-radius: 4px; font-size: 12px; font-weight: 500; cursor: pointer;
|
||
box-shadow: 0 1px 0 #000000aa, inset 0 0.75px 0 #ffffff22;
|
||
transition: all 0.05s ease;
|
||
`;
|
||
['mousedown', 'mouseleave', 'mouseup'].forEach(ev => {
|
||
btn.addEventListener(ev, () => {
|
||
btn.style.background = ev === 'mousedown' ? '#444' : '#1e1e1e';
|
||
btn.style.transform = ev === 'mousedown' ? 'translateY(1px)' : 'translateY(0)';
|
||
});
|
||
});
|
||
btn.onclick = () => this.addRefLayer();
|
||
return btn;
|
||
};
|
||
|
||
nodeType.prototype.createCanvasWrapper = function () {
|
||
const wrap = document.createElement('div');
|
||
wrap.style.cssText = `
|
||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||
overflow: hidden; padding-top: 0; box-sizing: border-box;
|
||
`;
|
||
|
||
this.refsCanvas = document.createElement('canvas');
|
||
this.refsCanvas.style.cssText = `
|
||
background: #222; border: 1px solid gray; max-width: 100%; max-height: 100%;
|
||
display: block; border-radius: 0;
|
||
`;
|
||
wrap.appendChild(this.refsCanvas);
|
||
|
||
return wrap;
|
||
};
|
||
|
||
// =================================================================
|
||
// Canvas & RefCanvas Setup
|
||
// =================================================================
|
||
nodeType.prototype.setupCanvas = function () {
|
||
this.refCanvasEditor = new RefCanvas(this.refsCanvas, this);
|
||
attachLassoHelpers(this);
|
||
attachSam2Helpers(this);
|
||
|
||
this.refreshCanvas = () => {
|
||
this.refCanvasEditor.render();
|
||
|
||
const ctx = this.refsCanvas.getContext('2d');
|
||
const { width, height } = this.refsCanvas;
|
||
ctx.strokeStyle = 'gray';
|
||
ctx.lineWidth = 1;
|
||
ctx.strokeRect(0, 0, width, height);
|
||
|
||
if (!this.refCanvasEditor.backgroundImage) {
|
||
ctx.fillStyle = '#787878';
|
||
ctx.font = '12px sans-serif';
|
||
ctx.fillText('PrepareRefs canvas', 20, 26);
|
||
}
|
||
|
||
this.refLayers?.forEach(layer => {
|
||
if (layer.value.lassoShape) {
|
||
const isSelected = this.selectedRefLayer === layer;
|
||
this.renderLayerLassoShapes?.(ctx, layer.value.lassoShape, width, height, isSelected, this.refCanvasEditor);
|
||
}
|
||
});
|
||
|
||
this.renderLassoPreview?.();
|
||
|
||
// Render SAM2 visuals if in SAM2 mode
|
||
if (this._sam2Mode) {
|
||
this.renderSam2Visuals?.(ctx, width, height);
|
||
}
|
||
};
|
||
|
||
this.forceCanvasRefresh = () => {
|
||
this.refreshCanvas();
|
||
this.setDirtyCanvas(true, true);
|
||
};
|
||
};
|
||
|
||
// =================================================================
|
||
// Ref Layer Management
|
||
// =================================================================
|
||
nodeType.prototype.setupRefLayers = function () {
|
||
this.refLayers = [];
|
||
this.selectedRefLayer = null;
|
||
|
||
// ref_layer_data widget is now created by Python INPUT_TYPES, not here
|
||
// Just ensure its callback updates the layer data
|
||
const refDataWidget = this.widgets?.find(w => w.name === 'ref_layer_data');
|
||
if (refDataWidget) {
|
||
const origCallback = refDataWidget.callback;
|
||
refDataWidget.callback = () => {
|
||
origCallback?.call(refDataWidget);
|
||
this.updateRefDataWidget();
|
||
};
|
||
}
|
||
};
|
||
|
||
nodeType.prototype.getRefLayerData = function () {
|
||
return (this.refLayers || [])
|
||
.map(l => ({ ...l.value }));
|
||
};
|
||
|
||
nodeType.prototype.restoreRefLayers = function (layerDataArray) {
|
||
if (!Array.isArray(layerDataArray) || layerDataArray.length === 0) return;
|
||
|
||
// Clear existing layers
|
||
this.refLayers.forEach(layer => {
|
||
if (layer.domElement?.parentNode) {
|
||
layer.domElement.parentNode.removeChild(layer.domElement);
|
||
}
|
||
});
|
||
this.refLayers = [];
|
||
this.selectedRefLayer = null;
|
||
|
||
// Recreate layers from saved data
|
||
layerDataArray.forEach((layerData, index) => {
|
||
const layer = new RefLayerWidget(layerData.name || `ref_${index + 1}`);
|
||
layer.value = { ...layerData };
|
||
layer.node = this;
|
||
this.refLayers.push(layer);
|
||
|
||
const el = this.createRefLayerElement(layer);
|
||
this.refLayersContainer.appendChild(el);
|
||
layer.domElement = el;
|
||
});
|
||
|
||
// Save to session storage for quick restore
|
||
try {
|
||
const sessionKey = `prepare-refs-layers-${this.uuid}`;
|
||
safeSetSessionItem(sessionKey, JSON.stringify(layerDataArray));
|
||
} catch (e) {
|
||
console.warn('[PrepareRefs] Failed to save layers to session:', e);
|
||
}
|
||
|
||
this.forceCanvasRefresh();
|
||
};
|
||
|
||
nodeType.prototype.addRefLayer = function () {
|
||
// Exit SAM2 mode if active before creating new layer
|
||
if (this._sam2Mode) {
|
||
this._sam2Mode = false;
|
||
const oldLayer = this._sam2ActiveLayer;
|
||
this._sam2ActiveLayer = null;
|
||
this._sam2Points = [];
|
||
this._sam2MaskPath = null;
|
||
this.clearSam2Visuals?.();
|
||
|
||
// Update wand button state on old layer if it exists
|
||
if (oldLayer && oldLayer.magicWandButton) {
|
||
oldLayer.magicWandButton.classList.remove('active');
|
||
oldLayer.magicWandButton.style.background = 'transparent';
|
||
oldLayer.magicWandButton.style.color = '#888';
|
||
}
|
||
}
|
||
|
||
const nextNum = (this.refLayers.reduce((m, l) => {
|
||
const n = parseInt(l.value.name.match(/ref_(\d+)/)?.[1] || 0);
|
||
return Math.max(m, n);
|
||
}, 0)) + 1;
|
||
|
||
const layer = new RefLayerWidget(`ref_${nextNum}`);
|
||
layer.value = { on: true, name: `ref_${nextNum}` };
|
||
layer.node = this;
|
||
this.refLayers.push(layer);
|
||
|
||
const el = this.createRefLayerElement(layer);
|
||
this.refLayersContainer.appendChild(el);
|
||
layer.domElement = el;
|
||
|
||
this.selectRefLayer(layer);
|
||
this.enterLassoMode?.(layer);
|
||
this.updateRefDataWidget();
|
||
this.forceCanvasRefresh();
|
||
};
|
||
|
||
nodeType.prototype.createRefLayerElement = function (layer) {
|
||
const el = document.createElement('div');
|
||
el.style.cssText = `
|
||
display: flex; align-items: center; padding: 4px 6px; background: #262626;
|
||
border: 1px solid #3a3a3a; border-radius: 3px; font-size: 11px; color: #e0e0e0;
|
||
cursor: pointer; user-select: none; transition: background 0.1s ease;
|
||
`;
|
||
|
||
// 1. Delete button (first)
|
||
const delBtn = this.createIconButton('Delete', '✕', () => this.removeRefLayer(layer));
|
||
el.appendChild(delBtn);
|
||
|
||
// 2. Name (second)
|
||
const name = document.createElement('span');
|
||
name.textContent = layer.value.name;
|
||
name.style.flex = '1';
|
||
name.style.marginLeft = '6px'; // Add spacing after delete button
|
||
el.appendChild(name);
|
||
|
||
// 3. Pencil/Edit button (third)
|
||
const editBtn = this.createIconButton('Edit', '✎', () => {
|
||
this.selectRefLayer(layer);
|
||
|
||
// If in SAM2 mode, exit SAM2 first (bakes mask to pencil)
|
||
if (this._sam2Mode) {
|
||
this.exitSam2Mode?.();
|
||
}
|
||
|
||
// Then handle pencil mode
|
||
if (this._lassoDrawingActive && this._lassoActiveLayer === layer) {
|
||
this.exitLassoMode?.();
|
||
} else {
|
||
this.enterLassoMode?.(layer);
|
||
}
|
||
});
|
||
el.appendChild(editBtn);
|
||
|
||
// Store reference to edit button for color updates
|
||
layer.editButton = editBtn;
|
||
|
||
// 4. Magic Wand button (last)
|
||
const magicWandBtn = this.createIconButton('Magic Wand (SAM2)', '🪄', () => {
|
||
this.selectRefLayer(layer);
|
||
this.toggleSam2Mode?.();
|
||
});
|
||
magicWandBtn.classList.add('magic-wand-btn');
|
||
el.appendChild(magicWandBtn);
|
||
|
||
// Store reference to magic wand button for state updates
|
||
layer.magicWandButton = magicWandBtn;
|
||
|
||
el.onclick = (e) => {
|
||
if (e.target.tagName === 'BUTTON') return;
|
||
if (this.selectedRefLayer === layer) {
|
||
this.deselectRefLayer();
|
||
} else {
|
||
this.selectRefLayer(layer);
|
||
}
|
||
};
|
||
|
||
return el;
|
||
};
|
||
|
||
nodeType.prototype.createIconButton = function (title, symbol, callback) {
|
||
const btn = document.createElement('button');
|
||
btn.innerHTML = symbol;
|
||
btn.title = title;
|
||
btn.style.cssText = `
|
||
width: 18px; height: 18px; padding: 0; background: transparent;
|
||
border: none; color: #888; cursor: pointer; font-size: 12px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
`;
|
||
btn.onmousedown = (e) => { e.stopPropagation(); btn.style.color = '#fff'; };
|
||
btn.onmouseleave = () => {
|
||
// Only reset to gray if not in edit mode
|
||
const isInEditMode = this._lassoDrawingActive && this._lassoActiveLayer &&
|
||
this._lassoActiveLayer.editButton === btn;
|
||
if (!isInEditMode) {
|
||
btn.style.color = '#888';
|
||
}
|
||
};
|
||
btn.onmouseup = () => {
|
||
// Only reset to gray if not in edit mode
|
||
const isInEditMode = this._lassoDrawingActive && this._lassoActiveLayer &&
|
||
this._lassoActiveLayer.editButton === btn;
|
||
if (!isInEditMode) {
|
||
btn.style.color = '#888';
|
||
}
|
||
};
|
||
btn.onclick = (e) => { e.stopPropagation(); callback(); };
|
||
return btn;
|
||
};
|
||
|
||
nodeType.prototype.selectRefLayer = function (layer) {
|
||
if (this.selectedRefLayer?.domElement) {
|
||
this.selectedRefLayer.domElement.style.background = '#262626';
|
||
}
|
||
this.selectedRefLayer = layer;
|
||
if (layer.domElement) layer.domElement.style.background = '#0c0c0c';
|
||
this.exitLassoMode?.();
|
||
this.updateLayerButtonStates();
|
||
this.forceCanvasRefresh();
|
||
};
|
||
|
||
nodeType.prototype.deselectRefLayer = function () {
|
||
if (this.selectedRefLayer?.domElement) {
|
||
this.selectedRefLayer.domElement.style.background = '#262626';
|
||
}
|
||
this.selectedRefLayer = null;
|
||
this.exitLassoMode?.();
|
||
this.updateLayerButtonStates();
|
||
this.forceCanvasRefresh();
|
||
};
|
||
|
||
nodeType.prototype.removeRefLayer = function (layer) {
|
||
const idx = this.refLayers.indexOf(layer);
|
||
if (idx === -1) return;
|
||
|
||
// Clear SAM2 mode if deleting the active SAM2 layer
|
||
if (this._sam2Mode && this._sam2ActiveLayer === layer) {
|
||
this._sam2Mode = false;
|
||
this._sam2ActiveLayer = null;
|
||
this._sam2Points = [];
|
||
this._sam2MaskPath = null;
|
||
this.clearSam2Visuals?.();
|
||
|
||
// Update wand button state if it exists
|
||
if (layer.magicWandButton) {
|
||
layer.magicWandButton.classList.remove('active');
|
||
layer.magicWandButton.style.background = 'transparent';
|
||
layer.magicWandButton.style.color = '#888';
|
||
}
|
||
}
|
||
|
||
if (this.selectedRefLayer === layer) this.selectedRefLayer = null;
|
||
this.refLayers.splice(idx, 1);
|
||
if (layer.domElement?.parentNode) layer.domElement.parentNode.removeChild(layer.domElement);
|
||
|
||
this.renumberRefLayers();
|
||
this.updateRefDataWidget();
|
||
this.forceCanvasRefresh();
|
||
};
|
||
|
||
nodeType.prototype.renumberRefLayers = function () {
|
||
this.refLayers.forEach((l, i) => {
|
||
l.value.name = `ref_${i + 1}`;
|
||
if (l.domElement) l.domElement.querySelector('span').textContent = l.value.name;
|
||
});
|
||
};
|
||
|
||
nodeType.prototype.updateRefDataWidget = function () {
|
||
const w = this.widgets.find(w => w.name === "ref_layer_data");
|
||
if (w) {
|
||
const layerData = this.getRefLayerData();
|
||
|
||
// Widget is STRING type in Python, so serialize to JSON
|
||
w.value = JSON.stringify(layerData);
|
||
|
||
// Also save to sessionStorage for quick restore on page refresh
|
||
try {
|
||
const sessionKey = `prepare-refs-layers-${this.uuid}`;
|
||
safeSetSessionItem(sessionKey, JSON.stringify(layerData));
|
||
} catch (e) {
|
||
console.warn('[PrepareRefs] Failed to save layers to session:', e);
|
||
}
|
||
}
|
||
};
|
||
|
||
nodeType.prototype.updateLayerButtonStates = function () {
|
||
this.refLayers?.forEach(layer => {
|
||
if (layer.editButton) {
|
||
// Make pen icon green if this layer is in edit mode
|
||
const isInEditMode = this._lassoDrawingActive && this._lassoActiveLayer === layer;
|
||
layer.editButton.style.color = isInEditMode ? '#4CAF50' : '#888';
|
||
}
|
||
});
|
||
};
|
||
|
||
// =================================================================
|
||
// Dimension & Canvas Size Helpers
|
||
// =================================================================
|
||
nodeType.prototype.updateCanvasSizeFromWidgets = function () {
|
||
const w = this.widgets.find(w => w.name === 'mask_width')?.value ?? 640;
|
||
const h = this.widgets.find(w => w.name === 'mask_height')?.value ?? 480;
|
||
|
||
this.refCanvasEditor.setSize(w, h);
|
||
this.refsCanvas.style.width = this.refsCanvas.style.height = '';
|
||
this.sizeManager.setCanvasSize(w, h);
|
||
this.refreshCanvas();
|
||
this.sizeManager.updateSize(true);
|
||
};
|
||
|
||
// =================================================================
|
||
// Image Refresh & Persistence
|
||
// =================================================================
|
||
nodeType.prototype.updateReferenceImageFromConnectedNode = async function () {
|
||
try {
|
||
const base64 = await getReferenceImageFromConnectedNode(this, 'bg_image');
|
||
if (!base64) return this.forceCanvasRefresh();
|
||
|
||
const img = await new Promise((resolve, reject) => {
|
||
const i = new Image();
|
||
i.onload = () => { this.refCanvasEditor.loadBackgroundImage(i); resolve(i); };
|
||
i.onerror = reject;
|
||
i.src = base64;
|
||
});
|
||
|
||
this.loadedBgImage = img;
|
||
|
||
// Save via backend
|
||
await fetch('/wanvideowrapper_qq/save_prepare_refs_images', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ bg_image: base64, ref_images: [] })
|
||
})
|
||
.then(r => r.json())
|
||
.then(res => {
|
||
if (res.success && res.paths?.bg_image_path) {
|
||
this.properties.bg_image_path = res.paths.bg_image_path;
|
||
safeSetSessionItem(`prepare-refs-bg-${this.uuid}`, res.paths.bg_image_path);
|
||
}
|
||
})
|
||
.catch(console.error);
|
||
|
||
this.forceCanvasRefresh();
|
||
} catch (err) {
|
||
console.error('[PrepareRefs] Failed to refresh background:', err);
|
||
this.forceCanvasRefresh();
|
||
}
|
||
};
|
||
|
||
nodeType.prototype.updateExtraRefsFromConnectedNode = async function () {
|
||
try {
|
||
// Safety check: only run if we have the required functions
|
||
if (typeof findConnectedSourceNode !== 'function' || typeof extractImagesFromSourceNode !== 'function') {
|
||
console.warn('[PrepareRefs] Graph query functions not available, skipping extra refs update');
|
||
return;
|
||
}
|
||
|
||
// Fetch images from extra_refs connection (e.g., CreateImageList)
|
||
// Use lower-level functions since getReferenceImagesFromConnectedNode is hardcoded to 'ref_images'
|
||
let sourceNodeObj = findConnectedSourceNode(this, 'extra_refs');
|
||
if (!sourceNodeObj) {
|
||
sourceNodeObj = findDeepSourceNode(this, 'extra_refs');
|
||
}
|
||
|
||
let extraRefImages = [];
|
||
if (sourceNodeObj) {
|
||
extraRefImages = await extractImagesFromSourceNode(sourceNodeObj, true);
|
||
}
|
||
|
||
if (extraRefImages && extraRefImages.length > 0) {
|
||
console.log(`[PrepareRefs] Found ${extraRefImages.length} extra ref images from connected node`);
|
||
this.loadedExtraRefImages = extraRefImages;
|
||
|
||
// Store in properties for persistence
|
||
this.properties.extra_ref_count = extraRefImages.length;
|
||
} else {
|
||
// No extra refs - this is normal, don't log as error
|
||
this.loadedExtraRefImages = null;
|
||
this.properties.extra_ref_count = 0;
|
||
}
|
||
} catch (err) {
|
||
// Silently handle errors - extra refs are optional
|
||
console.warn('[PrepareRefs] Could not fetch extra refs:', err.message);
|
||
this.loadedExtraRefImages = null;
|
||
this.properties.extra_ref_count = 0;
|
||
}
|
||
};
|
||
|
||
// =================================================================
|
||
// Session Restore on Load
|
||
// =================================================================
|
||
nodeType.prototype.restoreSessionImages = function () {
|
||
// Restore layers from sessionStorage
|
||
const sessionLayers = safeGetSessionItem(`prepare-refs-layers-${this.uuid}`);
|
||
if (sessionLayers) {
|
||
try {
|
||
const layerData = JSON.parse(sessionLayers);
|
||
if (Array.isArray(layerData) && layerData.length > 0) {
|
||
this.restoreRefLayers(layerData);
|
||
console.log('[PrepareRefs] Restored', layerData.length, 'layers from session');
|
||
}
|
||
} catch (e) {
|
||
console.warn('[PrepareRefs] Failed to restore layers from session:', e);
|
||
}
|
||
}
|
||
|
||
const bgPath = safeGetSessionItem(`prepare-refs-bg-${this.uuid}`);
|
||
const refPathsJson = safeGetSessionItem(`prepare-refs-images-${this.uuid}`);
|
||
|
||
if (bgPath) {
|
||
try {
|
||
// CRITICAL: Resolve relative path correctly using import.meta.url
|
||
const url = new URL(bgPath, import.meta.url).href;
|
||
loadImageFromUrl(url, this.refCanvasEditor)
|
||
.then(() => {
|
||
console.log('[PrepareRefs] Restored bg_image from session:', bgPath);
|
||
this.forceCanvasRefresh();
|
||
})
|
||
.catch(err => console.error('[PrepareRefs] Failed to restore bg_image:', err));
|
||
} catch (e) {
|
||
console.error('[PrepareRefs] Invalid bg_image path in session:', bgPath, e);
|
||
}
|
||
}
|
||
|
||
if (refPathsJson) {
|
||
try {
|
||
const paths = JSON.parse(refPathsJson);
|
||
if (Array.isArray(paths) && paths.length > 0) {
|
||
const resolvedPaths = paths.map(p => new URL(p, import.meta.url).href);
|
||
loadRefImagesFromPaths(resolvedPaths, this).then(imgs => {
|
||
this.loadedRefImages = imgs;
|
||
this.forceCanvasRefresh();
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('[PrepareRefs] Failed to parse/restore ref images from session:', e);
|
||
}
|
||
}
|
||
|
||
this.updateCanvasSizeFromWidgets();
|
||
};
|
||
|
||
// =================================================================
|
||
// Lifecycle Callbacks
|
||
// =================================================================
|
||
chainCallback(nodeType.prototype, 'onExecuted', async function (message) {
|
||
// Auto-size from incoming image if user hasn't manually adjusted
|
||
if (!this.properties.userAdjustedDims && message?.ui?.bg_image_dims?.[0]) {
|
||
const { width, height } = message.ui.bg_image_dims[0];
|
||
this.setDimensionValue?.('mask_width', Math.round(width));
|
||
this.setDimensionValue?.('mask_height', Math.round(height));
|
||
this.updateCanvasSizeFromWidgets();
|
||
}
|
||
|
||
// Refresh canvas with connected bg_image
|
||
await this.updateReferenceImageFromConnectedNode();
|
||
|
||
// Fetch extra refs from connected node (e.g., CreateImageList)
|
||
// TEMPORARILY DISABLED FOR DEBUGGING
|
||
/*
|
||
if (this.updateExtraRefsFromConnectedNode) {
|
||
try {
|
||
await this.updateExtraRefsFromConnectedNode();
|
||
} catch (err) {
|
||
console.warn('[PrepareRefs] Error in updateExtraRefsFromConnectedNode:', err);
|
||
}
|
||
}
|
||
*/
|
||
|
||
// Restore bg/ref images from execution message
|
||
if (message?.ui?.bg_image_path?.[0]) {
|
||
const path = message.ui.bg_image_path[0];
|
||
this.properties.bg_image_path = path;
|
||
safeSetSessionItem(`prepare-refs-bg-${this.uuid}`, path);
|
||
const url = new URL(path, import.meta.url).href;
|
||
await loadImageFromUrl(url, this.refCanvasEditor);
|
||
}
|
||
|
||
if (message?.ui?.ref_images_paths?.length) {
|
||
const paths = message.ui.ref_images_paths;
|
||
this.properties.ref_images_paths = paths;
|
||
safeSetSessionItem(`prepare-refs-images-${this.uuid}`, JSON.stringify(paths));
|
||
const resolvedPaths = paths.map(p => new URL(p, import.meta.url).href);
|
||
this.loadedRefImages = await loadRefImagesFromPaths(resolvedPaths, this);
|
||
}
|
||
|
||
this.forceCanvasRefresh();
|
||
});
|
||
|
||
chainCallback(nodeType.prototype, 'onConfigure', async function (info) {
|
||
// CRITICAL: Restore UUID from properties if available
|
||
// onConfigure runs AFTER onNodeCreated, so we need to update UUID here
|
||
if (this.properties?.uuid && this.properties.uuid !== this.uuid) {
|
||
this.uuid = this.properties.uuid;
|
||
}
|
||
|
||
// Find the ref_layer_data widget
|
||
const refDataWidget = this.widgets?.find(w => w.name === 'ref_layer_data');
|
||
|
||
// ComfyUI's configure applies widget values from widgets_values array to widgets BEFORE onConfigure runs
|
||
// The widget value is a JSON string (STRING type in Python), so parse it
|
||
let layerData = null;
|
||
if (refDataWidget && refDataWidget.value) {
|
||
try {
|
||
// Parse JSON string to get layer data array
|
||
layerData = typeof refDataWidget.value === 'string' ? JSON.parse(refDataWidget.value) : refDataWidget.value;
|
||
} catch (e) {
|
||
console.warn('[PrepareRefs onConfigure] Failed to parse ref_layer_data:', e);
|
||
layerData = [];
|
||
}
|
||
}
|
||
|
||
// If we have layer data from the workflow (even empty array), use it and DON'T restore from session
|
||
// This ensures loading a workflow with no layers doesn't restore old session data
|
||
if (layerData !== null && layerData !== undefined && Array.isArray(layerData)) {
|
||
if (layerData.length > 0) {
|
||
this.restoreRefLayers(layerData);
|
||
} else {
|
||
// Clear existing layers since the workflow has none
|
||
this.refLayers?.forEach(layer => {
|
||
if (layer.domElement?.parentNode) {
|
||
layer.domElement.parentNode.removeChild(layer.domElement);
|
||
}
|
||
});
|
||
this.refLayers = [];
|
||
this.selectedRefLayer = null;
|
||
}
|
||
} else {
|
||
// Only restore from session if no workflow data exists (undefined/null)
|
||
const sessionLayers = safeGetSessionItem(`prepare-refs-layers-${this.uuid}`);
|
||
if (sessionLayers) {
|
||
try {
|
||
const parsedData = JSON.parse(sessionLayers);
|
||
if (Array.isArray(parsedData) && parsedData.length > 0) {
|
||
this.restoreRefLayers(parsedData);
|
||
}
|
||
} catch (e) {
|
||
console.warn('[PrepareRefs] Failed to restore layers from session:', e);
|
||
}
|
||
}
|
||
}
|
||
|
||
const savedBgPath = this.properties.bg_image_path;
|
||
const savedRefPaths = this.properties.ref_images_paths;
|
||
|
||
const sessionBgPath = safeGetSessionItem(`prepare-refs-bg-${this.uuid}`);
|
||
const sessionRefPaths = safeGetSessionItem(`prepare-refs-images-${this.uuid}`);
|
||
|
||
const finalBgPath = savedBgPath || sessionBgPath;
|
||
const finalRefPaths = savedRefPaths || (sessionRefPaths ? JSON.parse(sessionRefPaths) : null);
|
||
|
||
if (finalBgPath) {
|
||
try {
|
||
const url = new URL(finalBgPath, import.meta.url).href;
|
||
await loadImageFromUrl(url, this.refCanvasEditor);
|
||
} catch (e) {
|
||
console.error('[onConfigure] Failed to load bg_image:', finalBgPath, e);
|
||
}
|
||
}
|
||
|
||
if (finalRefPaths && Array.isArray(finalRefPaths) && finalRefPaths.length > 0) {
|
||
try {
|
||
const urls = finalRefPaths.map(p => new URL(p, import.meta.url).href);
|
||
this.loadedRefImages = await loadRefImagesFromPaths(urls, this);
|
||
console.log('[onConfigure] Restored', this.loadedRefImages.length, 'ref images');
|
||
} catch (e) {
|
||
console.error('[onConfigure] Failed to restore ref images:', e);
|
||
}
|
||
}
|
||
|
||
this.forceCanvasRefresh();
|
||
});
|
||
|
||
// Ensure ref data and paths are serialized
|
||
const origSerialize = nodeType.prototype.onSerialize;
|
||
nodeType.prototype.onSerialize = function (o) {
|
||
const refWidget = this.widgets?.find(w => w.name === "ref_layer_data");
|
||
if (refWidget) {
|
||
const layerData = this.getRefLayerData();
|
||
// Widget is STRING type in Python, so serialize to JSON
|
||
refWidget.value = JSON.stringify(layerData);
|
||
}
|
||
|
||
o.properties = o.properties || {};
|
||
o.properties.uuid = this.uuid; // Save UUID for session storage persistence
|
||
if (this.properties.bg_image_path) o.properties.bg_image_path = this.properties.bg_image_path;
|
||
if (this.properties.ref_images_paths) o.properties.ref_images_paths = this.properties.ref_images_paths;
|
||
|
||
origSerialize?.call(this, o);
|
||
};
|
||
},
|
||
}); |