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(); + }); + } }