mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 22:35:43 -03:00
Refactor MaskTool chunk operations and shape offset handling
Introduces utility methods for chunk bounds calculation, intersection, and activation for better code reuse and clarity. Refactors shape mask application and removal to consistently account for output area extensions, and centralizes chunk empty status updates. Improves chunk activation logic for mask and shape operations to enhance visibility and maintainability.
This commit is contained in:
319
js/MaskTool.js
319
js/MaskTool.js
@@ -209,6 +209,109 @@ export class MaskTool {
|
|||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Gets extension offset for shape positioning
|
||||||
|
*/
|
||||||
|
getExtensionOffset() {
|
||||||
|
const ext = this.canvasInstance.outputAreaExtensionEnabled ?
|
||||||
|
this.canvasInstance.outputAreaExtensions :
|
||||||
|
{ top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
return { x: ext.left, y: ext.top };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Calculates chunk bounds for a given area
|
||||||
|
*/
|
||||||
|
calculateChunkBounds(left, top, right, bottom) {
|
||||||
|
return {
|
||||||
|
minX: Math.floor(left / this.chunkSize),
|
||||||
|
minY: Math.floor(top / this.chunkSize),
|
||||||
|
maxX: Math.floor(right / this.chunkSize),
|
||||||
|
maxY: Math.floor(bottom / this.chunkSize)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Activates chunks in a specific area and surrounding chunks for visibility
|
||||||
|
*/
|
||||||
|
activateChunksInArea(left, top, right, bottom) {
|
||||||
|
// First, deactivate all chunks
|
||||||
|
for (const chunk of this.maskChunks.values()) {
|
||||||
|
chunk.isActive = false;
|
||||||
|
}
|
||||||
|
const chunkBounds = this.calculateChunkBounds(left, top, right, bottom);
|
||||||
|
// Activate chunks in the area
|
||||||
|
for (let chunkY = chunkBounds.minY; chunkY <= chunkBounds.maxY; chunkY++) {
|
||||||
|
for (let chunkX = chunkBounds.minX; chunkX <= chunkBounds.maxX; chunkX++) {
|
||||||
|
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
||||||
|
chunk.isActive = true;
|
||||||
|
chunk.lastAccessTime = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also activate surrounding chunks for better visibility (3x3 grid around area)
|
||||||
|
const centerChunkX = Math.floor((left + right) / 2 / this.chunkSize);
|
||||||
|
const centerChunkY = Math.floor((top + bottom) / 2 / this.chunkSize);
|
||||||
|
for (let dy = -this.activeChunkRadius; dy <= this.activeChunkRadius; dy++) {
|
||||||
|
for (let dx = -this.activeChunkRadius; dx <= this.activeChunkRadius; dx++) {
|
||||||
|
const chunkX = centerChunkX + dx;
|
||||||
|
const chunkY = centerChunkY + dy;
|
||||||
|
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
||||||
|
chunk.isActive = true;
|
||||||
|
chunk.lastAccessTime = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(this.maskChunks.values()).filter(chunk => chunk.isActive).length;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Calculates intersection between a chunk and a rectangular area
|
||||||
|
* Returns null if no intersection exists
|
||||||
|
*/
|
||||||
|
calculateChunkIntersection(chunk, areaLeft, areaTop, areaRight, areaBottom) {
|
||||||
|
const chunkLeft = chunk.x;
|
||||||
|
const chunkTop = chunk.y;
|
||||||
|
const chunkRight = chunk.x + this.chunkSize;
|
||||||
|
const chunkBottom = chunk.y + this.chunkSize;
|
||||||
|
// Find intersection
|
||||||
|
const intersectLeft = Math.max(chunkLeft, areaLeft);
|
||||||
|
const intersectTop = Math.max(chunkTop, areaTop);
|
||||||
|
const intersectRight = Math.min(chunkRight, areaRight);
|
||||||
|
const intersectBottom = Math.min(chunkBottom, areaBottom);
|
||||||
|
// Check if there's actually an intersection
|
||||||
|
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
||||||
|
return null; // No intersection
|
||||||
|
}
|
||||||
|
// Calculate source coordinates (relative to area)
|
||||||
|
const srcX = intersectLeft - areaLeft;
|
||||||
|
const srcY = intersectTop - areaTop;
|
||||||
|
const srcWidth = intersectRight - intersectLeft;
|
||||||
|
const srcHeight = intersectBottom - intersectTop;
|
||||||
|
// Calculate destination coordinates (relative to chunk)
|
||||||
|
const destX = intersectLeft - chunkLeft;
|
||||||
|
const destY = intersectTop - chunkTop;
|
||||||
|
const destWidth = srcWidth;
|
||||||
|
const destHeight = srcHeight;
|
||||||
|
return {
|
||||||
|
intersectLeft, intersectTop, intersectRight, intersectBottom,
|
||||||
|
srcX, srcY, srcWidth, srcHeight,
|
||||||
|
destX, destY, destWidth, destHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks if a chunk is empty by examining its pixel data
|
||||||
|
* Updates the chunk's isEmpty flag
|
||||||
|
*/
|
||||||
|
updateChunkEmptyStatus(chunk) {
|
||||||
|
const imageData = chunk.ctx.getImageData(0, 0, this.chunkSize, this.chunkSize);
|
||||||
|
const data = imageData.data;
|
||||||
|
let hasData = false;
|
||||||
|
// Check alpha channel for any non-zero values
|
||||||
|
for (let i = 3; i < data.length; i += 4) {
|
||||||
|
if (data[i] > 0) {
|
||||||
|
hasData = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunk.isEmpty = !hasData;
|
||||||
|
chunk.isDirty = true;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Updates which chunks are active for drawing operations based on current drawing position
|
* Updates which chunks are active for drawing operations based on current drawing position
|
||||||
* Only activates chunks in a radius around the drawing position for performance
|
* Only activates chunks in a radius around the drawing position for performance
|
||||||
@@ -599,10 +702,13 @@ export class MaskTool {
|
|||||||
const shape = this.canvasInstance.outputAreaShape;
|
const shape = this.canvasInstance.outputAreaShape;
|
||||||
const viewport = this.canvasInstance.viewport;
|
const viewport = this.canvasInstance.viewport;
|
||||||
const bounds = this.canvasInstance.outputAreaBounds;
|
const bounds = this.canvasInstance.outputAreaBounds;
|
||||||
// Convert shape points to world coordinates first (relative to output area bounds)
|
// Convert shape points to world coordinates first accounting for extensions
|
||||||
|
const ext = this.canvasInstance.outputAreaExtensionEnabled ? this.canvasInstance.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
const shapeOffsetX = ext.left; // Add left extension to maintain relative position
|
||||||
|
const shapeOffsetY = ext.top; // Add top extension to maintain relative position
|
||||||
const worldShapePoints = shape.points.map(p => ({
|
const worldShapePoints = shape.points.map(p => ({
|
||||||
x: bounds.x + p.x,
|
x: bounds.x + shapeOffsetX + p.x,
|
||||||
y: bounds.y + p.y
|
y: bounds.y + shapeOffsetY + p.y
|
||||||
}));
|
}));
|
||||||
// Then convert world coordinates to screen coordinates
|
// Then convert world coordinates to screen coordinates
|
||||||
const screenPoints = worldShapePoints.map(p => ({
|
const screenPoints = worldShapePoints.map(p => ({
|
||||||
@@ -992,44 +1098,19 @@ export class MaskTool {
|
|||||||
* Clears mask data from a specific chunk in a given area
|
* Clears mask data from a specific chunk in a given area
|
||||||
*/
|
*/
|
||||||
clearMaskFromChunk(chunk, clearX, clearY, clearWidth, clearHeight) {
|
clearMaskFromChunk(chunk, clearX, clearY, clearWidth, clearHeight) {
|
||||||
// Calculate the intersection of the clear area with this chunk
|
|
||||||
const chunkLeft = chunk.x;
|
|
||||||
const chunkTop = chunk.y;
|
|
||||||
const chunkRight = chunk.x + this.chunkSize;
|
|
||||||
const chunkBottom = chunk.y + this.chunkSize;
|
|
||||||
const clearLeft = clearX;
|
const clearLeft = clearX;
|
||||||
const clearTop = clearY;
|
const clearTop = clearY;
|
||||||
const clearRight = clearX + clearWidth;
|
const clearRight = clearX + clearWidth;
|
||||||
const clearBottom = clearY + clearHeight;
|
const clearBottom = clearY + clearHeight;
|
||||||
// Find intersection
|
const intersection = this.calculateChunkIntersection(chunk, clearLeft, clearTop, clearRight, clearBottom);
|
||||||
const intersectLeft = Math.max(chunkLeft, clearLeft);
|
if (!intersection) {
|
||||||
const intersectTop = Math.max(chunkTop, clearTop);
|
|
||||||
const intersectRight = Math.min(chunkRight, clearRight);
|
|
||||||
const intersectBottom = Math.min(chunkBottom, clearBottom);
|
|
||||||
// Check if there's actually an intersection
|
|
||||||
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
|
||||||
return; // No intersection
|
return; // No intersection
|
||||||
}
|
}
|
||||||
// Calculate destination coordinates on the chunk
|
|
||||||
const destX = intersectLeft - chunkLeft;
|
|
||||||
const destY = intersectTop - chunkTop;
|
|
||||||
const destWidth = intersectRight - intersectLeft;
|
|
||||||
const destHeight = intersectBottom - intersectTop;
|
|
||||||
// Clear the area on this chunk
|
// Clear the area on this chunk
|
||||||
chunk.ctx.clearRect(destX, destY, destWidth, destHeight);
|
chunk.ctx.clearRect(intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight);
|
||||||
// Check if the entire chunk is now empty
|
// Update chunk empty status
|
||||||
const imageData = chunk.ctx.getImageData(0, 0, this.chunkSize, this.chunkSize);
|
this.updateChunkEmptyStatus(chunk);
|
||||||
const data = imageData.data;
|
log.debug(`Cleared area from chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${intersection.destX}, ${intersection.destY})`);
|
||||||
let hasData = false;
|
|
||||||
for (let i = 3; i < data.length; i += 4) { // Check alpha channel
|
|
||||||
if (data[i] > 0) {
|
|
||||||
hasData = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chunk.isEmpty = !hasData;
|
|
||||||
chunk.isDirty = true;
|
|
||||||
log.debug(`Cleared area from chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${destX}, ${destY})`);
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Clears all mask chunks - used by the clear() function
|
* Clears all mask chunks - used by the clear() function
|
||||||
@@ -1054,84 +1135,45 @@ export class MaskTool {
|
|||||||
const maskTop = bounds.y;
|
const maskTop = bounds.y;
|
||||||
const maskRight = bounds.x + image.width;
|
const maskRight = bounds.x + image.width;
|
||||||
const maskBottom = bounds.y + image.height;
|
const maskBottom = bounds.y + image.height;
|
||||||
const chunkMinX = Math.floor(maskLeft / this.chunkSize);
|
const chunkBounds = this.calculateChunkBounds(maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const chunkMinY = Math.floor(maskTop / this.chunkSize);
|
// Add mask to all affected chunks
|
||||||
const chunkMaxX = Math.floor(maskRight / this.chunkSize);
|
for (let chunkY = chunkBounds.minY; chunkY <= chunkBounds.maxY; chunkY++) {
|
||||||
const chunkMaxY = Math.floor(maskBottom / this.chunkSize);
|
for (let chunkX = chunkBounds.minX; chunkX <= chunkBounds.maxX; chunkX++) {
|
||||||
// First, deactivate all chunks
|
|
||||||
for (const chunk of this.maskChunks.values()) {
|
|
||||||
chunk.isActive = false;
|
|
||||||
}
|
|
||||||
// Add mask to all affected chunks and activate them so user can see the mask being applied
|
|
||||||
for (let chunkY = chunkMinY; chunkY <= chunkMaxY; chunkY++) {
|
|
||||||
for (let chunkX = chunkMinX; chunkX <= chunkMaxX; chunkX++) {
|
|
||||||
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
||||||
this.addMaskToChunk(chunk, image, bounds);
|
this.addMaskToChunk(chunk, image, bounds);
|
||||||
// Activate this chunk so user can see the mask being applied
|
|
||||||
chunk.isActive = true;
|
|
||||||
chunk.lastAccessTime = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Also activate surrounding chunks for better visibility (3x3 grid around mask area)
|
|
||||||
const centerChunkX = Math.floor((maskLeft + maskRight) / 2 / this.chunkSize);
|
|
||||||
const centerChunkY = Math.floor((maskTop + maskBottom) / 2 / this.chunkSize);
|
|
||||||
for (let dy = -this.activeChunkRadius; dy <= this.activeChunkRadius; dy++) {
|
|
||||||
for (let dx = -this.activeChunkRadius; dx <= this.activeChunkRadius; dx++) {
|
|
||||||
const chunkX = centerChunkX + dx;
|
|
||||||
const chunkY = centerChunkY + dy;
|
|
||||||
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
|
||||||
chunk.isActive = true;
|
|
||||||
chunk.lastAccessTime = Date.now();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Activate chunks in the area for visibility
|
||||||
|
const activatedChunks = this.activateChunksInArea(maskLeft, maskTop, maskRight, maskBottom);
|
||||||
// Update active canvas to show the new mask with activated chunks
|
// Update active canvas to show the new mask with activated chunks
|
||||||
this.updateActiveMaskCanvas(true); // Force full update to show all chunks including newly activated ones
|
this.updateActiveMaskCanvas(true); // Force full update to show all chunks including newly activated ones
|
||||||
if (this.onStateChange) {
|
if (this.onStateChange) {
|
||||||
this.onStateChange();
|
this.onStateChange();
|
||||||
}
|
}
|
||||||
this.canvasInstance.render();
|
this.canvasInstance.render();
|
||||||
const activatedChunks = Array.from(this.maskChunks.values()).filter(chunk => chunk.isActive).length;
|
|
||||||
log.info(`MaskTool added SAM mask to chunks covering bounds (${bounds.x}, ${bounds.y}) to (${maskRight}, ${maskBottom}) and activated ${activatedChunks} chunks for visibility`);
|
log.info(`MaskTool added SAM mask to chunks covering bounds (${bounds.x}, ${bounds.y}) to (${maskRight}, ${maskBottom}) and activated ${activatedChunks} chunks for visibility`);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Adds a mask image to a specific chunk
|
* Adds a mask image to a specific chunk
|
||||||
*/
|
*/
|
||||||
addMaskToChunk(chunk, maskImage, bounds) {
|
addMaskToChunk(chunk, maskImage, bounds) {
|
||||||
// Calculate the intersection of the mask with this chunk
|
|
||||||
const chunkLeft = chunk.x;
|
|
||||||
const chunkTop = chunk.y;
|
|
||||||
const chunkRight = chunk.x + this.chunkSize;
|
|
||||||
const chunkBottom = chunk.y + this.chunkSize;
|
|
||||||
const maskLeft = bounds.x;
|
const maskLeft = bounds.x;
|
||||||
const maskTop = bounds.y;
|
const maskTop = bounds.y;
|
||||||
const maskRight = bounds.x + maskImage.width;
|
const maskRight = bounds.x + maskImage.width;
|
||||||
const maskBottom = bounds.y + maskImage.height;
|
const maskBottom = bounds.y + maskImage.height;
|
||||||
// Find intersection
|
const intersection = this.calculateChunkIntersection(chunk, maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const intersectLeft = Math.max(chunkLeft, maskLeft);
|
if (!intersection) {
|
||||||
const intersectTop = Math.max(chunkTop, maskTop);
|
|
||||||
const intersectRight = Math.min(chunkRight, maskRight);
|
|
||||||
const intersectBottom = Math.min(chunkBottom, maskBottom);
|
|
||||||
// Check if there's actually an intersection
|
|
||||||
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
|
||||||
return; // No intersection
|
return; // No intersection
|
||||||
}
|
}
|
||||||
// Calculate source coordinates on the mask image
|
|
||||||
const srcX = intersectLeft - maskLeft;
|
|
||||||
const srcY = intersectTop - maskTop;
|
|
||||||
const srcWidth = intersectRight - intersectLeft;
|
|
||||||
const srcHeight = intersectBottom - intersectTop;
|
|
||||||
// Calculate destination coordinates on the chunk
|
|
||||||
const destX = intersectLeft - chunkLeft;
|
|
||||||
const destY = intersectTop - chunkTop;
|
|
||||||
// Draw the mask portion onto this chunk
|
// Draw the mask portion onto this chunk
|
||||||
chunk.ctx.globalCompositeOperation = 'source-over';
|
chunk.ctx.globalCompositeOperation = 'source-over';
|
||||||
chunk.ctx.drawImage(maskImage, srcX, srcY, srcWidth, srcHeight, // Source rectangle
|
chunk.ctx.drawImage(maskImage, intersection.srcX, intersection.srcY, intersection.srcWidth, intersection.srcHeight, // Source rectangle
|
||||||
destX, destY, srcWidth, srcHeight // Destination rectangle
|
intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight // Destination rectangle
|
||||||
);
|
);
|
||||||
// Mark chunk as dirty and not empty
|
// Mark chunk as dirty and not empty
|
||||||
chunk.isDirty = true;
|
chunk.isDirty = true;
|
||||||
chunk.isEmpty = false;
|
chunk.isEmpty = false;
|
||||||
log.debug(`Added mask to chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${destX}, ${destY})`);
|
log.debug(`Added mask to chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${intersection.destX}, ${intersection.destY})`);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Applies a mask canvas to the chunked system at a specific world position
|
* Applies a mask canvas to the chunked system at a specific world position
|
||||||
@@ -1183,92 +1225,46 @@ export class MaskTool {
|
|||||||
* Removes a mask canvas from a specific chunk using destination-out composition
|
* Removes a mask canvas from a specific chunk using destination-out composition
|
||||||
*/
|
*/
|
||||||
removeMaskCanvasFromChunk(chunk, maskCanvas, maskWorldX, maskWorldY) {
|
removeMaskCanvasFromChunk(chunk, maskCanvas, maskWorldX, maskWorldY) {
|
||||||
// Calculate the intersection of the mask with this chunk
|
|
||||||
const chunkLeft = chunk.x;
|
|
||||||
const chunkTop = chunk.y;
|
|
||||||
const chunkRight = chunk.x + this.chunkSize;
|
|
||||||
const chunkBottom = chunk.y + this.chunkSize;
|
|
||||||
const maskLeft = maskWorldX;
|
const maskLeft = maskWorldX;
|
||||||
const maskTop = maskWorldY;
|
const maskTop = maskWorldY;
|
||||||
const maskRight = maskWorldX + maskCanvas.width;
|
const maskRight = maskWorldX + maskCanvas.width;
|
||||||
const maskBottom = maskWorldY + maskCanvas.height;
|
const maskBottom = maskWorldY + maskCanvas.height;
|
||||||
// Find intersection
|
const intersection = this.calculateChunkIntersection(chunk, maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const intersectLeft = Math.max(chunkLeft, maskLeft);
|
if (!intersection) {
|
||||||
const intersectTop = Math.max(chunkTop, maskTop);
|
|
||||||
const intersectRight = Math.min(chunkRight, maskRight);
|
|
||||||
const intersectBottom = Math.min(chunkBottom, maskBottom);
|
|
||||||
// Check if there's actually an intersection
|
|
||||||
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
|
||||||
return; // No intersection
|
return; // No intersection
|
||||||
}
|
}
|
||||||
// Calculate source coordinates on the mask canvas
|
|
||||||
const srcX = intersectLeft - maskLeft;
|
|
||||||
const srcY = intersectTop - maskTop;
|
|
||||||
const srcWidth = intersectRight - intersectLeft;
|
|
||||||
const srcHeight = intersectBottom - intersectTop;
|
|
||||||
// Calculate destination coordinates on the chunk
|
|
||||||
const destX = intersectLeft - chunkLeft;
|
|
||||||
const destY = intersectTop - chunkTop;
|
|
||||||
// Use destination-out to remove the mask portion from this chunk
|
// Use destination-out to remove the mask portion from this chunk
|
||||||
chunk.ctx.globalCompositeOperation = 'destination-out';
|
chunk.ctx.globalCompositeOperation = 'destination-out';
|
||||||
chunk.ctx.drawImage(maskCanvas, srcX, srcY, srcWidth, srcHeight, // Source rectangle
|
chunk.ctx.drawImage(maskCanvas, intersection.srcX, intersection.srcY, intersection.srcWidth, intersection.srcHeight, // Source rectangle
|
||||||
destX, destY, srcWidth, srcHeight // Destination rectangle
|
intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight // Destination rectangle
|
||||||
);
|
);
|
||||||
// Restore normal composition mode
|
// Restore normal composition mode
|
||||||
chunk.ctx.globalCompositeOperation = 'source-over';
|
chunk.ctx.globalCompositeOperation = 'source-over';
|
||||||
// Check if the chunk is now empty
|
// Update chunk empty status
|
||||||
const imageData = chunk.ctx.getImageData(0, 0, this.chunkSize, this.chunkSize);
|
this.updateChunkEmptyStatus(chunk);
|
||||||
const data = imageData.data;
|
log.debug(`Removed mask canvas from chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${intersection.destX}, ${intersection.destY})`);
|
||||||
let hasData = false;
|
|
||||||
for (let i = 3; i < data.length; i += 4) { // Check alpha channel
|
|
||||||
if (data[i] > 0) {
|
|
||||||
hasData = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chunk.isEmpty = !hasData;
|
|
||||||
chunk.isDirty = true;
|
|
||||||
log.debug(`Removed mask canvas from chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${destX}, ${destY})`);
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Applies a mask canvas to a specific chunk
|
* Applies a mask canvas to a specific chunk
|
||||||
*/
|
*/
|
||||||
applyMaskCanvasToChunk(chunk, maskCanvas, maskWorldX, maskWorldY) {
|
applyMaskCanvasToChunk(chunk, maskCanvas, maskWorldX, maskWorldY) {
|
||||||
// Calculate the intersection of the mask with this chunk
|
|
||||||
const chunkLeft = chunk.x;
|
|
||||||
const chunkTop = chunk.y;
|
|
||||||
const chunkRight = chunk.x + this.chunkSize;
|
|
||||||
const chunkBottom = chunk.y + this.chunkSize;
|
|
||||||
const maskLeft = maskWorldX;
|
const maskLeft = maskWorldX;
|
||||||
const maskTop = maskWorldY;
|
const maskTop = maskWorldY;
|
||||||
const maskRight = maskWorldX + maskCanvas.width;
|
const maskRight = maskWorldX + maskCanvas.width;
|
||||||
const maskBottom = maskWorldY + maskCanvas.height;
|
const maskBottom = maskWorldY + maskCanvas.height;
|
||||||
// Find intersection
|
const intersection = this.calculateChunkIntersection(chunk, maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const intersectLeft = Math.max(chunkLeft, maskLeft);
|
if (!intersection) {
|
||||||
const intersectTop = Math.max(chunkTop, maskTop);
|
|
||||||
const intersectRight = Math.min(chunkRight, maskRight);
|
|
||||||
const intersectBottom = Math.min(chunkBottom, maskBottom);
|
|
||||||
// Check if there's actually an intersection
|
|
||||||
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
|
||||||
return; // No intersection
|
return; // No intersection
|
||||||
}
|
}
|
||||||
// Calculate source coordinates on the mask canvas
|
|
||||||
const srcX = intersectLeft - maskLeft;
|
|
||||||
const srcY = intersectTop - maskTop;
|
|
||||||
const srcWidth = intersectRight - intersectLeft;
|
|
||||||
const srcHeight = intersectBottom - intersectTop;
|
|
||||||
// Calculate destination coordinates on the chunk
|
|
||||||
const destX = intersectLeft - chunkLeft;
|
|
||||||
const destY = intersectTop - chunkTop;
|
|
||||||
// Draw the mask portion onto this chunk
|
// Draw the mask portion onto this chunk
|
||||||
chunk.ctx.globalCompositeOperation = 'source-over';
|
chunk.ctx.globalCompositeOperation = 'source-over';
|
||||||
chunk.ctx.drawImage(maskCanvas, srcX, srcY, srcWidth, srcHeight, // Source rectangle
|
chunk.ctx.drawImage(maskCanvas, intersection.srcX, intersection.srcY, intersection.srcWidth, intersection.srcHeight, // Source rectangle
|
||||||
destX, destY, srcWidth, srcHeight // Destination rectangle
|
intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight // Destination rectangle
|
||||||
);
|
);
|
||||||
// Mark chunk as dirty and not empty
|
// Mark chunk as dirty and not empty
|
||||||
chunk.isDirty = true;
|
chunk.isDirty = true;
|
||||||
chunk.isEmpty = false;
|
chunk.isEmpty = false;
|
||||||
log.debug(`Applied mask canvas to chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${destX}, ${destY})`);
|
log.debug(`Applied mask canvas to chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${intersection.destX}, ${intersection.destY})`);
|
||||||
}
|
}
|
||||||
applyShapeMask(saveState = true) {
|
applyShapeMask(saveState = true) {
|
||||||
if (!this.canvasInstance.outputAreaShape?.points || this.canvasInstance.outputAreaShape.points.length < 3) {
|
if (!this.canvasInstance.outputAreaShape?.points || this.canvasInstance.outputAreaShape.points.length < 3) {
|
||||||
@@ -1280,11 +1276,14 @@ export class MaskTool {
|
|||||||
}
|
}
|
||||||
const shape = this.canvasInstance.outputAreaShape;
|
const shape = this.canvasInstance.outputAreaShape;
|
||||||
const bounds = this.canvasInstance.outputAreaBounds;
|
const bounds = this.canvasInstance.outputAreaBounds;
|
||||||
// Calculate shape points in world coordinates
|
// Calculate shape points in world coordinates accounting for extensions
|
||||||
// Shape points are relative to the output area bounds
|
// Shape points are relative to the output area bounds, but need extension offset
|
||||||
|
const ext = this.canvasInstance.outputAreaExtensionEnabled ? this.canvasInstance.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
const shapeOffsetX = ext.left; // Add left extension to maintain relative position
|
||||||
|
const shapeOffsetY = ext.top; // Add top extension to maintain relative position
|
||||||
const worldShapePoints = shape.points.map(p => ({
|
const worldShapePoints = shape.points.map(p => ({
|
||||||
x: bounds.x + p.x,
|
x: bounds.x + shapeOffsetX + p.x,
|
||||||
y: bounds.y + p.y
|
y: bounds.y + shapeOffsetY + p.y
|
||||||
}));
|
}));
|
||||||
// Create the shape mask canvas
|
// Create the shape mask canvas
|
||||||
let shapeMaskCanvas;
|
let shapeMaskCanvas;
|
||||||
@@ -1332,15 +1331,24 @@ export class MaskTool {
|
|||||||
const expandedImageData = tempCtx.getImageData(0, 0, expandedMaskCanvas.width, expandedMaskCanvas.height);
|
const expandedImageData = tempCtx.getImageData(0, 0, expandedMaskCanvas.width, expandedMaskCanvas.height);
|
||||||
shapeMaskCanvas = this._createFeatheredMaskFromImageData(expandedImageData, this.canvasInstance.shapeMaskFeatherValue, tempCanvasWidth, tempCanvasHeight);
|
shapeMaskCanvas = this._createFeatheredMaskFromImageData(expandedImageData, this.canvasInstance.shapeMaskFeatherValue, tempCanvasWidth, tempCanvasHeight);
|
||||||
}
|
}
|
||||||
// Now apply the shape mask to the chunked system
|
// Calculate which chunks will be affected by the shape mask
|
||||||
this.applyMaskCanvasToChunks(shapeMaskCanvas, bounds.x - tempOffsetX, bounds.y - tempOffsetY);
|
const maskWorldX = bounds.x - tempOffsetX;
|
||||||
// Update the active mask canvas to show the changes
|
const maskWorldY = bounds.y - tempOffsetY;
|
||||||
this.updateActiveMaskCanvas();
|
const maskLeft = maskWorldX;
|
||||||
|
const maskTop = maskWorldY;
|
||||||
|
const maskRight = maskWorldX + shapeMaskCanvas.width;
|
||||||
|
const maskBottom = maskWorldY + shapeMaskCanvas.height;
|
||||||
|
// Apply the shape mask to the chunked system
|
||||||
|
this.applyMaskCanvasToChunks(shapeMaskCanvas, maskWorldX, maskWorldY);
|
||||||
|
// Activate chunks in the area for visibility
|
||||||
|
const activatedChunks = this.activateChunksInArea(maskLeft, maskTop, maskRight, maskBottom);
|
||||||
|
// Update the active mask canvas to show the changes with activated chunks
|
||||||
|
this.updateActiveMaskCanvas(true); // Force full update to show all chunks including newly activated ones
|
||||||
if (this.onStateChange) {
|
if (this.onStateChange) {
|
||||||
this.onStateChange();
|
this.onStateChange();
|
||||||
}
|
}
|
||||||
this.canvasInstance.render();
|
this.canvasInstance.render();
|
||||||
log.info(`Applied shape mask to chunks with expansion: ${needsExpansion}, feather: ${needsFeather}.`);
|
log.info(`Applied shape mask to chunks with expansion: ${needsExpansion}, feather: ${needsFeather} and activated ${activatedChunks} chunks for visibility`);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Removes mask in the area of the custom output area shape. This must use a hard-edged
|
* Removes mask in the area of the custom output area shape. This must use a hard-edged
|
||||||
@@ -1355,10 +1363,13 @@ export class MaskTool {
|
|||||||
this.canvasInstance.canvasState.saveMaskState();
|
this.canvasInstance.canvasState.saveMaskState();
|
||||||
const shape = this.canvasInstance.outputAreaShape;
|
const shape = this.canvasInstance.outputAreaShape;
|
||||||
const bounds = this.canvasInstance.outputAreaBounds;
|
const bounds = this.canvasInstance.outputAreaBounds;
|
||||||
// Calculate shape points in world coordinates (same as applyShapeMask)
|
// Calculate shape points in world coordinates accounting for extensions (same as applyShapeMask)
|
||||||
|
const ext = this.canvasInstance.outputAreaExtensionEnabled ? this.canvasInstance.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
const shapeOffsetX = ext.left; // Add left extension to maintain relative position
|
||||||
|
const shapeOffsetY = ext.top; // Add top extension to maintain relative position
|
||||||
const worldShapePoints = shape.points.map(p => ({
|
const worldShapePoints = shape.points.map(p => ({
|
||||||
x: bounds.x + p.x,
|
x: bounds.x + shapeOffsetX + p.x,
|
||||||
y: bounds.y + p.y
|
y: bounds.y + shapeOffsetY + p.y
|
||||||
}));
|
}));
|
||||||
// Check if we need to account for expansion when removing
|
// Check if we need to account for expansion when removing
|
||||||
const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0;
|
const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0;
|
||||||
|
|||||||
368
src/MaskTool.ts
368
src/MaskTool.ts
@@ -301,6 +301,132 @@ export class MaskTool {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets extension offset for shape positioning
|
||||||
|
*/
|
||||||
|
private getExtensionOffset(): { x: number, y: number } {
|
||||||
|
const ext = this.canvasInstance.outputAreaExtensionEnabled ?
|
||||||
|
this.canvasInstance.outputAreaExtensions :
|
||||||
|
{ top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
return { x: ext.left, y: ext.top };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates chunk bounds for a given area
|
||||||
|
*/
|
||||||
|
private calculateChunkBounds(left: number, top: number, right: number, bottom: number): {
|
||||||
|
minX: number, minY: number, maxX: number, maxY: number
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
minX: Math.floor(left / this.chunkSize),
|
||||||
|
minY: Math.floor(top / this.chunkSize),
|
||||||
|
maxX: Math.floor(right / this.chunkSize),
|
||||||
|
maxY: Math.floor(bottom / this.chunkSize)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates chunks in a specific area and surrounding chunks for visibility
|
||||||
|
*/
|
||||||
|
private activateChunksInArea(left: number, top: number, right: number, bottom: number): number {
|
||||||
|
// First, deactivate all chunks
|
||||||
|
for (const chunk of this.maskChunks.values()) {
|
||||||
|
chunk.isActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkBounds = this.calculateChunkBounds(left, top, right, bottom);
|
||||||
|
|
||||||
|
// Activate chunks in the area
|
||||||
|
for (let chunkY = chunkBounds.minY; chunkY <= chunkBounds.maxY; chunkY++) {
|
||||||
|
for (let chunkX = chunkBounds.minX; chunkX <= chunkBounds.maxX; chunkX++) {
|
||||||
|
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
||||||
|
chunk.isActive = true;
|
||||||
|
chunk.lastAccessTime = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also activate surrounding chunks for better visibility (3x3 grid around area)
|
||||||
|
const centerChunkX = Math.floor((left + right) / 2 / this.chunkSize);
|
||||||
|
const centerChunkY = Math.floor((top + bottom) / 2 / this.chunkSize);
|
||||||
|
|
||||||
|
for (let dy = -this.activeChunkRadius; dy <= this.activeChunkRadius; dy++) {
|
||||||
|
for (let dx = -this.activeChunkRadius; dx <= this.activeChunkRadius; dx++) {
|
||||||
|
const chunkX = centerChunkX + dx;
|
||||||
|
const chunkY = centerChunkY + dy;
|
||||||
|
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
||||||
|
chunk.isActive = true;
|
||||||
|
chunk.lastAccessTime = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(this.maskChunks.values()).filter(chunk => chunk.isActive).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates intersection between a chunk and a rectangular area
|
||||||
|
* Returns null if no intersection exists
|
||||||
|
*/
|
||||||
|
private calculateChunkIntersection(chunk: MaskChunk, areaLeft: number, areaTop: number, areaRight: number, areaBottom: number): {
|
||||||
|
intersectLeft: number, intersectTop: number, intersectRight: number, intersectBottom: number,
|
||||||
|
srcX: number, srcY: number, srcWidth: number, srcHeight: number,
|
||||||
|
destX: number, destY: number, destWidth: number, destHeight: number
|
||||||
|
} | null {
|
||||||
|
const chunkLeft = chunk.x;
|
||||||
|
const chunkTop = chunk.y;
|
||||||
|
const chunkRight = chunk.x + this.chunkSize;
|
||||||
|
const chunkBottom = chunk.y + this.chunkSize;
|
||||||
|
|
||||||
|
// Find intersection
|
||||||
|
const intersectLeft = Math.max(chunkLeft, areaLeft);
|
||||||
|
const intersectTop = Math.max(chunkTop, areaTop);
|
||||||
|
const intersectRight = Math.min(chunkRight, areaRight);
|
||||||
|
const intersectBottom = Math.min(chunkBottom, areaBottom);
|
||||||
|
|
||||||
|
// Check if there's actually an intersection
|
||||||
|
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
||||||
|
return null; // No intersection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate source coordinates (relative to area)
|
||||||
|
const srcX = intersectLeft - areaLeft;
|
||||||
|
const srcY = intersectTop - areaTop;
|
||||||
|
const srcWidth = intersectRight - intersectLeft;
|
||||||
|
const srcHeight = intersectBottom - intersectTop;
|
||||||
|
|
||||||
|
// Calculate destination coordinates (relative to chunk)
|
||||||
|
const destX = intersectLeft - chunkLeft;
|
||||||
|
const destY = intersectTop - chunkTop;
|
||||||
|
const destWidth = srcWidth;
|
||||||
|
const destHeight = srcHeight;
|
||||||
|
|
||||||
|
return {
|
||||||
|
intersectLeft, intersectTop, intersectRight, intersectBottom,
|
||||||
|
srcX, srcY, srcWidth, srcHeight,
|
||||||
|
destX, destY, destWidth, destHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a chunk is empty by examining its pixel data
|
||||||
|
* Updates the chunk's isEmpty flag
|
||||||
|
*/
|
||||||
|
private updateChunkEmptyStatus(chunk: MaskChunk): void {
|
||||||
|
const imageData = chunk.ctx.getImageData(0, 0, this.chunkSize, this.chunkSize);
|
||||||
|
const data = imageData.data;
|
||||||
|
let hasData = false;
|
||||||
|
|
||||||
|
// Check alpha channel for any non-zero values
|
||||||
|
for (let i = 3; i < data.length; i += 4) {
|
||||||
|
if (data[i] > 0) {
|
||||||
|
hasData = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.isEmpty = !hasData;
|
||||||
|
chunk.isDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates which chunks are active for drawing operations based on current drawing position
|
* Updates which chunks are active for drawing operations based on current drawing position
|
||||||
* Only activates chunks in a radius around the drawing position for performance
|
* Only activates chunks in a radius around the drawing position for performance
|
||||||
@@ -764,10 +890,14 @@ export class MaskTool {
|
|||||||
const viewport = this.canvasInstance.viewport;
|
const viewport = this.canvasInstance.viewport;
|
||||||
const bounds = this.canvasInstance.outputAreaBounds;
|
const bounds = this.canvasInstance.outputAreaBounds;
|
||||||
|
|
||||||
// Convert shape points to world coordinates first (relative to output area bounds)
|
// Convert shape points to world coordinates first accounting for extensions
|
||||||
|
const ext = this.canvasInstance.outputAreaExtensionEnabled ? this.canvasInstance.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
const shapeOffsetX = ext.left; // Add left extension to maintain relative position
|
||||||
|
const shapeOffsetY = ext.top; // Add top extension to maintain relative position
|
||||||
|
|
||||||
const worldShapePoints = shape.points.map(p => ({
|
const worldShapePoints = shape.points.map(p => ({
|
||||||
x: bounds.x + p.x,
|
x: bounds.x + shapeOffsetX + p.x,
|
||||||
y: bounds.y + p.y
|
y: bounds.y + shapeOffsetY + p.y
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Then convert world coordinates to screen coordinates
|
// Then convert world coordinates to screen coordinates
|
||||||
@@ -1216,52 +1346,23 @@ export class MaskTool {
|
|||||||
* Clears mask data from a specific chunk in a given area
|
* Clears mask data from a specific chunk in a given area
|
||||||
*/
|
*/
|
||||||
private clearMaskFromChunk(chunk: MaskChunk, clearX: number, clearY: number, clearWidth: number, clearHeight: number): void {
|
private clearMaskFromChunk(chunk: MaskChunk, clearX: number, clearY: number, clearWidth: number, clearHeight: number): void {
|
||||||
// Calculate the intersection of the clear area with this chunk
|
|
||||||
const chunkLeft = chunk.x;
|
|
||||||
const chunkTop = chunk.y;
|
|
||||||
const chunkRight = chunk.x + this.chunkSize;
|
|
||||||
const chunkBottom = chunk.y + this.chunkSize;
|
|
||||||
|
|
||||||
const clearLeft = clearX;
|
const clearLeft = clearX;
|
||||||
const clearTop = clearY;
|
const clearTop = clearY;
|
||||||
const clearRight = clearX + clearWidth;
|
const clearRight = clearX + clearWidth;
|
||||||
const clearBottom = clearY + clearHeight;
|
const clearBottom = clearY + clearHeight;
|
||||||
|
|
||||||
// Find intersection
|
const intersection = this.calculateChunkIntersection(chunk, clearLeft, clearTop, clearRight, clearBottom);
|
||||||
const intersectLeft = Math.max(chunkLeft, clearLeft);
|
if (!intersection) {
|
||||||
const intersectTop = Math.max(chunkTop, clearTop);
|
|
||||||
const intersectRight = Math.min(chunkRight, clearRight);
|
|
||||||
const intersectBottom = Math.min(chunkBottom, clearBottom);
|
|
||||||
|
|
||||||
// Check if there's actually an intersection
|
|
||||||
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
|
||||||
return; // No intersection
|
return; // No intersection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate destination coordinates on the chunk
|
|
||||||
const destX = intersectLeft - chunkLeft;
|
|
||||||
const destY = intersectTop - chunkTop;
|
|
||||||
const destWidth = intersectRight - intersectLeft;
|
|
||||||
const destHeight = intersectBottom - intersectTop;
|
|
||||||
|
|
||||||
// Clear the area on this chunk
|
// Clear the area on this chunk
|
||||||
chunk.ctx.clearRect(destX, destY, destWidth, destHeight);
|
chunk.ctx.clearRect(intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight);
|
||||||
|
|
||||||
// Check if the entire chunk is now empty
|
// Update chunk empty status
|
||||||
const imageData = chunk.ctx.getImageData(0, 0, this.chunkSize, this.chunkSize);
|
this.updateChunkEmptyStatus(chunk);
|
||||||
const data = imageData.data;
|
|
||||||
let hasData = false;
|
|
||||||
for (let i = 3; i < data.length; i += 4) { // Check alpha channel
|
|
||||||
if (data[i] > 0) {
|
|
||||||
hasData = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk.isEmpty = !hasData;
|
log.debug(`Cleared area from chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${intersection.destX}, ${intersection.destY})`);
|
||||||
chunk.isDirty = true;
|
|
||||||
|
|
||||||
log.debug(`Cleared area from chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${destX}, ${destY})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1292,41 +1393,18 @@ export class MaskTool {
|
|||||||
const maskRight = bounds.x + image.width;
|
const maskRight = bounds.x + image.width;
|
||||||
const maskBottom = bounds.y + image.height;
|
const maskBottom = bounds.y + image.height;
|
||||||
|
|
||||||
const chunkMinX = Math.floor(maskLeft / this.chunkSize);
|
const chunkBounds = this.calculateChunkBounds(maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const chunkMinY = Math.floor(maskTop / this.chunkSize);
|
|
||||||
const chunkMaxX = Math.floor(maskRight / this.chunkSize);
|
|
||||||
const chunkMaxY = Math.floor(maskBottom / this.chunkSize);
|
|
||||||
|
|
||||||
// First, deactivate all chunks
|
// Add mask to all affected chunks
|
||||||
for (const chunk of this.maskChunks.values()) {
|
for (let chunkY = chunkBounds.minY; chunkY <= chunkBounds.maxY; chunkY++) {
|
||||||
chunk.isActive = false;
|
for (let chunkX = chunkBounds.minX; chunkX <= chunkBounds.maxX; chunkX++) {
|
||||||
}
|
|
||||||
|
|
||||||
// Add mask to all affected chunks and activate them so user can see the mask being applied
|
|
||||||
for (let chunkY = chunkMinY; chunkY <= chunkMaxY; chunkY++) {
|
|
||||||
for (let chunkX = chunkMinX; chunkX <= chunkMaxX; chunkX++) {
|
|
||||||
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
||||||
this.addMaskToChunk(chunk, image, bounds);
|
this.addMaskToChunk(chunk, image, bounds);
|
||||||
|
|
||||||
// Activate this chunk so user can see the mask being applied
|
|
||||||
chunk.isActive = true;
|
|
||||||
chunk.lastAccessTime = Date.now();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also activate surrounding chunks for better visibility (3x3 grid around mask area)
|
// Activate chunks in the area for visibility
|
||||||
const centerChunkX = Math.floor((maskLeft + maskRight) / 2 / this.chunkSize);
|
const activatedChunks = this.activateChunksInArea(maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const centerChunkY = Math.floor((maskTop + maskBottom) / 2 / this.chunkSize);
|
|
||||||
|
|
||||||
for (let dy = -this.activeChunkRadius; dy <= this.activeChunkRadius; dy++) {
|
|
||||||
for (let dx = -this.activeChunkRadius; dx <= this.activeChunkRadius; dx++) {
|
|
||||||
const chunkX = centerChunkX + dx;
|
|
||||||
const chunkY = centerChunkY + dy;
|
|
||||||
const chunk = this.getChunkForPosition(chunkX * this.chunkSize, chunkY * this.chunkSize);
|
|
||||||
chunk.isActive = true;
|
|
||||||
chunk.lastAccessTime = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update active canvas to show the new mask with activated chunks
|
// Update active canvas to show the new mask with activated chunks
|
||||||
this.updateActiveMaskCanvas(true); // Force full update to show all chunks including newly activated ones
|
this.updateActiveMaskCanvas(true); // Force full update to show all chunks including newly activated ones
|
||||||
@@ -1336,7 +1414,6 @@ export class MaskTool {
|
|||||||
}
|
}
|
||||||
this.canvasInstance.render();
|
this.canvasInstance.render();
|
||||||
|
|
||||||
const activatedChunks = Array.from(this.maskChunks.values()).filter(chunk => chunk.isActive).length;
|
|
||||||
log.info(`MaskTool added SAM mask to chunks covering bounds (${bounds.x}, ${bounds.y}) to (${maskRight}, ${maskBottom}) and activated ${activatedChunks} chunks for visibility`);
|
log.info(`MaskTool added SAM mask to chunks covering bounds (${bounds.x}, ${bounds.y}) to (${maskRight}, ${maskBottom}) and activated ${activatedChunks} chunks for visibility`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1344,51 +1421,29 @@ export class MaskTool {
|
|||||||
* Adds a mask image to a specific chunk
|
* Adds a mask image to a specific chunk
|
||||||
*/
|
*/
|
||||||
private addMaskToChunk(chunk: MaskChunk, maskImage: HTMLImageElement, bounds: { x: number, y: number, width: number, height: number }): void {
|
private addMaskToChunk(chunk: MaskChunk, maskImage: HTMLImageElement, bounds: { x: number, y: number, width: number, height: number }): void {
|
||||||
// Calculate the intersection of the mask with this chunk
|
|
||||||
const chunkLeft = chunk.x;
|
|
||||||
const chunkTop = chunk.y;
|
|
||||||
const chunkRight = chunk.x + this.chunkSize;
|
|
||||||
const chunkBottom = chunk.y + this.chunkSize;
|
|
||||||
|
|
||||||
const maskLeft = bounds.x;
|
const maskLeft = bounds.x;
|
||||||
const maskTop = bounds.y;
|
const maskTop = bounds.y;
|
||||||
const maskRight = bounds.x + maskImage.width;
|
const maskRight = bounds.x + maskImage.width;
|
||||||
const maskBottom = bounds.y + maskImage.height;
|
const maskBottom = bounds.y + maskImage.height;
|
||||||
|
|
||||||
// Find intersection
|
const intersection = this.calculateChunkIntersection(chunk, maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const intersectLeft = Math.max(chunkLeft, maskLeft);
|
if (!intersection) {
|
||||||
const intersectTop = Math.max(chunkTop, maskTop);
|
|
||||||
const intersectRight = Math.min(chunkRight, maskRight);
|
|
||||||
const intersectBottom = Math.min(chunkBottom, maskBottom);
|
|
||||||
|
|
||||||
// Check if there's actually an intersection
|
|
||||||
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
|
||||||
return; // No intersection
|
return; // No intersection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate source coordinates on the mask image
|
|
||||||
const srcX = intersectLeft - maskLeft;
|
|
||||||
const srcY = intersectTop - maskTop;
|
|
||||||
const srcWidth = intersectRight - intersectLeft;
|
|
||||||
const srcHeight = intersectBottom - intersectTop;
|
|
||||||
|
|
||||||
// Calculate destination coordinates on the chunk
|
|
||||||
const destX = intersectLeft - chunkLeft;
|
|
||||||
const destY = intersectTop - chunkTop;
|
|
||||||
|
|
||||||
// Draw the mask portion onto this chunk
|
// Draw the mask portion onto this chunk
|
||||||
chunk.ctx.globalCompositeOperation = 'source-over';
|
chunk.ctx.globalCompositeOperation = 'source-over';
|
||||||
chunk.ctx.drawImage(
|
chunk.ctx.drawImage(
|
||||||
maskImage,
|
maskImage,
|
||||||
srcX, srcY, srcWidth, srcHeight, // Source rectangle
|
intersection.srcX, intersection.srcY, intersection.srcWidth, intersection.srcHeight, // Source rectangle
|
||||||
destX, destY, srcWidth, srcHeight // Destination rectangle
|
intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight // Destination rectangle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mark chunk as dirty and not empty
|
// Mark chunk as dirty and not empty
|
||||||
chunk.isDirty = true;
|
chunk.isDirty = true;
|
||||||
chunk.isEmpty = false;
|
chunk.isEmpty = false;
|
||||||
|
|
||||||
log.debug(`Added mask to chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${destX}, ${destY})`);
|
log.debug(`Added mask to chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${intersection.destX}, ${intersection.destY})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1450,115 +1505,60 @@ export class MaskTool {
|
|||||||
* Removes a mask canvas from a specific chunk using destination-out composition
|
* Removes a mask canvas from a specific chunk using destination-out composition
|
||||||
*/
|
*/
|
||||||
private removeMaskCanvasFromChunk(chunk: MaskChunk, maskCanvas: HTMLCanvasElement, maskWorldX: number, maskWorldY: number): void {
|
private removeMaskCanvasFromChunk(chunk: MaskChunk, maskCanvas: HTMLCanvasElement, maskWorldX: number, maskWorldY: number): void {
|
||||||
// Calculate the intersection of the mask with this chunk
|
|
||||||
const chunkLeft = chunk.x;
|
|
||||||
const chunkTop = chunk.y;
|
|
||||||
const chunkRight = chunk.x + this.chunkSize;
|
|
||||||
const chunkBottom = chunk.y + this.chunkSize;
|
|
||||||
|
|
||||||
const maskLeft = maskWorldX;
|
const maskLeft = maskWorldX;
|
||||||
const maskTop = maskWorldY;
|
const maskTop = maskWorldY;
|
||||||
const maskRight = maskWorldX + maskCanvas.width;
|
const maskRight = maskWorldX + maskCanvas.width;
|
||||||
const maskBottom = maskWorldY + maskCanvas.height;
|
const maskBottom = maskWorldY + maskCanvas.height;
|
||||||
|
|
||||||
// Find intersection
|
const intersection = this.calculateChunkIntersection(chunk, maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const intersectLeft = Math.max(chunkLeft, maskLeft);
|
if (!intersection) {
|
||||||
const intersectTop = Math.max(chunkTop, maskTop);
|
|
||||||
const intersectRight = Math.min(chunkRight, maskRight);
|
|
||||||
const intersectBottom = Math.min(chunkBottom, maskBottom);
|
|
||||||
|
|
||||||
// Check if there's actually an intersection
|
|
||||||
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
|
||||||
return; // No intersection
|
return; // No intersection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate source coordinates on the mask canvas
|
|
||||||
const srcX = intersectLeft - maskLeft;
|
|
||||||
const srcY = intersectTop - maskTop;
|
|
||||||
const srcWidth = intersectRight - intersectLeft;
|
|
||||||
const srcHeight = intersectBottom - intersectTop;
|
|
||||||
|
|
||||||
// Calculate destination coordinates on the chunk
|
|
||||||
const destX = intersectLeft - chunkLeft;
|
|
||||||
const destY = intersectTop - chunkTop;
|
|
||||||
|
|
||||||
// Use destination-out to remove the mask portion from this chunk
|
// Use destination-out to remove the mask portion from this chunk
|
||||||
chunk.ctx.globalCompositeOperation = 'destination-out';
|
chunk.ctx.globalCompositeOperation = 'destination-out';
|
||||||
chunk.ctx.drawImage(
|
chunk.ctx.drawImage(
|
||||||
maskCanvas,
|
maskCanvas,
|
||||||
srcX, srcY, srcWidth, srcHeight, // Source rectangle
|
intersection.srcX, intersection.srcY, intersection.srcWidth, intersection.srcHeight, // Source rectangle
|
||||||
destX, destY, srcWidth, srcHeight // Destination rectangle
|
intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight // Destination rectangle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restore normal composition mode
|
// Restore normal composition mode
|
||||||
chunk.ctx.globalCompositeOperation = 'source-over';
|
chunk.ctx.globalCompositeOperation = 'source-over';
|
||||||
|
|
||||||
// Check if the chunk is now empty
|
// Update chunk empty status
|
||||||
const imageData = chunk.ctx.getImageData(0, 0, this.chunkSize, this.chunkSize);
|
this.updateChunkEmptyStatus(chunk);
|
||||||
const data = imageData.data;
|
|
||||||
let hasData = false;
|
|
||||||
for (let i = 3; i < data.length; i += 4) { // Check alpha channel
|
|
||||||
if (data[i] > 0) {
|
|
||||||
hasData = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk.isEmpty = !hasData;
|
log.debug(`Removed mask canvas from chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${intersection.destX}, ${intersection.destY})`);
|
||||||
chunk.isDirty = true;
|
|
||||||
|
|
||||||
log.debug(`Removed mask canvas from chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${destX}, ${destY})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a mask canvas to a specific chunk
|
* Applies a mask canvas to a specific chunk
|
||||||
*/
|
*/
|
||||||
private applyMaskCanvasToChunk(chunk: MaskChunk, maskCanvas: HTMLCanvasElement, maskWorldX: number, maskWorldY: number): void {
|
private applyMaskCanvasToChunk(chunk: MaskChunk, maskCanvas: HTMLCanvasElement, maskWorldX: number, maskWorldY: number): void {
|
||||||
// Calculate the intersection of the mask with this chunk
|
|
||||||
const chunkLeft = chunk.x;
|
|
||||||
const chunkTop = chunk.y;
|
|
||||||
const chunkRight = chunk.x + this.chunkSize;
|
|
||||||
const chunkBottom = chunk.y + this.chunkSize;
|
|
||||||
|
|
||||||
const maskLeft = maskWorldX;
|
const maskLeft = maskWorldX;
|
||||||
const maskTop = maskWorldY;
|
const maskTop = maskWorldY;
|
||||||
const maskRight = maskWorldX + maskCanvas.width;
|
const maskRight = maskWorldX + maskCanvas.width;
|
||||||
const maskBottom = maskWorldY + maskCanvas.height;
|
const maskBottom = maskWorldY + maskCanvas.height;
|
||||||
|
|
||||||
// Find intersection
|
const intersection = this.calculateChunkIntersection(chunk, maskLeft, maskTop, maskRight, maskBottom);
|
||||||
const intersectLeft = Math.max(chunkLeft, maskLeft);
|
if (!intersection) {
|
||||||
const intersectTop = Math.max(chunkTop, maskTop);
|
|
||||||
const intersectRight = Math.min(chunkRight, maskRight);
|
|
||||||
const intersectBottom = Math.min(chunkBottom, maskBottom);
|
|
||||||
|
|
||||||
// Check if there's actually an intersection
|
|
||||||
if (intersectLeft >= intersectRight || intersectTop >= intersectBottom) {
|
|
||||||
return; // No intersection
|
return; // No intersection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate source coordinates on the mask canvas
|
|
||||||
const srcX = intersectLeft - maskLeft;
|
|
||||||
const srcY = intersectTop - maskTop;
|
|
||||||
const srcWidth = intersectRight - intersectLeft;
|
|
||||||
const srcHeight = intersectBottom - intersectTop;
|
|
||||||
|
|
||||||
// Calculate destination coordinates on the chunk
|
|
||||||
const destX = intersectLeft - chunkLeft;
|
|
||||||
const destY = intersectTop - chunkTop;
|
|
||||||
|
|
||||||
// Draw the mask portion onto this chunk
|
// Draw the mask portion onto this chunk
|
||||||
chunk.ctx.globalCompositeOperation = 'source-over';
|
chunk.ctx.globalCompositeOperation = 'source-over';
|
||||||
chunk.ctx.drawImage(
|
chunk.ctx.drawImage(
|
||||||
maskCanvas,
|
maskCanvas,
|
||||||
srcX, srcY, srcWidth, srcHeight, // Source rectangle
|
intersection.srcX, intersection.srcY, intersection.srcWidth, intersection.srcHeight, // Source rectangle
|
||||||
destX, destY, srcWidth, srcHeight // Destination rectangle
|
intersection.destX, intersection.destY, intersection.destWidth, intersection.destHeight // Destination rectangle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mark chunk as dirty and not empty
|
// Mark chunk as dirty and not empty
|
||||||
chunk.isDirty = true;
|
chunk.isDirty = true;
|
||||||
chunk.isEmpty = false;
|
chunk.isEmpty = false;
|
||||||
|
|
||||||
log.debug(`Applied mask canvas to chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${destX}, ${destY})`);
|
log.debug(`Applied mask canvas to chunk (${Math.floor(chunk.x / this.chunkSize)}, ${Math.floor(chunk.y / this.chunkSize)}) at local position (${intersection.destX}, ${intersection.destY})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyShapeMask(saveState: boolean = true): void {
|
applyShapeMask(saveState: boolean = true): void {
|
||||||
@@ -1573,11 +1573,15 @@ export class MaskTool {
|
|||||||
const shape = this.canvasInstance.outputAreaShape;
|
const shape = this.canvasInstance.outputAreaShape;
|
||||||
const bounds = this.canvasInstance.outputAreaBounds;
|
const bounds = this.canvasInstance.outputAreaBounds;
|
||||||
|
|
||||||
// Calculate shape points in world coordinates
|
// Calculate shape points in world coordinates accounting for extensions
|
||||||
// Shape points are relative to the output area bounds
|
// Shape points are relative to the output area bounds, but need extension offset
|
||||||
|
const ext = this.canvasInstance.outputAreaExtensionEnabled ? this.canvasInstance.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
const shapeOffsetX = ext.left; // Add left extension to maintain relative position
|
||||||
|
const shapeOffsetY = ext.top; // Add top extension to maintain relative position
|
||||||
|
|
||||||
const worldShapePoints = shape.points.map(p => ({
|
const worldShapePoints = shape.points.map(p => ({
|
||||||
x: bounds.x + p.x,
|
x: bounds.x + shapeOffsetX + p.x,
|
||||||
y: bounds.y + p.y
|
y: bounds.y + shapeOffsetY + p.y
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create the shape mask canvas
|
// Create the shape mask canvas
|
||||||
@@ -1629,17 +1633,29 @@ export class MaskTool {
|
|||||||
shapeMaskCanvas = this._createFeatheredMaskFromImageData(expandedImageData, this.canvasInstance.shapeMaskFeatherValue, tempCanvasWidth, tempCanvasHeight);
|
shapeMaskCanvas = this._createFeatheredMaskFromImageData(expandedImageData, this.canvasInstance.shapeMaskFeatherValue, tempCanvasWidth, tempCanvasHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now apply the shape mask to the chunked system
|
// Calculate which chunks will be affected by the shape mask
|
||||||
this.applyMaskCanvasToChunks(shapeMaskCanvas, bounds.x - tempOffsetX, bounds.y - tempOffsetY);
|
const maskWorldX = bounds.x - tempOffsetX;
|
||||||
|
const maskWorldY = bounds.y - tempOffsetY;
|
||||||
|
const maskLeft = maskWorldX;
|
||||||
|
const maskTop = maskWorldY;
|
||||||
|
const maskRight = maskWorldX + shapeMaskCanvas.width;
|
||||||
|
const maskBottom = maskWorldY + shapeMaskCanvas.height;
|
||||||
|
|
||||||
// Update the active mask canvas to show the changes
|
// Apply the shape mask to the chunked system
|
||||||
this.updateActiveMaskCanvas();
|
this.applyMaskCanvasToChunks(shapeMaskCanvas, maskWorldX, maskWorldY);
|
||||||
|
|
||||||
|
// Activate chunks in the area for visibility
|
||||||
|
const activatedChunks = this.activateChunksInArea(maskLeft, maskTop, maskRight, maskBottom);
|
||||||
|
|
||||||
|
// Update the active mask canvas to show the changes with activated chunks
|
||||||
|
this.updateActiveMaskCanvas(true); // Force full update to show all chunks including newly activated ones
|
||||||
|
|
||||||
if (this.onStateChange) {
|
if (this.onStateChange) {
|
||||||
this.onStateChange();
|
this.onStateChange();
|
||||||
}
|
}
|
||||||
this.canvasInstance.render();
|
this.canvasInstance.render();
|
||||||
log.info(`Applied shape mask to chunks with expansion: ${needsExpansion}, feather: ${needsFeather}.`);
|
|
||||||
|
log.info(`Applied shape mask to chunks with expansion: ${needsExpansion}, feather: ${needsFeather} and activated ${activatedChunks} chunks for visibility`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1658,10 +1674,14 @@ export class MaskTool {
|
|||||||
const shape = this.canvasInstance.outputAreaShape;
|
const shape = this.canvasInstance.outputAreaShape;
|
||||||
const bounds = this.canvasInstance.outputAreaBounds;
|
const bounds = this.canvasInstance.outputAreaBounds;
|
||||||
|
|
||||||
// Calculate shape points in world coordinates (same as applyShapeMask)
|
// Calculate shape points in world coordinates accounting for extensions (same as applyShapeMask)
|
||||||
|
const ext = this.canvasInstance.outputAreaExtensionEnabled ? this.canvasInstance.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
const shapeOffsetX = ext.left; // Add left extension to maintain relative position
|
||||||
|
const shapeOffsetY = ext.top; // Add top extension to maintain relative position
|
||||||
|
|
||||||
const worldShapePoints = shape.points.map(p => ({
|
const worldShapePoints = shape.points.map(p => ({
|
||||||
x: bounds.x + p.x,
|
x: bounds.x + shapeOffsetX + p.x,
|
||||||
y: bounds.y + p.y
|
y: bounds.y + shapeOffsetY + p.y
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Check if we need to account for expansion when removing
|
// Check if we need to account for expansion when removing
|
||||||
|
|||||||
Reference in New Issue
Block a user