From d40f68b8c68d362ca62833d9996e7cf49d77de63 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 3 Jul 2025 11:52:16 +0200 Subject: [PATCH] Preserve batch generation area during canvas changes Introduces a 'generationArea' context for batch image generation, ensuring that batch preview outlines and image placement remain accurate when the canvas is moved or resized. Updates related logic in Canvas, CanvasInteractions, CanvasLayers, and CanvasRenderer to track and render the correct area, and synchronizes context updates across user interactions. --- js/BatchPreviewManager.js | 6 +++- js/Canvas.js | 63 ++++++++++++++++++++++++++------------- js/CanvasIO.js | 4 +-- js/CanvasInteractions.js | 52 +++++++++++++++++++++++++++----- js/CanvasLayers.js | 17 ++++++----- js/CanvasRenderer.js | 33 ++++++++++++++++++++ 6 files changed, 137 insertions(+), 38 deletions(-) diff --git a/js/BatchPreviewManager.js b/js/BatchPreviewManager.js index 3cdbf72..2c74389 100644 --- a/js/BatchPreviewManager.js +++ b/js/BatchPreviewManager.js @@ -3,7 +3,7 @@ import {createModuleLogger} from "./utils/LoggerUtils.js"; const log = createModuleLogger('BatchPreviewManager'); export class BatchPreviewManager { - constructor(canvas, initialPosition = { x: 0, y: 0 }) { + constructor(canvas, initialPosition = { x: 0, y: 0 }, generationArea = null) { this.canvas = canvas; this.active = false; this.layers = []; @@ -16,6 +16,7 @@ export class BatchPreviewManager { this.worldX = initialPosition.x; this.worldY = initialPosition.y; this.isDragging = false; + this.generationArea = generationArea; // Store the generation area } updateScreenPosition(viewport) { @@ -185,6 +186,9 @@ export class BatchPreviewManager { this.canvas.batchPreviewManagers.splice(index, 1); } + // Trigger a final render to ensure the generation area outline is removed + this.canvas.render(); + // Restore mask visibility if it was hidden by this manager if (this.maskWasVisible && !this.canvas.maskTool.isOverlayVisible) { this.canvas.maskTool.toggleOverlayVisibility(); diff --git a/js/Canvas.js b/js/Canvas.js index e0c39c1..37bc11a 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -168,7 +168,7 @@ export class Canvas { this.canvasIO = new CanvasIO(this); this.imageReferenceManager = new ImageReferenceManager(this); this.batchPreviewManagers = []; - this.pendingBatchSpawnPosition = null; + this.pendingBatchContext = null; log.debug('Canvas modules initialized successfully'); } @@ -485,36 +485,57 @@ export class Canvas { let lastExecutionStartTime = 0; const handleExecutionStart = () => { - lastExecutionStartTime = Date.now(); - // Store the spawn position for the next batch menu, relative to the output area - this.pendingBatchSpawnPosition = { - x: this.width / 2, // Horizontally centered on the output area - y: this.height // At the bottom of the output area - }; - log.debug(`Execution started, pending spawn position set relative to output area at:`, this.pendingBatchSpawnPosition); + if (autoRefreshEnabled) { + lastExecutionStartTime = Date.now(); + // Store a snapshot of the context for the upcoming batch + this.pendingBatchContext = { + // For the menu position + spawnPosition: { + x: this.width / 2, + y: this.height + }, + // For the image placement + outputArea: { + x: 0, + y: 0, + width: this.width, + height: this.height + } + }; + log.debug(`Execution started, pending batch context captured:`, this.pendingBatchContext); + this.render(); // Trigger render to show the pending outline immediately + } }; const handleExecutionSuccess = async () => { if (autoRefreshEnabled) { log.info('Auto-refresh triggered, importing latest images.'); - const newLayers = await this.canvasIO.importLatestImages(lastExecutionStartTime); + + if (!this.pendingBatchContext) { + log.warn("execution_start did not fire, cannot process batch. Awaiting next execution."); + return; + } + + // Use the captured output area for image import + const newLayers = await this.canvasIO.importLatestImages( + lastExecutionStartTime, + this.pendingBatchContext.outputArea + ); if (newLayers && newLayers.length > 1) { - if (!this.pendingBatchSpawnPosition) { - // Fallback in case execution_start didn't fire - this.pendingBatchSpawnPosition = { - x: this.width / 2, - y: this.height - }; - log.warn("execution_start did not fire, using fallback spawn position."); - } - - const newManager = new BatchPreviewManager(this, this.pendingBatchSpawnPosition); + const newManager = new BatchPreviewManager( + this, + this.pendingBatchContext.spawnPosition, + this.pendingBatchContext.outputArea + ); this.batchPreviewManagers.push(newManager); newManager.show(newLayers); - - this.pendingBatchSpawnPosition = null; // Consume the position } + + // Consume the context + this.pendingBatchContext = null; + // Final render to clear the outline if it was the last one + this.render(); } }; diff --git a/js/CanvasIO.js b/js/CanvasIO.js index 973adea..793a153 100644 --- a/js/CanvasIO.js +++ b/js/CanvasIO.js @@ -757,7 +757,7 @@ export class CanvasIO { } } - async importLatestImages(sinceTimestamp) { + async importLatestImages(sinceTimestamp, targetArea = null) { try { log.info(`Fetching latest images since ${sinceTimestamp}...`); const response = await fetch(`/layerforge/get-latest-images/${sinceTimestamp}`); @@ -774,7 +774,7 @@ export class CanvasIO { img.onerror = reject; img.src = imageData; }); - const newLayer = await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit'); + const newLayer = await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit', targetArea); newLayers.push(newLayer); } log.info("All new images imported and placed on canvas successfully."); diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index cf19f7e..89ca848 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -532,11 +532,26 @@ export class CanvasInteractions { this.canvas.maskTool.updatePosition(-finalX, -finalY); + // If a batch generation is in progress, update the captured context as well + if (this.canvas.pendingBatchContext) { + this.canvas.pendingBatchContext.outputArea.x -= finalX; + this.canvas.pendingBatchContext.outputArea.y -= finalY; + + // Also update the menu spawn position to keep it relative + this.canvas.pendingBatchContext.spawnPosition.x -= finalX; + this.canvas.pendingBatchContext.spawnPosition.y -= finalY; + log.debug("Updated pending batch context during canvas move:", this.canvas.pendingBatchContext); + } + // Also move any active batch preview menus if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) { this.canvas.batchPreviewManagers.forEach(manager => { manager.worldX -= finalX; manager.worldY -= finalY; + if (manager.generationArea) { + manager.generationArea.x -= finalX; + manager.generationArea.y -= finalY; + } }); } @@ -709,20 +724,43 @@ export class CanvasInteractions { if (this.interaction.canvasResizeRect && this.interaction.canvasResizeRect.width > 1 && this.interaction.canvasResizeRect.height > 1) { const newWidth = Math.round(this.interaction.canvasResizeRect.width); const newHeight = Math.round(this.interaction.canvasResizeRect.height); - const rectX = this.interaction.canvasResizeRect.x; - const rectY = this.interaction.canvasResizeRect.y; + const finalX = this.interaction.canvasResizeRect.x; + const finalY = this.interaction.canvasResizeRect.y; this.canvas.updateOutputAreaSize(newWidth, newHeight); this.canvas.layers.forEach(layer => { - layer.x -= rectX; - layer.y -= rectY; + layer.x -= finalX; + layer.y -= finalY; }); - this.canvas.maskTool.updatePosition(-rectX, -rectY); + this.canvas.maskTool.updatePosition(-finalX, -finalY); - this.canvas.viewport.x -= rectX; - this.canvas.viewport.y -= rectY; + // If a batch generation is in progress, update the captured context as well + if (this.canvas.pendingBatchContext) { + this.canvas.pendingBatchContext.outputArea.x -= finalX; + this.canvas.pendingBatchContext.outputArea.y -= finalY; + + // Also update the menu spawn position to keep it relative + this.canvas.pendingBatchContext.spawnPosition.x -= finalX; + this.canvas.pendingBatchContext.spawnPosition.y -= finalY; + log.debug("Updated pending batch context during canvas resize:", this.canvas.pendingBatchContext); + } + + // Also move any active batch preview menus + if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) { + this.canvas.batchPreviewManagers.forEach(manager => { + manager.worldX -= finalX; + manager.worldY -= finalY; + if (manager.generationArea) { + manager.generationArea.x -= finalX; + manager.generationArea.y -= finalY; + } + }); + } + + this.canvas.viewport.x -= finalX; + this.canvas.viewport.y -= finalY; } } diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 001f96d..5cac2db 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -149,12 +149,12 @@ export class CanvasLayers { } - addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default') => { + addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default', targetArea = null) => { if (!image) { throw createValidationError("Image is required for layer creation"); } - log.debug("Adding layer with image:", image, "with mode:", addMode); + log.debug("Adding layer with image:", image, "with mode:", addMode, "targetArea:", targetArea); const imageId = generateUUID(); await saveImage(imageId, image.src); this.canvas.imageCache.set(imageId, image.src); @@ -163,18 +163,21 @@ export class CanvasLayers { let finalHeight = image.height; let finalX, finalY; + // Use the targetArea if provided, otherwise default to the current canvas dimensions + const area = targetArea || { width: this.canvas.width, height: this.canvas.height, x: 0, y: 0 }; + if (addMode === 'fit') { - const scale = Math.min(this.canvas.width / image.width, this.canvas.height / image.height); + const scale = Math.min(area.width / image.width, area.height / image.height); finalWidth = image.width * scale; finalHeight = image.height * scale; - finalX = (this.canvas.width - finalWidth) / 2; - finalY = (this.canvas.height - finalHeight) / 2; + finalX = area.x + (area.width - finalWidth) / 2; + finalY = area.y + (area.height - finalHeight) / 2; } else if (addMode === 'mouse') { finalX = this.canvas.lastMousePosition.x - finalWidth / 2; finalY = this.canvas.lastMousePosition.y - finalHeight / 2; } else { // 'center' or 'default' - finalX = (this.canvas.width - finalWidth) / 2; - finalY = (this.canvas.height - finalHeight) / 2; + finalX = area.x + (area.width - finalWidth) / 2; + finalY = area.y + (area.height - finalHeight) / 2; } const layer = { diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index e60bdcd..d08c2c4 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -82,6 +82,7 @@ export class CanvasRenderer { }); this.drawCanvasOutline(ctx); + this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines const maskImage = this.canvas.maskTool.getMask(); if (maskImage && this.canvas.maskTool.isOverlayVisible) { @@ -328,4 +329,36 @@ export class CanvasRenderer { ctx.stroke(); } } + + drawPendingGenerationAreas(ctx) { + const areasToDraw = []; + + // 1. Get areas from active managers + if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) { + this.canvas.batchPreviewManagers.forEach(manager => { + if (manager.generationArea) { + areasToDraw.push(manager.generationArea); + } + }); + } + + // 2. Get the area from the pending context (if it exists) + if (this.canvas.pendingBatchContext && this.canvas.pendingBatchContext.outputArea) { + areasToDraw.push(this.canvas.pendingBatchContext.outputArea); + } + + if (areasToDraw.length === 0) { + return; + } + + // 3. Draw all collected areas + areasToDraw.forEach(area => { + ctx.save(); + ctx.strokeStyle = 'rgba(0, 150, 255, 0.9)'; // Blue color + ctx.lineWidth = 3 / this.canvas.viewport.zoom; + ctx.setLineDash([12 / this.canvas.viewport.zoom, 6 / this.canvas.viewport.zoom]); + ctx.strokeRect(area.x, area.y, area.width, area.height); + ctx.restore(); + }); + } }