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.
This commit is contained in:
Dariusz L
2025-07-03 11:52:16 +02:00
parent e5060fd8c3
commit d40f68b8c6
6 changed files with 137 additions and 38 deletions

View File

@@ -3,7 +3,7 @@ import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('BatchPreviewManager'); const log = createModuleLogger('BatchPreviewManager');
export class BatchPreviewManager { export class BatchPreviewManager {
constructor(canvas, initialPosition = { x: 0, y: 0 }) { constructor(canvas, initialPosition = { x: 0, y: 0 }, generationArea = null) {
this.canvas = canvas; this.canvas = canvas;
this.active = false; this.active = false;
this.layers = []; this.layers = [];
@@ -16,6 +16,7 @@ export class BatchPreviewManager {
this.worldX = initialPosition.x; this.worldX = initialPosition.x;
this.worldY = initialPosition.y; this.worldY = initialPosition.y;
this.isDragging = false; this.isDragging = false;
this.generationArea = generationArea; // Store the generation area
} }
updateScreenPosition(viewport) { updateScreenPosition(viewport) {
@@ -185,6 +186,9 @@ export class BatchPreviewManager {
this.canvas.batchPreviewManagers.splice(index, 1); 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 // Restore mask visibility if it was hidden by this manager
if (this.maskWasVisible && !this.canvas.maskTool.isOverlayVisible) { if (this.maskWasVisible && !this.canvas.maskTool.isOverlayVisible) {
this.canvas.maskTool.toggleOverlayVisibility(); this.canvas.maskTool.toggleOverlayVisibility();

View File

@@ -168,7 +168,7 @@ export class Canvas {
this.canvasIO = new CanvasIO(this); this.canvasIO = new CanvasIO(this);
this.imageReferenceManager = new ImageReferenceManager(this); this.imageReferenceManager = new ImageReferenceManager(this);
this.batchPreviewManagers = []; this.batchPreviewManagers = [];
this.pendingBatchSpawnPosition = null; this.pendingBatchContext = null;
log.debug('Canvas modules initialized successfully'); log.debug('Canvas modules initialized successfully');
} }
@@ -485,36 +485,57 @@ export class Canvas {
let lastExecutionStartTime = 0; let lastExecutionStartTime = 0;
const handleExecutionStart = () => { const handleExecutionStart = () => {
lastExecutionStartTime = Date.now(); if (autoRefreshEnabled) {
// Store the spawn position for the next batch menu, relative to the output area lastExecutionStartTime = Date.now();
this.pendingBatchSpawnPosition = { // Store a snapshot of the context for the upcoming batch
x: this.width / 2, // Horizontally centered on the output area this.pendingBatchContext = {
y: this.height // At the bottom of the output area // For the menu position
}; spawnPosition: {
log.debug(`Execution started, pending spawn position set relative to output area at:`, this.pendingBatchSpawnPosition); 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 () => { const handleExecutionSuccess = async () => {
if (autoRefreshEnabled) { if (autoRefreshEnabled) {
log.info('Auto-refresh triggered, importing latest images.'); 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 (newLayers && newLayers.length > 1) {
if (!this.pendingBatchSpawnPosition) { const newManager = new BatchPreviewManager(
// Fallback in case execution_start didn't fire this,
this.pendingBatchSpawnPosition = { this.pendingBatchContext.spawnPosition,
x: this.width / 2, this.pendingBatchContext.outputArea
y: this.height );
};
log.warn("execution_start did not fire, using fallback spawn position.");
}
const newManager = new BatchPreviewManager(this, this.pendingBatchSpawnPosition);
this.batchPreviewManagers.push(newManager); this.batchPreviewManagers.push(newManager);
newManager.show(newLayers); 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();
} }
}; };

View File

@@ -757,7 +757,7 @@ export class CanvasIO {
} }
} }
async importLatestImages(sinceTimestamp) { async importLatestImages(sinceTimestamp, targetArea = null) {
try { try {
log.info(`Fetching latest images since ${sinceTimestamp}...`); log.info(`Fetching latest images since ${sinceTimestamp}...`);
const response = await fetch(`/layerforge/get-latest-images/${sinceTimestamp}`); const response = await fetch(`/layerforge/get-latest-images/${sinceTimestamp}`);
@@ -774,7 +774,7 @@ export class CanvasIO {
img.onerror = reject; img.onerror = reject;
img.src = imageData; 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); 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.");

View File

@@ -532,11 +532,26 @@ export class CanvasInteractions {
this.canvas.maskTool.updatePosition(-finalX, -finalY); 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 // Also move any active batch preview menus
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) { if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
this.canvas.batchPreviewManagers.forEach(manager => { this.canvas.batchPreviewManagers.forEach(manager => {
manager.worldX -= finalX; manager.worldX -= finalX;
manager.worldY -= finalY; 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) { if (this.interaction.canvasResizeRect && this.interaction.canvasResizeRect.width > 1 && this.interaction.canvasResizeRect.height > 1) {
const newWidth = Math.round(this.interaction.canvasResizeRect.width); const newWidth = Math.round(this.interaction.canvasResizeRect.width);
const newHeight = Math.round(this.interaction.canvasResizeRect.height); const newHeight = Math.round(this.interaction.canvasResizeRect.height);
const rectX = this.interaction.canvasResizeRect.x; const finalX = this.interaction.canvasResizeRect.x;
const rectY = this.interaction.canvasResizeRect.y; const finalY = this.interaction.canvasResizeRect.y;
this.canvas.updateOutputAreaSize(newWidth, newHeight); this.canvas.updateOutputAreaSize(newWidth, newHeight);
this.canvas.layers.forEach(layer => { this.canvas.layers.forEach(layer => {
layer.x -= rectX; layer.x -= finalX;
layer.y -= rectY; layer.y -= finalY;
}); });
this.canvas.maskTool.updatePosition(-rectX, -rectY); this.canvas.maskTool.updatePosition(-finalX, -finalY);
this.canvas.viewport.x -= rectX; // If a batch generation is in progress, update the captured context as well
this.canvas.viewport.y -= rectY; 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;
} }
} }

View File

@@ -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) { if (!image) {
throw createValidationError("Image is required for layer creation"); 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(); const imageId = generateUUID();
await saveImage(imageId, image.src); await saveImage(imageId, image.src);
this.canvas.imageCache.set(imageId, image.src); this.canvas.imageCache.set(imageId, image.src);
@@ -163,18 +163,21 @@ export class CanvasLayers {
let finalHeight = image.height; let finalHeight = image.height;
let finalX, finalY; 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') { 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; finalWidth = image.width * scale;
finalHeight = image.height * scale; finalHeight = image.height * scale;
finalX = (this.canvas.width - finalWidth) / 2; finalX = area.x + (area.width - finalWidth) / 2;
finalY = (this.canvas.height - finalHeight) / 2; finalY = area.y + (area.height - finalHeight) / 2;
} else if (addMode === 'mouse') { } else if (addMode === 'mouse') {
finalX = this.canvas.lastMousePosition.x - finalWidth / 2; finalX = this.canvas.lastMousePosition.x - finalWidth / 2;
finalY = this.canvas.lastMousePosition.y - finalHeight / 2; finalY = this.canvas.lastMousePosition.y - finalHeight / 2;
} else { // 'center' or 'default' } else { // 'center' or 'default'
finalX = (this.canvas.width - finalWidth) / 2; finalX = area.x + (area.width - finalWidth) / 2;
finalY = (this.canvas.height - finalHeight) / 2; finalY = area.y + (area.height - finalHeight) / 2;
} }
const layer = { const layer = {

View File

@@ -82,6 +82,7 @@ export class CanvasRenderer {
}); });
this.drawCanvasOutline(ctx); this.drawCanvasOutline(ctx);
this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines
const maskImage = this.canvas.maskTool.getMask(); const maskImage = this.canvas.maskTool.getMask();
if (maskImage && this.canvas.maskTool.isOverlayVisible) { if (maskImage && this.canvas.maskTool.isOverlayVisible) {
@@ -328,4 +329,36 @@ export class CanvasRenderer {
ctx.stroke(); 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();
});
}
} }