mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 06:22:14 -03:00
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:
@@ -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();
|
||||||
|
|||||||
63
js/Canvas.js
63
js/Canvas.js
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user