mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Refactored batch preview management to allow multiple BatchPreviewManager instances per canvas. Updated positioning logic to use an initial spawn position, adjusted UI updates, and ensured batch preview menus move correctly with canvas panning. Removed single-instance references and updated related event handling.
255 lines
8.9 KiB
JavaScript
255 lines
8.9 KiB
JavaScript
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
|
|
|
const log = createModuleLogger('BatchPreviewManager');
|
|
|
|
export class BatchPreviewManager {
|
|
constructor(canvas, initialPosition = { x: 0, y: 0 }) {
|
|
this.canvas = canvas;
|
|
this.active = false;
|
|
this.layers = [];
|
|
this.currentIndex = 0;
|
|
this.element = null;
|
|
this.uiInitialized = false;
|
|
this.maskWasVisible = false;
|
|
|
|
// Position in canvas world coordinates
|
|
this.worldX = initialPosition.x;
|
|
this.worldY = initialPosition.y;
|
|
this.isDragging = false;
|
|
}
|
|
|
|
updateScreenPosition(viewport) {
|
|
if (!this.active || !this.element) return;
|
|
|
|
// Translate world coordinates to screen coordinates
|
|
const screenX = (this.worldX - viewport.x) * viewport.zoom;
|
|
const screenY = (this.worldY - viewport.y) * viewport.zoom;
|
|
|
|
// We can also scale the menu with zoom, but let's keep it constant for now for readability
|
|
const scale = 1; // viewport.zoom;
|
|
|
|
// Use transform for performance
|
|
this.element.style.transform = `translate(${screenX}px, ${screenY}px) scale(${scale})`;
|
|
}
|
|
|
|
_createUI() {
|
|
if (this.uiInitialized) return;
|
|
|
|
this.element = document.createElement('div');
|
|
this.element.id = 'layerforge-batch-preview';
|
|
this.element.style.cssText = `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
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;
|
|
cursor: move;
|
|
user-select: none;
|
|
`;
|
|
|
|
this.element.addEventListener('mousedown', (e) => {
|
|
if (e.target.tagName === 'BUTTON') return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
this.isDragging = true;
|
|
|
|
const handleMouseMove = (moveEvent) => {
|
|
if (this.isDragging) {
|
|
// Convert screen pixel movement to world coordinate movement
|
|
const deltaX = moveEvent.movementX / this.canvas.viewport.zoom;
|
|
const deltaY = moveEvent.movementY / this.canvas.viewport.zoom;
|
|
|
|
this.worldX += deltaX;
|
|
this.worldY += deltaY;
|
|
|
|
// The render loop will handle updating the screen position, but we need to trigger it.
|
|
this.canvas.render();
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
this.isDragging = false;
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
});
|
|
|
|
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();
|
|
|
|
// Auto-hide mask logic
|
|
this.maskWasVisible = this.canvas.maskTool.isOverlayVisible;
|
|
if (this.maskWasVisible) {
|
|
this.canvas.maskTool.toggleOverlayVisibility();
|
|
const toggleBtn = document.getElementById(`toggle-mask-btn-${this.canvas.node.id}`);
|
|
if (toggleBtn) {
|
|
toggleBtn.classList.remove('primary');
|
|
toggleBtn.textContent = "Hide Mask";
|
|
}
|
|
this.canvas.render();
|
|
}
|
|
|
|
log.info(`Showing batch preview for ${layers.length} layers.`);
|
|
this.layers = layers;
|
|
this.currentIndex = 0;
|
|
|
|
// Make the element visible BEFORE calculating its size
|
|
this.element.style.display = 'flex';
|
|
this.active = true;
|
|
|
|
// Now that it's visible, we can get its dimensions and adjust the position.
|
|
const menuWidthInWorld = this.element.offsetWidth / this.canvas.viewport.zoom;
|
|
const paddingInWorld = 20 / this.canvas.viewport.zoom;
|
|
|
|
this.worldX -= menuWidthInWorld / 2; // Center horizontally
|
|
this.worldY += paddingInWorld; // Add padding below the output area
|
|
|
|
this._update();
|
|
}
|
|
|
|
hide() {
|
|
log.info('Hiding batch preview.');
|
|
if (this.element) {
|
|
this.element.remove();
|
|
}
|
|
this.active = false;
|
|
|
|
const index = this.canvas.batchPreviewManagers.indexOf(this);
|
|
if (index > -1) {
|
|
this.canvas.batchPreviewManagers.splice(index, 1);
|
|
}
|
|
|
|
// Restore mask visibility if it was hidden by this manager
|
|
if (this.maskWasVisible && !this.canvas.maskTool.isOverlayVisible) {
|
|
this.canvas.maskTool.toggleOverlayVisibility();
|
|
const toggleBtn = document.getElementById(`toggle-mask-btn-${this.canvas.node.id}`);
|
|
if (toggleBtn) {
|
|
toggleBtn.classList.add('primary');
|
|
toggleBtn.textContent = "Show Mask";
|
|
}
|
|
}
|
|
this.maskWasVisible = false; // Reset state
|
|
|
|
// 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();
|
|
}
|
|
}
|