mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Add layer fusion (flatten/merge) feature
Introduces a new 'Fuse' button to the canvas UI, allowing users to flatten and merge multiple selected layers into a single layer. The implementation handles bounding box calculation, z-index ordering, and updates the canvas state and selection accordingly. The fuse button is enabled only when at least two layers are selected.
This commit is contained in:
@@ -968,4 +968,152 @@ export class CanvasLayers {
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuses (flattens and merges) selected layers into a single layer
|
||||
*/
|
||||
async fuseLayers() {
|
||||
if (this.canvas.selectedLayers.length < 2) {
|
||||
alert("Please select at least 2 layers to fuse.");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Fusing ${this.canvas.selectedLayers.length} selected layers`);
|
||||
|
||||
try {
|
||||
// Save state for undo
|
||||
this.canvas.saveState();
|
||||
|
||||
// Calculate bounding box of all selected layers
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
this.canvas.selectedLayers.forEach(layer => {
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
const rad = layer.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(rad);
|
||||
const sin = Math.sin(rad);
|
||||
|
||||
const halfW = layer.width / 2;
|
||||
const halfH = layer.height / 2;
|
||||
|
||||
const corners = [
|
||||
{x: -halfW, y: -halfH},
|
||||
{x: halfW, y: -halfH},
|
||||
{x: halfW, y: halfH},
|
||||
{x: -halfW, y: halfH}
|
||||
];
|
||||
|
||||
corners.forEach(p => {
|
||||
const worldX = centerX + (p.x * cos - p.y * sin);
|
||||
const worldY = centerY + (p.x * sin + p.y * cos);
|
||||
|
||||
minX = Math.min(minX, worldX);
|
||||
minY = Math.min(minY, worldY);
|
||||
maxX = Math.max(maxX, worldX);
|
||||
maxY = Math.max(maxY, worldY);
|
||||
});
|
||||
});
|
||||
|
||||
const fusedWidth = Math.ceil(maxX - minX);
|
||||
const fusedHeight = Math.ceil(maxY - minY);
|
||||
|
||||
if (fusedWidth <= 0 || fusedHeight <= 0) {
|
||||
log.warn("Calculated fused layer dimensions are invalid");
|
||||
alert("Cannot fuse layers: invalid dimensions calculated.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temporary canvas for flattening
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = fusedWidth;
|
||||
tempCanvas.height = fusedHeight;
|
||||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
// Translate context to account for the bounding box offset
|
||||
tempCtx.translate(-minX, -minY);
|
||||
|
||||
// Sort selected layers by z-index and render them
|
||||
const sortedSelection = [...this.canvas.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
|
||||
sortedSelection.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
|
||||
tempCtx.save();
|
||||
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
tempCtx.translate(centerX, centerY);
|
||||
tempCtx.rotate(layer.rotation * Math.PI / 180);
|
||||
tempCtx.drawImage(
|
||||
layer.image,
|
||||
-layer.width / 2, -layer.height / 2,
|
||||
layer.width, layer.height
|
||||
);
|
||||
tempCtx.restore();
|
||||
});
|
||||
|
||||
// Convert flattened canvas to image
|
||||
const fusedImage = new Image();
|
||||
fusedImage.src = tempCanvas.toDataURL();
|
||||
await new Promise((resolve, reject) => {
|
||||
fusedImage.onload = resolve;
|
||||
fusedImage.onerror = reject;
|
||||
});
|
||||
|
||||
// Find the lowest z-index among selected layers to maintain visual order
|
||||
const minZIndex = Math.min(...this.canvas.selectedLayers.map(layer => layer.zIndex));
|
||||
|
||||
// Generate unique ID for the new fused layer
|
||||
const imageId = generateUUID();
|
||||
await saveImage(imageId, fusedImage.src);
|
||||
this.canvas.imageCache.set(imageId, fusedImage.src);
|
||||
|
||||
// Create the new fused layer
|
||||
const fusedLayer = {
|
||||
image: fusedImage,
|
||||
imageId: imageId,
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: fusedWidth,
|
||||
height: fusedHeight,
|
||||
originalWidth: fusedWidth,
|
||||
originalHeight: fusedHeight,
|
||||
rotation: 0,
|
||||
zIndex: minZIndex,
|
||||
blendMode: 'normal',
|
||||
opacity: 1
|
||||
};
|
||||
|
||||
// Remove selected layers from canvas
|
||||
this.canvas.layers = this.canvas.layers.filter(layer => !this.canvas.selectedLayers.includes(layer));
|
||||
|
||||
// Insert the fused layer at the correct position
|
||||
this.canvas.layers.push(fusedLayer);
|
||||
|
||||
// Re-index all layers to maintain proper z-order
|
||||
this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
|
||||
this.canvas.layers.forEach((layer, index) => {
|
||||
layer.zIndex = index;
|
||||
});
|
||||
|
||||
// Select the new fused layer
|
||||
this.canvas.updateSelection([fusedLayer]);
|
||||
|
||||
// Render and save state
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
|
||||
log.info("Layers fused successfully", {
|
||||
originalLayerCount: sortedSelection.length,
|
||||
fusedDimensions: { width: fusedWidth, height: fusedHeight },
|
||||
fusedPosition: { x: minX, y: minY }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
log.error("Error during layer fusion:", error);
|
||||
alert(`Error fusing layers: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +789,11 @@ async function createCanvasWidget(node, widget, app) {
|
||||
title: "Move selected layer(s) down",
|
||||
onclick: () => canvas.canvasLayers.moveLayerDown()
|
||||
}),
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Fuse",
|
||||
title: "Flatten and merge selected layers into a single layer",
|
||||
onclick: () => canvas.canvasLayers.fuseLayers()
|
||||
}),
|
||||
]),
|
||||
|
||||
$el("div.painter-separator"),
|
||||
@@ -1008,7 +1013,12 @@ async function createCanvasWidget(node, widget, app) {
|
||||
const selectionCount = canvas.selectedLayers.length;
|
||||
const hasSelection = selectionCount > 0;
|
||||
controlPanel.querySelectorAll('.requires-selection').forEach(btn => {
|
||||
btn.disabled = !hasSelection;
|
||||
// Special handling for Fuse button - requires at least 2 layers
|
||||
if (btn.textContent === 'Fuse') {
|
||||
btn.disabled = selectionCount < 2;
|
||||
} else {
|
||||
btn.disabled = !hasSelection;
|
||||
}
|
||||
});
|
||||
const mattingBtn = controlPanel.querySelector('.matting-button');
|
||||
if (mattingBtn && !mattingBtn.classList.contains('loading')) {
|
||||
|
||||
Reference in New Issue
Block a user