mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 14:25:44 -03:00
Add batch preview manager and mask overlay toggle
Introduces BatchPreviewManager for reviewing and confirming multiple imported layers after auto-refresh. Adds a toggle button for mask overlay visibility in the UI and updates mask rendering logic to respect overlay visibility. Also refactors image import to return new layers and adds a utility for removing layers by ID.
This commit is contained in:
162
js/BatchPreviewManager.js
Normal file
162
js/BatchPreviewManager.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||||
|
|
||||||
|
const log = createModuleLogger('BatchPreviewManager');
|
||||||
|
|
||||||
|
export class BatchPreviewManager {
|
||||||
|
constructor(canvas) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.active = false;
|
||||||
|
this.layers = [];
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.element = null;
|
||||||
|
this.uiInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createUI() {
|
||||||
|
if (this.uiInitialized) return;
|
||||||
|
|
||||||
|
this.element = document.createElement('div');
|
||||||
|
this.element.id = 'layerforge-batch-preview';
|
||||||
|
this.element.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
z-index: 1001;
|
||||||
|
border: 1px solid #555;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const prevButton = this._createButton('◀', 'Previous'); // Left arrow
|
||||||
|
const nextButton = this._createButton('▶', 'Next'); // Right arrow
|
||||||
|
const confirmButton = this._createButton('✔', 'Confirm'); // Checkmark
|
||||||
|
const cancelButton = this._createButton('✖', 'Cancel All'); // X mark
|
||||||
|
const closeButton = this._createButton('➲', 'Close'); // Door icon
|
||||||
|
|
||||||
|
this.counterElement = document.createElement('span');
|
||||||
|
this.counterElement.style.minWidth = '40px';
|
||||||
|
this.counterElement.style.textAlign = 'center';
|
||||||
|
this.counterElement.style.fontWeight = 'bold';
|
||||||
|
|
||||||
|
prevButton.onclick = () => this.navigate(-1);
|
||||||
|
nextButton.onclick = () => this.navigate(1);
|
||||||
|
confirmButton.onclick = () => this.confirm();
|
||||||
|
cancelButton.onclick = () => this.cancelAndRemoveAll();
|
||||||
|
closeButton.onclick = () => this.hide();
|
||||||
|
|
||||||
|
this.element.append(prevButton, this.counterElement, nextButton, confirmButton, cancelButton, closeButton);
|
||||||
|
if (this.canvas.canvas.parentNode) {
|
||||||
|
this.canvas.canvas.parentNode.appendChild(this.element);
|
||||||
|
} else {
|
||||||
|
log.error("Could not find parent node to attach batch preview UI.");
|
||||||
|
}
|
||||||
|
this.uiInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createButton(innerHTML, title) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.innerHTML = innerHTML;
|
||||||
|
button.title = title;
|
||||||
|
button.style.cssText = `
|
||||||
|
background: #555;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #777;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
button.onmouseover = () => button.style.background = '#666';
|
||||||
|
button.onmouseout = () => button.style.background = '#555';
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(layers) {
|
||||||
|
if (!layers || layers.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._createUI();
|
||||||
|
|
||||||
|
log.info(`Showing batch preview for ${layers.length} layers.`);
|
||||||
|
this.layers = layers;
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.element.style.display = 'flex';
|
||||||
|
this.active = true;
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
log.info('Hiding batch preview.');
|
||||||
|
this.element.style.display = 'none';
|
||||||
|
this.active = false;
|
||||||
|
this.layers = [];
|
||||||
|
this.currentIndex = 0;
|
||||||
|
// Make all layers visible again upon closing
|
||||||
|
this.canvas.layers.forEach(l => l.visible = true);
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(direction) {
|
||||||
|
this.currentIndex += direction;
|
||||||
|
if (this.currentIndex < 0) {
|
||||||
|
this.currentIndex = this.layers.length - 1;
|
||||||
|
} else if (this.currentIndex >= this.layers.length) {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
const layerToKeep = this.layers[this.currentIndex];
|
||||||
|
log.info(`Confirming selection: Keeping layer ${layerToKeep.id}.`);
|
||||||
|
|
||||||
|
const layersToDelete = this.layers.filter(l => l.id !== layerToKeep.id);
|
||||||
|
const layerIdsToDelete = layersToDelete.map(l => l.id);
|
||||||
|
|
||||||
|
this.canvas.removeLayersByIds(layerIdsToDelete);
|
||||||
|
log.info(`Deleted ${layersToDelete.length} other layers.`);
|
||||||
|
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAndRemoveAll() {
|
||||||
|
log.info('Cancel clicked. Removing all new layers.');
|
||||||
|
|
||||||
|
const layerIdsToDelete = this.layers.map(l => l.id);
|
||||||
|
this.canvas.removeLayersByIds(layerIdsToDelete);
|
||||||
|
log.info(`Deleted all ${layerIdsToDelete.length} new layers.`);
|
||||||
|
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
_update() {
|
||||||
|
this.counterElement.textContent = `${this.currentIndex + 1} / ${this.layers.length}`;
|
||||||
|
this._focusOnLayer(this.layers[this.currentIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusOnLayer(layer) {
|
||||||
|
if (!layer) return;
|
||||||
|
log.debug(`Focusing on layer ${layer.id}`);
|
||||||
|
|
||||||
|
// Move the selected layer to the top of the layer stack
|
||||||
|
this.canvas.canvasLayers.moveLayers([layer], { toIndex: 0 });
|
||||||
|
|
||||||
|
this.canvas.updateSelection([layer]);
|
||||||
|
|
||||||
|
// Render is called by moveLayers, but we call it again to be safe
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
js/Canvas.js
30
js/Canvas.js
@@ -9,6 +9,7 @@ import {CanvasLayersPanel} from "./CanvasLayersPanel.js";
|
|||||||
import {CanvasRenderer} from "./CanvasRenderer.js";
|
import {CanvasRenderer} from "./CanvasRenderer.js";
|
||||||
import {CanvasIO} from "./CanvasIO.js";
|
import {CanvasIO} from "./CanvasIO.js";
|
||||||
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
||||||
|
import {BatchPreviewManager} from "./BatchPreviewManager.js";
|
||||||
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||||
import {mask_editor_showing, mask_editor_listen_for_cancel} from "./utils/mask_utils.js";
|
import {mask_editor_showing, mask_editor_listen_for_cancel} from "./utils/mask_utils.js";
|
||||||
import { debounce } from "./utils/CommonUtils.js";
|
import { debounce } from "./utils/CommonUtils.js";
|
||||||
@@ -166,6 +167,7 @@ export class Canvas {
|
|||||||
this.canvasRenderer = new CanvasRenderer(this);
|
this.canvasRenderer = new CanvasRenderer(this);
|
||||||
this.canvasIO = new CanvasIO(this);
|
this.canvasIO = new CanvasIO(this);
|
||||||
this.imageReferenceManager = new ImageReferenceManager(this);
|
this.imageReferenceManager = new ImageReferenceManager(this);
|
||||||
|
this.batchPreviewManager = new BatchPreviewManager(this);
|
||||||
|
|
||||||
log.debug('Canvas modules initialized successfully');
|
log.debug('Canvas modules initialized successfully');
|
||||||
}
|
}
|
||||||
@@ -286,6 +288,26 @@ export class Canvas {
|
|||||||
/**
|
/**
|
||||||
* Usuwa wybrane warstwy
|
* Usuwa wybrane warstwy
|
||||||
*/
|
*/
|
||||||
|
removeLayersByIds(layerIds) {
|
||||||
|
if (!layerIds || layerIds.length === 0) return;
|
||||||
|
|
||||||
|
const initialCount = this.layers.length;
|
||||||
|
this.saveState();
|
||||||
|
this.layers = this.layers.filter(l => !layerIds.includes(l.id));
|
||||||
|
|
||||||
|
// If the current selection was part of the removal, clear it
|
||||||
|
const newSelection = this.selectedLayers.filter(l => !layerIds.includes(l.id));
|
||||||
|
this.updateSelection(newSelection);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.saveState();
|
||||||
|
|
||||||
|
if (this.canvasLayersPanel) {
|
||||||
|
this.canvasLayersPanel.onLayersChanged();
|
||||||
|
}
|
||||||
|
log.info(`Removed ${initialCount - this.layers.length} layers by ID.`);
|
||||||
|
}
|
||||||
|
|
||||||
removeSelectedLayers() {
|
removeSelectedLayers() {
|
||||||
if (this.selectedLayers.length > 0) {
|
if (this.selectedLayers.length > 0) {
|
||||||
log.info('Removing selected layers', {
|
log.info('Removing selected layers', {
|
||||||
@@ -466,10 +488,14 @@ export class Canvas {
|
|||||||
log.debug(`Execution started, timestamp set to: ${lastExecutionStartTime}`);
|
log.debug(`Execution started, timestamp set to: ${lastExecutionStartTime}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExecutionSuccess = () => {
|
const handleExecutionSuccess = async () => {
|
||||||
if (autoRefreshEnabled) {
|
if (autoRefreshEnabled) {
|
||||||
log.info('Auto-refresh triggered, importing latest images.');
|
log.info('Auto-refresh triggered, importing latest images.');
|
||||||
this.canvasIO.importLatestImages(lastExecutionStartTime);
|
const newLayers = await this.canvasIO.importLatestImages(lastExecutionStartTime);
|
||||||
|
|
||||||
|
if (newLayers && newLayers.length > 1) {
|
||||||
|
this.batchPreviewManager.show(newLayers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -765,6 +765,7 @@ export class CanvasIO {
|
|||||||
|
|
||||||
if (result.success && result.images && result.images.length > 0) {
|
if (result.success && result.images && result.images.length > 0) {
|
||||||
log.info(`Received ${result.images.length} new images, adding to canvas.`);
|
log.info(`Received ${result.images.length} new images, adding to canvas.`);
|
||||||
|
const newLayers = [];
|
||||||
|
|
||||||
for (const imageData of result.images) {
|
for (const imageData of result.images) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -773,14 +774,15 @@ export class CanvasIO {
|
|||||||
img.onerror = reject;
|
img.onerror = reject;
|
||||||
img.src = imageData;
|
img.src = imageData;
|
||||||
});
|
});
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit');
|
const newLayer = await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit');
|
||||||
|
newLayers.push(newLayer);
|
||||||
}
|
}
|
||||||
log.info("All new images imported and placed on canvas successfully.");
|
log.info("All new images imported and placed on canvas successfully.");
|
||||||
return true;
|
return newLayers;
|
||||||
|
|
||||||
} else if (result.success) {
|
} else if (result.success) {
|
||||||
log.info("No new images found since last generation.");
|
log.info("No new images found since last generation.");
|
||||||
return true;
|
return [];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(result.error || "Failed to fetch latest images.");
|
throw new Error(result.error || "Failed to fetch latest images.");
|
||||||
@@ -788,7 +790,7 @@ export class CanvasIO {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Error importing latest images:", error);
|
log.error("Error importing latest images:", error);
|
||||||
alert(`Failed to import latest images: ${error.message}`);
|
alert(`Failed to import latest images: ${error.message}`);
|
||||||
return false;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const layer = {
|
const layer = {
|
||||||
|
id: generateUUID(),
|
||||||
image: image,
|
image: image,
|
||||||
imageId: imageId,
|
imageId: imageId,
|
||||||
x: finalX,
|
x: finalX,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class CanvasRenderer {
|
|||||||
|
|
||||||
this.drawCanvasOutline(ctx);
|
this.drawCanvasOutline(ctx);
|
||||||
const maskImage = this.canvas.maskTool.getMask();
|
const maskImage = this.canvas.maskTool.getMask();
|
||||||
if (maskImage) {
|
if (maskImage && this.canvas.maskTool.isOverlayVisible) {
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
|
|||||||
@@ -870,6 +870,24 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
]),
|
]),
|
||||||
$el("div.painter-separator"),
|
$el("div.painter-separator"),
|
||||||
$el("div.painter-button-group", {id: "mask-controls"}, [
|
$el("div.painter-button-group", {id: "mask-controls"}, [
|
||||||
|
$el("button.painter-button.primary", {
|
||||||
|
id: "toggle-mask-btn",
|
||||||
|
textContent: "Show Mask",
|
||||||
|
title: "Toggle mask overlay visibility",
|
||||||
|
onclick: (e) => {
|
||||||
|
const button = e.target;
|
||||||
|
canvas.maskTool.toggleOverlayVisibility();
|
||||||
|
canvas.render();
|
||||||
|
|
||||||
|
if (canvas.maskTool.isOverlayVisible) {
|
||||||
|
button.classList.add('primary');
|
||||||
|
button.textContent = "Show Mask";
|
||||||
|
} else {
|
||||||
|
button.classList.remove('primary');
|
||||||
|
button.textContent = "Hide Mask";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button", {
|
||||||
textContent: "Edit Mask",
|
textContent: "Edit Mask",
|
||||||
title: "Open the current canvas view in the mask editor",
|
title: "Open the current canvas view in the mask editor",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export class MaskTool {
|
|||||||
this.x = 0;
|
this.x = 0;
|
||||||
this.y = 0;
|
this.y = 0;
|
||||||
|
|
||||||
|
this.isOverlayVisible = true;
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.brushSize = 20;
|
this.brushSize = 20;
|
||||||
this.brushStrength = 0.5;
|
this.brushStrength = 0.5;
|
||||||
@@ -280,6 +281,11 @@ export class MaskTool {
|
|||||||
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleOverlayVisibility() {
|
||||||
|
this.isOverlayVisible = !this.isOverlayVisible;
|
||||||
|
log.info(`Mask overlay visibility toggled to: ${this.isOverlayVisible}`);
|
||||||
|
}
|
||||||
|
|
||||||
setMask(image) {
|
setMask(image) {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user