mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Refine shape mask UI and ensure hard-edged mask removal
Consolidates shape mask controls into a styled container, improves UI logic for showing/hiding sub-options, and updates slider initialization to reflect current values. Mask removal now always uses a hard-edged shape, even if feathering was previously applied, ensuring complete erasure of feathered areas. The mask application logic also clears the maximum possible mask area before applying the new mask to prevent artifacts from previous slider values. Checkbox and slider labels are updated for clarity.
This commit is contained in:
@@ -63,9 +63,7 @@ export class CustomShapeMenu {
|
||||
`;
|
||||
// Create menu content
|
||||
const lines = [
|
||||
"🎯 Custom Output Area Active",
|
||||
"Press Shift+S to modify shape",
|
||||
"Shape defines generation area"
|
||||
"🎯 Custom Output Area Active"
|
||||
];
|
||||
lines.forEach(line => {
|
||||
const lineElement = document.createElement('div');
|
||||
@@ -76,7 +74,17 @@ export class CustomShapeMenu {
|
||||
`;
|
||||
this.element.appendChild(lineElement);
|
||||
});
|
||||
// Add main auto-apply checkbox
|
||||
// Create a container for the entire shape mask feature set
|
||||
const featureContainer = document.createElement('div');
|
||||
featureContainer.id = 'shape-mask-feature-container';
|
||||
featureContainer.style.cssText = `
|
||||
background-color: #282828;
|
||||
border-radius: 6px;
|
||||
margin-top: 6px;
|
||||
padding: 4px 0;
|
||||
border: 1px solid #444;
|
||||
`;
|
||||
// Add main auto-apply checkbox to the new container
|
||||
const checkboxContainer = this._createCheckbox(() => `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`, () => {
|
||||
this.canvas.autoApplyShapeMask = !this.canvas.autoApplyShapeMask;
|
||||
if (this.canvas.autoApplyShapeMask) {
|
||||
@@ -85,13 +93,15 @@ export class CustomShapeMenu {
|
||||
}
|
||||
else {
|
||||
this.canvas.maskTool.removeShapeMask();
|
||||
log.info("Auto-apply shape mask disabled - mask removed automatically");
|
||||
this.canvas.shapeMaskExpansion = false;
|
||||
this.canvas.shapeMaskFeather = false;
|
||||
log.info("Auto-apply shape mask disabled - mask area removed and sub-options reset.");
|
||||
}
|
||||
this._updateUI();
|
||||
this.canvas.render();
|
||||
});
|
||||
this.element.appendChild(checkboxContainer);
|
||||
// Add expansion checkbox (only visible when auto-apply is enabled)
|
||||
featureContainer.appendChild(checkboxContainer);
|
||||
// Add expansion checkbox
|
||||
const expansionContainer = this._createCheckbox(() => `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`, () => {
|
||||
this.canvas.shapeMaskExpansion = !this.canvas.shapeMaskExpansion;
|
||||
this._updateUI();
|
||||
@@ -101,12 +111,12 @@ export class CustomShapeMenu {
|
||||
}
|
||||
});
|
||||
expansionContainer.id = 'expansion-checkbox';
|
||||
this.element.appendChild(expansionContainer);
|
||||
// Add expansion slider container (only visible when expansion is enabled)
|
||||
featureContainer.appendChild(expansionContainer);
|
||||
// Add expansion slider container
|
||||
const expansionSliderContainer = document.createElement('div');
|
||||
expansionSliderContainer.id = 'expansion-slider-container';
|
||||
expansionSliderContainer.style.cssText = `
|
||||
margin: 6px 0;
|
||||
margin: 0 8px 6px 8px;
|
||||
padding: 4px 8px;
|
||||
display: none;
|
||||
`;
|
||||
@@ -121,7 +131,7 @@ export class CustomShapeMenu {
|
||||
expansionSlider.type = 'range';
|
||||
expansionSlider.min = '-300';
|
||||
expansionSlider.max = '300';
|
||||
expansionSlider.value = '0';
|
||||
expansionSlider.value = String(this.canvas.shapeMaskExpansionValue);
|
||||
expansionSlider.style.cssText = `
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
@@ -141,41 +151,25 @@ export class CustomShapeMenu {
|
||||
this.canvas.shapeMaskExpansionValue = value;
|
||||
expansionValueDisplay.textContent = value > 0 ? `+${value}px` : `${value}px`;
|
||||
};
|
||||
// Add preview system for expansion slider
|
||||
let expansionTimeout = null;
|
||||
let isExpansionDragging = false;
|
||||
expansionSlider.onmousedown = () => {
|
||||
isExpansionDragging = true;
|
||||
};
|
||||
expansionSlider.onmousedown = () => { isExpansionDragging = true; };
|
||||
expansionSlider.oninput = () => {
|
||||
updateExpansionSliderDisplay();
|
||||
if (this.canvas.autoApplyShapeMask) {
|
||||
// Clear previous timeout
|
||||
if (expansionTimeout) {
|
||||
clearTimeout(expansionTimeout);
|
||||
}
|
||||
if (isExpansionDragging) {
|
||||
// Show blue preview line while dragging - NO mask application
|
||||
const featherValue = this.canvas.shapeMaskFeather ? this.canvas.shapeMaskFeatherValue : 0;
|
||||
this.canvas.maskTool.showShapePreview(this.canvas.shapeMaskExpansionValue, featherValue);
|
||||
}
|
||||
else {
|
||||
// Apply mask immediately for programmatic changes (not user dragging)
|
||||
this.canvas.maskTool.hideShapePreview();
|
||||
this.canvas.maskTool.applyShapeMask(false);
|
||||
this.canvas.render();
|
||||
}
|
||||
// Clear any pending timeout - we only apply mask on mouseup now
|
||||
if (expansionTimeout) {
|
||||
clearTimeout(expansionTimeout);
|
||||
expansionTimeout = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
expansionSlider.onmouseup = () => {
|
||||
isExpansionDragging = false;
|
||||
if (this.canvas.autoApplyShapeMask) {
|
||||
// Apply final mask immediately when user releases slider
|
||||
this.canvas.maskTool.hideShapePreview();
|
||||
this.canvas.maskTool.applyShapeMask(true);
|
||||
this.canvas.render();
|
||||
@@ -185,8 +179,8 @@ export class CustomShapeMenu {
|
||||
expansionSliderContainer.appendChild(expansionSliderLabel);
|
||||
expansionSliderContainer.appendChild(expansionSlider);
|
||||
expansionSliderContainer.appendChild(expansionValueDisplay);
|
||||
this.element.appendChild(expansionSliderContainer);
|
||||
// Add feather checkbox (only visible when auto-apply is enabled)
|
||||
featureContainer.appendChild(expansionSliderContainer);
|
||||
// Add feather checkbox
|
||||
const featherContainer = this._createCheckbox(() => `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`, () => {
|
||||
this.canvas.shapeMaskFeather = !this.canvas.shapeMaskFeather;
|
||||
this._updateUI();
|
||||
@@ -196,12 +190,12 @@ export class CustomShapeMenu {
|
||||
}
|
||||
});
|
||||
featherContainer.id = 'feather-checkbox';
|
||||
this.element.appendChild(featherContainer);
|
||||
// Add feather slider container (only visible when feather is enabled)
|
||||
featureContainer.appendChild(featherContainer);
|
||||
// Add feather slider container
|
||||
const featherSliderContainer = document.createElement('div');
|
||||
featherSliderContainer.id = 'feather-slider-container';
|
||||
featherSliderContainer.style.cssText = `
|
||||
margin: 6px 0;
|
||||
margin: 0 8px 6px 8px;
|
||||
padding: 4px 8px;
|
||||
display: none;
|
||||
`;
|
||||
@@ -216,7 +210,7 @@ export class CustomShapeMenu {
|
||||
featherSlider.type = 'range';
|
||||
featherSlider.min = '0';
|
||||
featherSlider.max = '300';
|
||||
featherSlider.value = '0';
|
||||
featherSlider.value = String(this.canvas.shapeMaskFeatherValue);
|
||||
featherSlider.style.cssText = `
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
@@ -236,22 +230,16 @@ export class CustomShapeMenu {
|
||||
this.canvas.shapeMaskFeatherValue = value;
|
||||
featherValueDisplay.textContent = `${value}px`;
|
||||
};
|
||||
// Add preview system for feather slider (mirrors expansion slider)
|
||||
let featherTimeout = null;
|
||||
let isFeatherDragging = false;
|
||||
featherSlider.onmousedown = () => {
|
||||
isFeatherDragging = true;
|
||||
};
|
||||
featherSlider.onmousedown = () => { isFeatherDragging = true; };
|
||||
featherSlider.oninput = () => {
|
||||
updateFeatherSliderDisplay();
|
||||
if (this.canvas.autoApplyShapeMask) {
|
||||
if (isFeatherDragging) {
|
||||
// Show blue preview line while dragging
|
||||
const expansionValue = this.canvas.shapeMaskExpansion ? this.canvas.shapeMaskExpansionValue : 0;
|
||||
this.canvas.maskTool.showShapePreview(expansionValue, this.canvas.shapeMaskFeatherValue);
|
||||
}
|
||||
else {
|
||||
// Apply immediately for programmatic changes
|
||||
this.canvas.maskTool.hideShapePreview();
|
||||
this.canvas.maskTool.applyShapeMask(false);
|
||||
this.canvas.render();
|
||||
@@ -261,7 +249,6 @@ export class CustomShapeMenu {
|
||||
featherSlider.onmouseup = () => {
|
||||
isFeatherDragging = false;
|
||||
if (this.canvas.autoApplyShapeMask) {
|
||||
// Apply final mask when user releases slider
|
||||
this.canvas.maskTool.hideShapePreview();
|
||||
this.canvas.maskTool.applyShapeMask(true); // true = save state
|
||||
this.canvas.render();
|
||||
@@ -271,7 +258,8 @@ export class CustomShapeMenu {
|
||||
featherSliderContainer.appendChild(featherSliderLabel);
|
||||
featherSliderContainer.appendChild(featherSlider);
|
||||
featherSliderContainer.appendChild(featherValueDisplay);
|
||||
this.element.appendChild(featherSliderContainer);
|
||||
featureContainer.appendChild(featherSliderContainer);
|
||||
this.element.appendChild(featureContainer);
|
||||
// Add to DOM
|
||||
if (this.canvas.canvas.parentElement) {
|
||||
this.canvas.canvas.parentElement.appendChild(this.element);
|
||||
@@ -315,27 +303,23 @@ export class CustomShapeMenu {
|
||||
_updateUI() {
|
||||
if (!this.element)
|
||||
return;
|
||||
// Update expansion checkbox visibility
|
||||
// Toggle visibility of sub-options based on the main checkbox state
|
||||
const expansionCheckbox = this.element.querySelector('#expansion-checkbox');
|
||||
if (expansionCheckbox) {
|
||||
expansionCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'block' : 'none';
|
||||
}
|
||||
// Update expansion slider container visibility
|
||||
const expansionSliderContainer = this.element.querySelector('#expansion-slider-container');
|
||||
if (expansionSliderContainer) {
|
||||
expansionSliderContainer.style.display =
|
||||
(this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none';
|
||||
}
|
||||
// Update feather checkbox visibility
|
||||
const featherCheckbox = this.element.querySelector('#feather-checkbox');
|
||||
if (featherCheckbox) {
|
||||
featherCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'block' : 'none';
|
||||
}
|
||||
// Update feather slider container visibility
|
||||
// Update sliders visibility based on their respective checkboxes
|
||||
const expansionSliderContainer = this.element.querySelector('#expansion-slider-container');
|
||||
if (expansionSliderContainer) {
|
||||
expansionSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none';
|
||||
}
|
||||
const featherSliderContainer = this.element.querySelector('#feather-slider-container');
|
||||
if (featherSliderContainer) {
|
||||
featherSliderContainer.style.display =
|
||||
(this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none';
|
||||
featherSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none';
|
||||
}
|
||||
// Update checkbox texts
|
||||
const checkboxes = this.element.querySelectorAll('div[style*="cursor: pointer"]');
|
||||
@@ -344,7 +328,7 @@ export class CustomShapeMenu {
|
||||
checkbox.textContent = `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`;
|
||||
}
|
||||
else if (index === 1) { // Expansion checkbox
|
||||
checkbox.textContent = `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`;
|
||||
checkbox.textContent = `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Dilate/Erode mask`;
|
||||
}
|
||||
else if (index === 2) { // Feather checkbox
|
||||
checkbox.textContent = `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`;
|
||||
|
||||
@@ -620,10 +620,17 @@ export class MaskTool {
|
||||
const shape = this.canvasInstance.outputAreaShape;
|
||||
const destX = -this.x;
|
||||
const destY = -this.y;
|
||||
// Clear the entire mask canvas first
|
||||
this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||
// Create points relative to the mask canvas's coordinate system (by applying the offset)
|
||||
const maskPoints = shape.points.map(p => ({ x: p.x + destX, y: p.y + destY }));
|
||||
// --- Clear Previous State ---
|
||||
// To prevent artifacts from previous slider values, we first clear the maximum
|
||||
// possible area the shape could have occupied.
|
||||
const maxExpansion = 300; // The maximum value of the expansion slider
|
||||
const clearingMaskCanvas = this._createExpandedMaskCanvas(maskPoints, maxExpansion, this.maskCanvas.width, this.maskCanvas.height);
|
||||
this.maskCtx.globalCompositeOperation = 'destination-out';
|
||||
this.maskCtx.drawImage(clearingMaskCanvas, 0, 0);
|
||||
// --- Apply Current State ---
|
||||
// Now, apply the new, correct mask additively.
|
||||
this.maskCtx.globalCompositeOperation = 'source-over';
|
||||
// Check if we need expansion or feathering
|
||||
const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0;
|
||||
const needsFeather = this.canvasInstance.shapeMaskFeather && this.canvasInstance.shapeMaskFeatherValue > 0;
|
||||
@@ -668,7 +675,8 @@ export class MaskTool {
|
||||
log.info(`Applied shape mask with expansion: ${needsExpansion}, feather: ${needsFeather}.`);
|
||||
}
|
||||
/**
|
||||
* Removes mask in the area of the custom output area shape
|
||||
* Removes mask in the area of the custom output area shape. This must use a hard-edged
|
||||
* shape to correctly erase any feathered "glow" that might have been applied.
|
||||
*/
|
||||
removeShapeMask() {
|
||||
if (!this.canvasInstance.outputAreaShape?.points || this.canvasInstance.outputAreaShape.points.length < 3) {
|
||||
@@ -679,22 +687,34 @@ export class MaskTool {
|
||||
const shape = this.canvasInstance.outputAreaShape;
|
||||
const destX = -this.x;
|
||||
const destY = -this.y;
|
||||
this.maskCtx.save();
|
||||
// Use 'destination-out' to erase the shape area
|
||||
this.maskCtx.globalCompositeOperation = 'destination-out';
|
||||
this.maskCtx.translate(destX, destY);
|
||||
this.maskCtx.beginPath();
|
||||
this.maskCtx.moveTo(shape.points[0].x, shape.points[0].y);
|
||||
for (let i = 1; i < shape.points.length; i++) {
|
||||
this.maskCtx.lineTo(shape.points[i].x, shape.points[i].y);
|
||||
const maskPoints = shape.points.map(p => ({ x: p.x + destX, y: p.y + destY }));
|
||||
const needsExpansion = this.canvasInstance.shapeMaskExpansion && this.canvasInstance.shapeMaskExpansionValue !== 0;
|
||||
// IMPORTANT: Removal should always be hard-edged, even if feather was on.
|
||||
// This ensures the feathered "glow" is completely removed. We only care about expansion.
|
||||
if (needsExpansion) {
|
||||
// If expansion was active, remove the expanded area with a hard edge.
|
||||
const expandedMaskCanvas = this._createExpandedMaskCanvas(maskPoints, this.canvasInstance.shapeMaskExpansionValue, this.maskCanvas.width, this.maskCanvas.height);
|
||||
this.maskCtx.drawImage(expandedMaskCanvas, 0, 0);
|
||||
}
|
||||
this.maskCtx.closePath();
|
||||
this.maskCtx.fill();
|
||||
this.maskCtx.restore();
|
||||
else {
|
||||
// If no expansion, just remove the base shape with a hard edge.
|
||||
this.maskCtx.beginPath();
|
||||
this.maskCtx.moveTo(maskPoints[0].x, maskPoints[0].y);
|
||||
for (let i = 1; i < maskPoints.length; i++) {
|
||||
this.maskCtx.lineTo(maskPoints[i].x, maskPoints[i].y);
|
||||
}
|
||||
this.maskCtx.closePath();
|
||||
this.maskCtx.fill('evenodd');
|
||||
}
|
||||
// Restore default composite operation
|
||||
this.maskCtx.globalCompositeOperation = 'source-over';
|
||||
if (this.onStateChange) {
|
||||
this.onStateChange();
|
||||
}
|
||||
this.canvasInstance.render();
|
||||
log.info(`Removed shape mask with ${shape.points.length} points`);
|
||||
log.info(`Removed shape mask area (hard-edged) with expansion: ${needsExpansion}.`);
|
||||
}
|
||||
_createFeatheredMaskCanvas(points, featherRadius, width, height) {
|
||||
// 1. Create a binary mask on a temporary canvas.
|
||||
|
||||
Reference in New Issue
Block a user