mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 12:52:10 -03:00
feat: add auto adjust output area for selected layers
Implements one-click auto adjustment of output area to fit selected layers with intelligent bounding box calculation. Supports rotation, crop mode, flips, and includes automatic padding with complete canvas state updates.
This commit is contained in:
@@ -196,6 +196,117 @@ export class CanvasLayers {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Automatically adjust output area to fit selected layers
|
||||
* Calculates precise bounding box for all selected layers including rotation and crop mode support
|
||||
*/
|
||||
autoAdjustOutputToSelection() {
|
||||
const selectedLayers = this.canvas.canvasSelection.selectedLayers;
|
||||
if (selectedLayers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// Calculate bounding box of selected layers
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
selectedLayers.forEach((layer) => {
|
||||
// For crop mode layers, use the visible crop bounds
|
||||
if (layer.cropMode && layer.cropBounds && layer.originalWidth && layer.originalHeight) {
|
||||
const layerScaleX = layer.width / layer.originalWidth;
|
||||
const layerScaleY = layer.height / layer.originalHeight;
|
||||
const cropWidth = layer.cropBounds.width * layerScaleX;
|
||||
const cropHeight = layer.cropBounds.height * layerScaleY;
|
||||
const effectiveCropX = layer.flipH
|
||||
? layer.originalWidth - (layer.cropBounds.x + layer.cropBounds.width)
|
||||
: layer.cropBounds.x;
|
||||
const effectiveCropY = layer.flipV
|
||||
? layer.originalHeight - (layer.cropBounds.y + layer.cropBounds.height)
|
||||
: layer.cropBounds.y;
|
||||
const cropOffsetX = effectiveCropX * layerScaleX;
|
||||
const cropOffsetY = effectiveCropY * layerScaleY;
|
||||
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);
|
||||
// Calculate corners of the crop rectangle
|
||||
const corners = [
|
||||
{ x: cropOffsetX, y: cropOffsetY },
|
||||
{ x: cropOffsetX + cropWidth, y: cropOffsetY },
|
||||
{ x: cropOffsetX + cropWidth, y: cropOffsetY + cropHeight },
|
||||
{ x: cropOffsetX, y: cropOffsetY + cropHeight }
|
||||
];
|
||||
corners.forEach(p => {
|
||||
// Transform to layer space (centered)
|
||||
const localX = p.x - layer.width / 2;
|
||||
const localY = p.y - layer.height / 2;
|
||||
// Apply rotation
|
||||
const worldX = centerX + (localX * cos - localY * sin);
|
||||
const worldY = centerY + (localX * sin + localY * cos);
|
||||
minX = Math.min(minX, worldX);
|
||||
minY = Math.min(minY, worldY);
|
||||
maxX = Math.max(maxX, worldX);
|
||||
maxY = Math.max(maxY, worldY);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// For normal layers, use the full layer bounds
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
// Calculate new dimensions without padding for precise fit
|
||||
const newWidth = Math.ceil(maxX - minX);
|
||||
const newHeight = Math.ceil(maxY - minY);
|
||||
if (newWidth <= 0 || newHeight <= 0) {
|
||||
log.error("Cannot calculate valid output area dimensions");
|
||||
return false;
|
||||
}
|
||||
// Update output area bounds
|
||||
this.canvas.outputAreaBounds = {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: newWidth,
|
||||
height: newHeight
|
||||
};
|
||||
// Update canvas dimensions
|
||||
this.canvas.width = newWidth;
|
||||
this.canvas.height = newHeight;
|
||||
this.canvas.maskTool.resize(newWidth, newHeight);
|
||||
this.canvas.canvas.width = newWidth;
|
||||
this.canvas.canvas.height = newHeight;
|
||||
// Reset extensions
|
||||
this.canvas.outputAreaExtensions = { top: 0, bottom: 0, left: 0, right: 0 };
|
||||
this.canvas.outputAreaExtensionEnabled = false;
|
||||
this.canvas.lastOutputAreaExtensions = { top: 0, bottom: 0, left: 0, right: 0 };
|
||||
// Update original canvas size and position
|
||||
this.canvas.originalCanvasSize = { width: newWidth, height: newHeight };
|
||||
this.canvas.originalOutputAreaPosition = { x: minX, y: minY };
|
||||
// Save state and render
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
log.info(`Auto-adjusted output area to fit ${selectedLayers.length} selected layer(s)`, {
|
||||
bounds: { x: minX, y: minY, width: newWidth, height: newHeight }
|
||||
});
|
||||
return true;
|
||||
}
|
||||
pasteLayers() {
|
||||
if (this.internalClipboard.length === 0)
|
||||
return;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { clearAllCanvasStates } from "./db.js";
|
||||
import { ImageCache } from "./ImageCache.js";
|
||||
import { createCanvas } from "./utils/CommonUtils.js";
|
||||
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||
import { showErrorNotification, showSuccessNotification, showInfoNotification } from "./utils/NotificationUtils.js";
|
||||
import { showErrorNotification, showSuccessNotification, showInfoNotification, showWarningNotification } from "./utils/NotificationUtils.js";
|
||||
import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
|
||||
import { setupSAMDetectorHook } from "./SAMDetectorIntegration.js";
|
||||
const log = createModuleLogger('Canvas_view');
|
||||
@@ -213,6 +213,25 @@ async function createCanvasWidget(node, widget, app) {
|
||||
]),
|
||||
$el("div.painter-separator"),
|
||||
$el("div.painter-button-group", {}, [
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Auto Adjust Output",
|
||||
title: "Automatically adjust output area to fit selected layers",
|
||||
onclick: () => {
|
||||
const selectedLayers = canvas.canvasSelection.selectedLayers;
|
||||
if (selectedLayers.length === 0) {
|
||||
showWarningNotification("Please select one or more layers first");
|
||||
return;
|
||||
}
|
||||
const success = canvas.canvasLayers.autoAdjustOutputToSelection();
|
||||
if (success) {
|
||||
const bounds = canvas.outputAreaBounds;
|
||||
showSuccessNotification(`Output area adjusted to ${bounds.width}x${bounds.height}px`);
|
||||
}
|
||||
else {
|
||||
showErrorNotification("Cannot calculate valid output area dimensions");
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el("button.painter-button", {
|
||||
textContent: "Output Area Size",
|
||||
title: "Set the size of the output area",
|
||||
|
||||
@@ -135,6 +135,142 @@ export class CanvasLayers {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically adjust output area to fit selected layers
|
||||
* Calculates precise bounding box for all selected layers including rotation and crop mode support
|
||||
*/
|
||||
autoAdjustOutputToSelection(): boolean {
|
||||
const selectedLayers = this.canvas.canvasSelection.selectedLayers;
|
||||
if (selectedLayers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate bounding box of selected layers
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
selectedLayers.forEach((layer: Layer) => {
|
||||
// For crop mode layers, use the visible crop bounds
|
||||
if (layer.cropMode && layer.cropBounds && layer.originalWidth && layer.originalHeight) {
|
||||
const layerScaleX = layer.width / layer.originalWidth;
|
||||
const layerScaleY = layer.height / layer.originalHeight;
|
||||
|
||||
const cropWidth = layer.cropBounds.width * layerScaleX;
|
||||
const cropHeight = layer.cropBounds.height * layerScaleY;
|
||||
|
||||
const effectiveCropX = layer.flipH
|
||||
? layer.originalWidth - (layer.cropBounds.x + layer.cropBounds.width)
|
||||
: layer.cropBounds.x;
|
||||
const effectiveCropY = layer.flipV
|
||||
? layer.originalHeight - (layer.cropBounds.y + layer.cropBounds.height)
|
||||
: layer.cropBounds.y;
|
||||
|
||||
const cropOffsetX = effectiveCropX * layerScaleX;
|
||||
const cropOffsetY = effectiveCropY * layerScaleY;
|
||||
|
||||
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);
|
||||
|
||||
// Calculate corners of the crop rectangle
|
||||
const corners = [
|
||||
{ x: cropOffsetX, y: cropOffsetY },
|
||||
{ x: cropOffsetX + cropWidth, y: cropOffsetY },
|
||||
{ x: cropOffsetX + cropWidth, y: cropOffsetY + cropHeight },
|
||||
{ x: cropOffsetX, y: cropOffsetY + cropHeight }
|
||||
];
|
||||
|
||||
corners.forEach(p => {
|
||||
// Transform to layer space (centered)
|
||||
const localX = p.x - layer.width / 2;
|
||||
const localY = p.y - layer.height / 2;
|
||||
|
||||
// Apply rotation
|
||||
const worldX = centerX + (localX * cos - localY * sin);
|
||||
const worldY = centerY + (localX * sin + localY * cos);
|
||||
|
||||
minX = Math.min(minX, worldX);
|
||||
minY = Math.min(minY, worldY);
|
||||
maxX = Math.max(maxX, worldX);
|
||||
maxY = Math.max(maxY, worldY);
|
||||
});
|
||||
} else {
|
||||
// For normal layers, use the full layer bounds
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate new dimensions without padding for precise fit
|
||||
const newWidth = Math.ceil(maxX - minX);
|
||||
const newHeight = Math.ceil(maxY - minY);
|
||||
|
||||
if (newWidth <= 0 || newHeight <= 0) {
|
||||
log.error("Cannot calculate valid output area dimensions");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update output area bounds
|
||||
this.canvas.outputAreaBounds = {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: newWidth,
|
||||
height: newHeight
|
||||
};
|
||||
|
||||
// Update canvas dimensions
|
||||
this.canvas.width = newWidth;
|
||||
this.canvas.height = newHeight;
|
||||
this.canvas.maskTool.resize(newWidth, newHeight);
|
||||
this.canvas.canvas.width = newWidth;
|
||||
this.canvas.canvas.height = newHeight;
|
||||
|
||||
// Reset extensions
|
||||
this.canvas.outputAreaExtensions = { top: 0, bottom: 0, left: 0, right: 0 };
|
||||
this.canvas.outputAreaExtensionEnabled = false;
|
||||
this.canvas.lastOutputAreaExtensions = { top: 0, bottom: 0, left: 0, right: 0 };
|
||||
|
||||
// Update original canvas size and position
|
||||
this.canvas.originalCanvasSize = { width: newWidth, height: newHeight };
|
||||
this.canvas.originalOutputAreaPosition = { x: minX, y: minY };
|
||||
|
||||
// Save state and render
|
||||
this.canvas.render();
|
||||
this.canvas.saveState();
|
||||
|
||||
log.info(`Auto-adjusted output area to fit ${selectedLayers.length} selected layer(s)`, {
|
||||
bounds: { x: minX, y: minY, width: newWidth, height: newHeight }
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pasteLayers(): void {
|
||||
if (this.internalClipboard.length === 0) return;
|
||||
this.canvas.saveState();
|
||||
|
||||
@@ -268,6 +268,25 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
||||
|
||||
$el("div.painter-separator"),
|
||||
$el("div.painter-button-group", {}, [
|
||||
$el("button.painter-button.requires-selection", {
|
||||
textContent: "Auto Adjust Output",
|
||||
title: "Automatically adjust output area to fit selected layers",
|
||||
onclick: () => {
|
||||
const selectedLayers = canvas.canvasSelection.selectedLayers;
|
||||
if (selectedLayers.length === 0) {
|
||||
showWarningNotification("Please select one or more layers first");
|
||||
return;
|
||||
}
|
||||
|
||||
const success = canvas.canvasLayers.autoAdjustOutputToSelection();
|
||||
if (success) {
|
||||
const bounds = canvas.outputAreaBounds;
|
||||
showSuccessNotification(`Output area adjusted to ${bounds.width}x${bounds.height}px`);
|
||||
} else {
|
||||
showErrorNotification("Cannot calculate valid output area dimensions");
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el("button.painter-button", {
|
||||
textContent: "Output Area Size",
|
||||
title: "Set the size of the output area",
|
||||
|
||||
Reference in New Issue
Block a user