Refactor output area and mask handling for flexible canvas bounds

This update introduces a unified output area bounds system, allowing the output area to be extended in all directions independently of the custom shape. All mask and layer operations now reference outputAreaBounds, ensuring correct alignment and rendering. The mask tool, mask editor, and export logic have been refactored to use these bounds, and a new UI for output area extension with live preview and tooltips has been added. The code also improves logging and visualization of mask and output area boundaries.
This commit is contained in:
Dariusz L
2025-07-26 18:27:14 +02:00
parent 1fc06f65a2
commit 14c5f291a6
17 changed files with 1637 additions and 597 deletions

View File

@@ -59,12 +59,18 @@ export class MaskTool {
}
initMaskCanvas() {
const extraSpace = 2000; // Allow for a generous drawing area outside the output area
this.maskCanvas.width = this.canvasInstance.width + extraSpace;
this.maskCanvas.height = this.canvasInstance.height + extraSpace;
this.x = -extraSpace / 2;
this.y = -extraSpace / 2;
const bounds = this.canvasInstance.outputAreaBounds;
// Mask canvas should cover output area + extra space around it
const maskLeft = bounds.x - extraSpace / 2;
const maskTop = bounds.y - extraSpace / 2;
const maskWidth = bounds.width + extraSpace;
const maskHeight = bounds.height + extraSpace;
this.maskCanvas.width = maskWidth;
this.maskCanvas.height = maskHeight;
this.x = maskLeft;
this.y = maskTop;
this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
log.info(`Initialized mask canvas with extended size: ${this.maskCanvas.width}x${this.maskCanvas.height}, origin at (${this.x}, ${this.y})`);
log.info(`Initialized mask canvas with size: ${this.maskCanvas.width}x${this.maskCanvas.height}, positioned at (${this.x}, ${this.y}) to cover output area at (${bounds.x}, ${bounds.y})`);
}
activate() {
if (!this.previewCanvasInitialized) {
@@ -582,24 +588,82 @@ export class MaskTool {
this.y += dy;
log.info(`Mask position updated to (${this.x}, ${this.y})`);
}
/**
* Updates mask canvas to ensure it covers the current output area
* This should be called when output area position or size changes
*/
updateMaskCanvasForOutputArea() {
const extraSpace = 2000;
const bounds = this.canvasInstance.outputAreaBounds;
// Calculate required mask canvas bounds
const requiredLeft = bounds.x - extraSpace / 2;
const requiredTop = bounds.y - extraSpace / 2;
const requiredWidth = bounds.width + extraSpace;
const requiredHeight = bounds.height + extraSpace;
// Check if current mask canvas covers the required area
const currentRight = this.x + this.maskCanvas.width;
const currentBottom = this.y + this.maskCanvas.height;
const requiredRight = requiredLeft + requiredWidth;
const requiredBottom = requiredTop + requiredHeight;
const needsResize = requiredLeft < this.x ||
requiredTop < this.y ||
requiredRight > currentRight ||
requiredBottom > currentBottom;
if (needsResize) {
log.info(`Updating mask canvas to cover output area at (${bounds.x}, ${bounds.y})`);
// Save current mask content
const oldMask = this.maskCanvas;
const oldX = this.x;
const oldY = this.y;
// Create new mask canvas with proper size and position
this.maskCanvas = document.createElement('canvas');
this.maskCanvas.width = requiredWidth;
this.maskCanvas.height = requiredHeight;
this.x = requiredLeft;
this.y = requiredTop;
const newMaskCtx = this.maskCanvas.getContext('2d', { willReadFrequently: true });
if (!newMaskCtx) {
throw new Error("Failed to get 2D context for new mask canvas");
}
this.maskCtx = newMaskCtx;
// Copy old mask content to new position
if (oldMask.width > 0 && oldMask.height > 0) {
const offsetX = oldX - this.x;
const offsetY = oldY - this.y;
this.maskCtx.drawImage(oldMask, offsetX, offsetY);
log.debug(`Preserved mask content with offset (${offsetX}, ${offsetY})`);
}
log.info(`Mask canvas updated to ${this.maskCanvas.width}x${this.maskCanvas.height} at (${this.x}, ${this.y})`);
}
}
toggleOverlayVisibility() {
this.isOverlayVisible = !this.isOverlayVisible;
log.info(`Mask overlay visibility toggled to: ${this.isOverlayVisible}`);
}
setMask(image) {
const destX = -this.x;
const destY = -this.y;
// Pozycja gdzie ma być aplikowana maska na canvas MaskTool
// MaskTool canvas ma pozycję (this.x, this.y) w świecie
// Maska reprezentuje output bounds, więc musimy ją umieścić
// w pozycji bounds względem pozycji MaskTool
const bounds = this.canvasInstance.outputAreaBounds;
const destX = bounds.x - this.x;
const destY = bounds.y - this.y;
this.maskCtx.clearRect(destX, destY, this.canvasInstance.width, this.canvasInstance.height);
this.maskCtx.drawImage(image, destX, destY);
if (this.onStateChange) {
this.onStateChange();
}
this.canvasInstance.render();
log.info(`MaskTool updated with a new mask image at correct canvas position (${destX}, ${destY}).`);
log.info(`MaskTool updated with a new mask image at position (${destX}, ${destY}) relative to bounds (${bounds.x}, ${bounds.y}).`);
}
addMask(image) {
const destX = -this.x;
const destY = -this.y;
// Pozycja gdzie ma być aplikowana maska na canvas MaskTool
// MaskTool canvas ma pozycję (this.x, this.y) w świecie
// Maska z SAM reprezentuje output bounds, więc musimy ją umieścić
// w pozycji bounds względem pozycji MaskTool
const bounds = this.canvasInstance.outputAreaBounds;
const destX = bounds.x - this.x;
const destY = bounds.y - this.y;
// Don't clear existing mask - just add to it
this.maskCtx.globalCompositeOperation = 'source-over';
this.maskCtx.drawImage(image, destX, destY);
@@ -607,7 +671,7 @@ export class MaskTool {
this.onStateChange();
}
this.canvasInstance.render();
log.info(`MaskTool added mask overlay at correct canvas position (${destX}, ${destY}) without clearing existing mask.`);
log.info(`MaskTool added SAM mask overlay at position (${destX}, ${destY}) relative to bounds (${bounds.x}, ${bounds.y}) without clearing existing mask.`);
}
applyShapeMask(saveState = true) {
if (!this.canvasInstance.outputAreaShape?.points || this.canvasInstance.outputAreaShape.points.length < 3) {