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');
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();

View File

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

View File

@@ -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.");

View File

@@ -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;
}
}

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) {
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 = {

View File

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