diff --git a/js/BatchPreviewManager.js b/js/BatchPreviewManager.js new file mode 100644 index 0000000..a2a4b5a --- /dev/null +++ b/js/BatchPreviewManager.js @@ -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(); + } +} diff --git a/js/Canvas.js b/js/Canvas.js index 5dc06a5..20bd719 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -9,6 +9,7 @@ import {CanvasLayersPanel} from "./CanvasLayersPanel.js"; import {CanvasRenderer} from "./CanvasRenderer.js"; import {CanvasIO} from "./CanvasIO.js"; import {ImageReferenceManager} from "./ImageReferenceManager.js"; +import {BatchPreviewManager} from "./BatchPreviewManager.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; import {mask_editor_showing, mask_editor_listen_for_cancel} from "./utils/mask_utils.js"; import { debounce } from "./utils/CommonUtils.js"; @@ -166,6 +167,7 @@ export class Canvas { this.canvasRenderer = new CanvasRenderer(this); this.canvasIO = new CanvasIO(this); this.imageReferenceManager = new ImageReferenceManager(this); + this.batchPreviewManager = new BatchPreviewManager(this); log.debug('Canvas modules initialized successfully'); } @@ -286,6 +288,26 @@ export class Canvas { /** * 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() { if (this.selectedLayers.length > 0) { log.info('Removing selected layers', { @@ -466,10 +488,14 @@ export class Canvas { log.debug(`Execution started, timestamp set to: ${lastExecutionStartTime}`); }; - const handleExecutionSuccess = () => { + const handleExecutionSuccess = async () => { if (autoRefreshEnabled) { 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); + } } }; diff --git a/js/CanvasIO.js b/js/CanvasIO.js index fd5e985..973adea 100644 --- a/js/CanvasIO.js +++ b/js/CanvasIO.js @@ -765,6 +765,7 @@ export class CanvasIO { if (result.success && result.images && result.images.length > 0) { log.info(`Received ${result.images.length} new images, adding to canvas.`); + const newLayers = []; for (const imageData of result.images) { const img = new Image(); @@ -773,14 +774,15 @@ export class CanvasIO { img.onerror = reject; 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."); - return true; + return newLayers; } else if (result.success) { log.info("No new images found since last generation."); - return true; + return []; } else { throw new Error(result.error || "Failed to fetch latest images."); @@ -788,7 +790,7 @@ export class CanvasIO { } catch (error) { log.error("Error importing latest images:", error); alert(`Failed to import latest images: ${error.message}`); - return false; + return []; } } } diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index a82433e..001f96d 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -178,6 +178,7 @@ export class CanvasLayers { } const layer = { + id: generateUUID(), image: image, imageId: imageId, x: finalX, diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index faab8dc..e128158 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -83,7 +83,7 @@ export class CanvasRenderer { this.drawCanvasOutline(ctx); const maskImage = this.canvas.maskTool.getMask(); - if (maskImage) { + if (maskImage && this.canvas.maskTool.isOverlayVisible) { ctx.save(); diff --git a/js/CanvasView.js b/js/CanvasView.js index fdddcb4..04609a2 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -870,6 +870,24 @@ async function createCanvasWidget(node, widget, app) { ]), $el("div.painter-separator"), $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", { textContent: "Edit Mask", title: "Open the current canvas view in the mask editor", diff --git a/js/MaskTool.js b/js/MaskTool.js index 4ca82a8..2343199 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -13,6 +13,7 @@ export class MaskTool { this.x = 0; this.y = 0; + this.isOverlayVisible = true; this.isActive = false; this.brushSize = 20; this.brushStrength = 0.5; @@ -280,6 +281,11 @@ export class MaskTool { 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) {