mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Add layer copy/paste and node duplication with layers
Implements two new features: - Layer copy/paste within canvas using Ctrl+C/V - Node duplication that preserves all layers Layer Copy/Paste: - Added Ctrl+V keyboard shortcut handler for pasting layers - Intercept keydown events during capture phase to handle before ComfyUI - Focus canvas when layer is clicked to ensure shortcuts work - Prevent layers panel from stealing focus on mousedown Node Duplication: - Store source node ID during serialize for copy operations - Track pending copy sources across node ID changes (-1 to real ID) - Copy canvas state from source to destination in onAdded hook - Use Map to persist copy metadata through node lifecycle
This commit is contained in:
@@ -104,6 +104,8 @@ export class CanvasInteractions {
|
||||
// Add a blur event listener to the window to reset key states
|
||||
window.addEventListener('blur', this.onBlur);
|
||||
document.addEventListener('paste', this.onPaste);
|
||||
// Intercept Ctrl+V during capture phase to handle layer paste before ComfyUI
|
||||
document.addEventListener('keydown', this.onKeyDown, { capture: true });
|
||||
this.canvas.canvas.addEventListener('mouseenter', this.onMouseEnter);
|
||||
this.canvas.canvas.addEventListener('mouseleave', this.onMouseLeave);
|
||||
this.canvas.canvas.addEventListener('dragover', this.onDragOver);
|
||||
@@ -119,6 +121,8 @@ export class CanvasInteractions {
|
||||
this.canvas.canvas.removeEventListener('wheel', this.onWheel);
|
||||
this.canvas.canvas.removeEventListener('keydown', this.onKeyDown);
|
||||
this.canvas.canvas.removeEventListener('keyup', this.onKeyUp);
|
||||
// Remove document-level capture listener
|
||||
document.removeEventListener('keydown', this.onKeyDown, { capture: true });
|
||||
window.removeEventListener('blur', this.onBlur);
|
||||
document.removeEventListener('paste', this.onPaste);
|
||||
this.canvas.canvas.removeEventListener('mouseenter', this.onMouseEnter);
|
||||
@@ -570,6 +574,12 @@ export class CanvasInteractions {
|
||||
this.canvas.canvasLayers.copySelectedLayers();
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
// Paste layers from internal clipboard
|
||||
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
||||
this.canvas.canvasLayers.pasteLayers();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
break;
|
||||
|
||||
@@ -285,6 +285,8 @@ export class CanvasLayersPanel {
|
||||
if (nameElement && nameElement.classList.contains('editing')) {
|
||||
return;
|
||||
}
|
||||
// Prevent the layers panel from stealing focus
|
||||
e.preventDefault();
|
||||
this.handleLayerClick(e, layer, index);
|
||||
});
|
||||
// --- PRAWY PRZYCISK: ODJAZNACZ LAYER ---
|
||||
@@ -329,6 +331,8 @@ export class CanvasLayersPanel {
|
||||
// Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM
|
||||
this.updateSelectionAppearance();
|
||||
this.updateButtonStates();
|
||||
// Focus the canvas so keyboard shortcuts (like Ctrl+C/V) work for layer operations
|
||||
this.canvas.canvas.focus();
|
||||
log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`);
|
||||
}
|
||||
startEditingLayerName(nameElement, layer) {
|
||||
|
||||
@@ -1076,6 +1076,8 @@ app.registerExtension({
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass === "LayerForgeNode") {
|
||||
// Map to track pending copy sources across node ID changes
|
||||
const pendingCopySources = new Map();
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
log.debug("CanvasNode onNodeCreated: Base widget setup.");
|
||||
@@ -1106,6 +1108,38 @@ app.registerExtension({
|
||||
log.info(`Registered CanvasNode instance for ID: ${this.id}`);
|
||||
// Store the canvas widget on the node
|
||||
this.canvasWidget = canvasWidget;
|
||||
// Check if this node has a pending copy source (from onConfigure)
|
||||
// Check both the current ID and -1 (temporary ID during paste)
|
||||
let sourceNodeId = pendingCopySources.get(this.id);
|
||||
if (!sourceNodeId) {
|
||||
sourceNodeId = pendingCopySources.get(-1);
|
||||
if (sourceNodeId) {
|
||||
// Transfer from -1 to the real ID and clear -1
|
||||
pendingCopySources.delete(-1);
|
||||
}
|
||||
}
|
||||
if (sourceNodeId && sourceNodeId !== this.id) {
|
||||
log.info(`Node ${this.id} will copy canvas state from node ${sourceNodeId}`);
|
||||
// Clear the flag
|
||||
pendingCopySources.delete(this.id);
|
||||
// Copy the canvas state now that the widget is initialized
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const { getCanvasState, setCanvasState } = await import('./db.js');
|
||||
const sourceState = await getCanvasState(String(sourceNodeId));
|
||||
if (!sourceState) {
|
||||
log.debug(`No canvas state found for source node ${sourceNodeId}`);
|
||||
return;
|
||||
}
|
||||
await setCanvasState(String(this.id), sourceState);
|
||||
await canvasWidget.canvas.loadInitialState();
|
||||
log.info(`Canvas state copied successfully from node ${sourceNodeId} to node ${this.id}`);
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Error copying canvas state:`, error);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
// Check if there are already connected inputs
|
||||
setTimeout(() => {
|
||||
if (this.inputs && this.inputs.length > 0) {
|
||||
@@ -1271,6 +1305,31 @@ app.registerExtension({
|
||||
}
|
||||
return onRemoved?.apply(this, arguments);
|
||||
};
|
||||
// Handle copy/paste - save canvas state when copying
|
||||
const originalSerialize = nodeType.prototype.serialize;
|
||||
nodeType.prototype.serialize = function () {
|
||||
const data = originalSerialize ? originalSerialize.apply(this) : {};
|
||||
// Store a reference to the source node ID so we can copy layer data
|
||||
data.sourceNodeId = this.id;
|
||||
log.debug(`Serializing node ${this.id} for copy`);
|
||||
return data;
|
||||
};
|
||||
// Handle copy/paste - load canvas state from source node when pasting
|
||||
const originalConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = async function (data) {
|
||||
if (originalConfigure) {
|
||||
originalConfigure.apply(this, [data]);
|
||||
}
|
||||
// Store the source node ID in the map (persists across node ID changes)
|
||||
// This will be picked up later in onAdded when the canvas widget is ready
|
||||
if (data.sourceNodeId && data.sourceNodeId !== this.id) {
|
||||
const existingSource = pendingCopySources.get(this.id);
|
||||
if (!existingSource) {
|
||||
pendingCopySources.set(this.id, data.sourceNodeId);
|
||||
log.debug(`Stored pending copy source: ${data.sourceNodeId} for node ${this.id}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||
// FIRST: Call original to let other extensions add their options
|
||||
|
||||
@@ -176,6 +176,9 @@ export class CanvasInteractions {
|
||||
|
||||
document.addEventListener('paste', this.onPaste as unknown as EventListener);
|
||||
|
||||
// Intercept Ctrl+V during capture phase to handle layer paste before ComfyUI
|
||||
document.addEventListener('keydown', this.onKeyDown as EventListener, { capture: true });
|
||||
|
||||
this.canvas.canvas.addEventListener('mouseenter', this.onMouseEnter as EventListener);
|
||||
this.canvas.canvas.addEventListener('mouseleave', this.onMouseLeave as EventListener);
|
||||
|
||||
@@ -195,6 +198,9 @@ export class CanvasInteractions {
|
||||
this.canvas.canvas.removeEventListener('keydown', this.onKeyDown as EventListener);
|
||||
this.canvas.canvas.removeEventListener('keyup', this.onKeyUp as EventListener);
|
||||
|
||||
// Remove document-level capture listener
|
||||
document.removeEventListener('keydown', this.onKeyDown as EventListener, { capture: true });
|
||||
|
||||
window.removeEventListener('blur', this.onBlur);
|
||||
document.removeEventListener('paste', this.onPaste as unknown as EventListener);
|
||||
|
||||
@@ -697,6 +703,12 @@ export class CanvasInteractions {
|
||||
this.canvas.canvasLayers.copySelectedLayers();
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
// Paste layers from internal clipboard
|
||||
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
||||
this.canvas.canvasLayers.pasteLayers();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
break;
|
||||
|
||||
@@ -336,6 +336,8 @@ export class CanvasLayersPanel {
|
||||
if (nameElement && nameElement.classList.contains('editing')) {
|
||||
return;
|
||||
}
|
||||
// Prevent the layers panel from stealing focus
|
||||
e.preventDefault();
|
||||
this.handleLayerClick(e, layer, index);
|
||||
});
|
||||
|
||||
@@ -383,11 +385,14 @@ export class CanvasLayersPanel {
|
||||
// Aktualizuj wewnętrzny stan zaznaczenia w obiekcie canvas
|
||||
// Ta funkcja NIE powinna już wywoływać onSelectionChanged w panelu.
|
||||
this.canvas.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
|
||||
|
||||
|
||||
// Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM
|
||||
this.updateSelectionAppearance();
|
||||
this.updateButtonStates();
|
||||
|
||||
// Focus the canvas so keyboard shortcuts (like Ctrl+C/V) work for layer operations
|
||||
this.canvas.canvas.focus();
|
||||
|
||||
log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.canvasSelection.selectedLayers.length}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1239,6 +1239,9 @@ app.registerExtension({
|
||||
|
||||
async beforeRegisterNodeDef(nodeType: any, nodeData: any, app: ComfyApp) {
|
||||
if (nodeType.comfyClass === "LayerForgeNode") {
|
||||
// Map to track pending copy sources across node ID changes
|
||||
const pendingCopySources = new Map<number, number>();
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function (this: ComfyNode) {
|
||||
log.debug("CanvasNode onNodeCreated: Base widget setup.");
|
||||
@@ -1271,10 +1274,47 @@ app.registerExtension({
|
||||
const canvasWidget = await createCanvasWidget(this, null, app);
|
||||
canvasNodeInstances.set(this.id, canvasWidget);
|
||||
log.info(`Registered CanvasNode instance for ID: ${this.id}`);
|
||||
|
||||
|
||||
// Store the canvas widget on the node
|
||||
(this as any).canvasWidget = canvasWidget;
|
||||
|
||||
// Check if this node has a pending copy source (from onConfigure)
|
||||
// Check both the current ID and -1 (temporary ID during paste)
|
||||
let sourceNodeId = pendingCopySources.get(this.id);
|
||||
if (!sourceNodeId) {
|
||||
sourceNodeId = pendingCopySources.get(-1);
|
||||
if (sourceNodeId) {
|
||||
// Transfer from -1 to the real ID and clear -1
|
||||
pendingCopySources.delete(-1);
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceNodeId && sourceNodeId !== this.id) {
|
||||
log.info(`Node ${this.id} will copy canvas state from node ${sourceNodeId}`);
|
||||
|
||||
// Clear the flag
|
||||
pendingCopySources.delete(this.id);
|
||||
|
||||
// Copy the canvas state now that the widget is initialized
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const { getCanvasState, setCanvasState } = await import('./db.js');
|
||||
const sourceState = await getCanvasState(String(sourceNodeId));
|
||||
|
||||
if (!sourceState) {
|
||||
log.debug(`No canvas state found for source node ${sourceNodeId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await setCanvasState(String(this.id), sourceState);
|
||||
await canvasWidget.canvas.loadInitialState();
|
||||
log.info(`Canvas state copied successfully from node ${sourceNodeId} to node ${this.id}`);
|
||||
} catch (error) {
|
||||
log.error(`Error copying canvas state:`, error);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Check if there are already connected inputs
|
||||
setTimeout(() => {
|
||||
if (this.inputs && this.inputs.length > 0) {
|
||||
@@ -1458,6 +1498,36 @@ app.registerExtension({
|
||||
return onRemoved?.apply(this, arguments as any);
|
||||
};
|
||||
|
||||
// Handle copy/paste - save canvas state when copying
|
||||
const originalSerialize = nodeType.prototype.serialize;
|
||||
nodeType.prototype.serialize = function (this: ComfyNode) {
|
||||
const data = originalSerialize ? originalSerialize.apply(this) : {};
|
||||
|
||||
// Store a reference to the source node ID so we can copy layer data
|
||||
data.sourceNodeId = this.id;
|
||||
log.debug(`Serializing node ${this.id} for copy`);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// Handle copy/paste - load canvas state from source node when pasting
|
||||
const originalConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = async function (this: ComfyNode, data: any) {
|
||||
if (originalConfigure) {
|
||||
originalConfigure.apply(this, [data]);
|
||||
}
|
||||
|
||||
// Store the source node ID in the map (persists across node ID changes)
|
||||
// This will be picked up later in onAdded when the canvas widget is ready
|
||||
if (data.sourceNodeId && data.sourceNodeId !== this.id) {
|
||||
const existingSource = pendingCopySources.get(this.id);
|
||||
if (!existingSource) {
|
||||
pendingCopySources.set(this.id, data.sourceNodeId);
|
||||
log.debug(`Stored pending copy source: ${data.sourceNodeId} for node ${this.id}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (this: ComfyNode, _: any, options: any[]) {
|
||||
// FIRST: Call original to let other extensions add their options
|
||||
|
||||
Reference in New Issue
Block a user