Files
Comfyui-LayerForge/js/CustomShapeMenu.js
Dariusz L 24ef702f16 Add custom shape mask menu with expansion and feathering
Introduces a CustomShapeMenu UI component for managing custom output area shape masks, including options for auto-applying the mask, expansion/contraction, and feathering. Updates Canvas and MaskTool to support these new mask operations, and ensures the menu is shown or hidden based on shape presence. Adds distance transform-based algorithms for accurate mask expansion and feathering.
2025-07-25 18:40:21 +02:00

318 lines
13 KiB
JavaScript

import { createModuleLogger } from "./utils/LoggerUtils.js";
const log = createModuleLogger('CustomShapeMenu');
export class CustomShapeMenu {
constructor(canvas) {
this.canvas = canvas;
this.element = null;
this.worldX = 0;
this.worldY = 0;
this.uiInitialized = false;
}
show() {
if (!this.canvas.outputAreaShape) {
return;
}
this._createUI();
if (this.element) {
this.element.style.display = 'block';
}
// Position in top-left corner of viewport (closer to edge)
const viewLeft = this.canvas.viewport.x;
const viewTop = this.canvas.viewport.y;
this.worldX = viewLeft + (8 / this.canvas.viewport.zoom);
this.worldY = viewTop + (8 / this.canvas.viewport.zoom);
this.updateScreenPosition();
}
hide() {
if (this.element) {
this.element.remove();
this.element = null;
this.uiInitialized = false;
}
}
updateScreenPosition() {
if (!this.element)
return;
const screenX = (this.worldX - this.canvas.viewport.x) * this.canvas.viewport.zoom;
const screenY = (this.worldY - this.canvas.viewport.y) * this.canvas.viewport.zoom;
this.element.style.transform = `translate(${screenX}px, ${screenY}px)`;
}
_createUI() {
if (this.uiInitialized)
return;
this.element = document.createElement('div');
this.element.id = 'layerforge-custom-shape-menu';
this.element.style.cssText = `
position: absolute;
top: 0;
left: 0;
background-color: #333;
color: white;
padding: 8px 15px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
display: none;
flex-direction: column;
gap: 4px;
font-family: sans-serif;
font-size: 12px;
z-index: 1001;
border: 1px solid #555;
user-select: none;
min-width: 200px;
`;
// Create menu content
const lines = [
"🎯 Custom Output Area Active",
"Press Shift+S to modify shape",
"Shape defines generation area"
];
lines.forEach(line => {
const lineElement = document.createElement('div');
lineElement.textContent = line;
lineElement.style.cssText = `
margin: 2px 0;
line-height: 18px;
`;
this.element.appendChild(lineElement);
});
// Add main auto-apply checkbox
const checkboxContainer = this._createCheckbox(() => `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`, () => {
this.canvas.autoApplyShapeMask = !this.canvas.autoApplyShapeMask;
if (this.canvas.autoApplyShapeMask) {
this.canvas.maskTool.applyShapeMask();
log.info("Auto-apply shape mask enabled - mask applied automatically");
}
else {
this.canvas.maskTool.removeShapeMask();
log.info("Auto-apply shape mask disabled - mask removed automatically");
}
this._updateUI();
this.canvas.render();
});
this.element.appendChild(checkboxContainer);
// Add expansion checkbox (only visible when auto-apply is enabled)
const expansionContainer = this._createCheckbox(() => `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`, () => {
this.canvas.shapeMaskExpansion = !this.canvas.shapeMaskExpansion;
this._updateUI();
if (this.canvas.autoApplyShapeMask) {
this.canvas.maskTool.applyShapeMask();
this.canvas.render();
}
});
expansionContainer.id = 'expansion-checkbox';
this.element.appendChild(expansionContainer);
// Add expansion slider container (only visible when expansion is enabled)
const expansionSliderContainer = document.createElement('div');
expansionSliderContainer.id = 'expansion-slider-container';
expansionSliderContainer.style.cssText = `
margin: 6px 0;
padding: 4px 8px;
display: none;
`;
const expansionSliderLabel = document.createElement('div');
expansionSliderLabel.textContent = 'Expansion amount:';
expansionSliderLabel.style.cssText = `
font-size: 11px;
margin-bottom: 4px;
color: #ccc;
`;
const expansionSlider = document.createElement('input');
expansionSlider.type = 'range';
expansionSlider.min = '-300';
expansionSlider.max = '300';
expansionSlider.value = '0';
expansionSlider.style.cssText = `
width: 100%;
height: 4px;
background: #555;
outline: none;
border-radius: 2px;
`;
const expansionValueDisplay = document.createElement('div');
expansionValueDisplay.style.cssText = `
font-size: 10px;
text-align: center;
margin-top: 2px;
color: #aaa;
`;
const updateExpansionSliderDisplay = () => {
const value = parseInt(expansionSlider.value);
this.canvas.shapeMaskExpansionValue = value;
expansionValueDisplay.textContent = value > 0 ? `+${value}px` : `${value}px`;
};
// Add debouncing for expansion slider
let expansionTimeout = null;
expansionSlider.oninput = () => {
updateExpansionSliderDisplay();
if (this.canvas.autoApplyShapeMask) {
// Clear previous timeout
if (expansionTimeout) {
clearTimeout(expansionTimeout);
}
// Apply mask immediately for visual feedback (without saving state)
this.canvas.maskTool.applyShapeMask(false); // false = don't save state
this.canvas.render();
// Save state after 500ms of no changes
expansionTimeout = window.setTimeout(() => {
this.canvas.canvasState.saveMaskState();
}, 500);
}
};
updateExpansionSliderDisplay();
expansionSliderContainer.appendChild(expansionSliderLabel);
expansionSliderContainer.appendChild(expansionSlider);
expansionSliderContainer.appendChild(expansionValueDisplay);
this.element.appendChild(expansionSliderContainer);
// Add feather checkbox (only visible when auto-apply is enabled)
const featherContainer = this._createCheckbox(() => `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`, () => {
this.canvas.shapeMaskFeather = !this.canvas.shapeMaskFeather;
this._updateUI();
if (this.canvas.autoApplyShapeMask) {
this.canvas.maskTool.applyShapeMask();
this.canvas.render();
}
});
featherContainer.id = 'feather-checkbox';
this.element.appendChild(featherContainer);
// Add feather slider container (only visible when feather is enabled)
const featherSliderContainer = document.createElement('div');
featherSliderContainer.id = 'feather-slider-container';
featherSliderContainer.style.cssText = `
margin: 6px 0;
padding: 4px 8px;
display: none;
`;
const featherSliderLabel = document.createElement('div');
featherSliderLabel.textContent = 'Feather amount:';
featherSliderLabel.style.cssText = `
font-size: 11px;
margin-bottom: 4px;
color: #ccc;
`;
const featherSlider = document.createElement('input');
featherSlider.type = 'range';
featherSlider.min = '0';
featherSlider.max = '300';
featherSlider.value = '0';
featherSlider.style.cssText = `
width: 100%;
height: 4px;
background: #555;
outline: none;
border-radius: 2px;
`;
const featherValueDisplay = document.createElement('div');
featherValueDisplay.style.cssText = `
font-size: 10px;
text-align: center;
margin-top: 2px;
color: #aaa;
`;
const updateFeatherSliderDisplay = () => {
const value = parseInt(featherSlider.value);
this.canvas.shapeMaskFeatherValue = value;
featherValueDisplay.textContent = `${value}px`;
};
// Add debouncing for feather slider
let featherTimeout = null;
featherSlider.oninput = () => {
updateFeatherSliderDisplay();
if (this.canvas.autoApplyShapeMask) {
// Clear previous timeout
if (featherTimeout) {
clearTimeout(featherTimeout);
}
// Apply mask immediately for visual feedback (without saving state)
this.canvas.maskTool.applyShapeMask(false); // false = don't save state
this.canvas.render();
// Save state after 500ms of no changes
featherTimeout = window.setTimeout(() => {
this.canvas.canvasState.saveMaskState();
}, 500);
}
};
updateFeatherSliderDisplay();
featherSliderContainer.appendChild(featherSliderLabel);
featherSliderContainer.appendChild(featherSlider);
featherSliderContainer.appendChild(featherValueDisplay);
this.element.appendChild(featherSliderContainer);
// Add to DOM
if (this.canvas.canvas.parentElement) {
this.canvas.canvas.parentElement.appendChild(this.element);
}
else {
log.error("Could not find parent node to attach custom shape menu.");
}
this.uiInitialized = true;
this._updateUI();
}
_createCheckbox(textFn, clickHandler) {
const container = document.createElement('div');
container.style.cssText = `
margin: 6px 0 2px 0;
padding: 4px 8px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
line-height: 18px;
`;
container.onmouseover = () => {
container.style.backgroundColor = '#555';
};
container.onmouseout = () => {
container.style.backgroundColor = 'transparent';
};
const updateText = () => {
container.textContent = textFn();
};
updateText();
container.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
clickHandler();
updateText();
};
return container;
}
_updateUI() {
if (!this.element)
return;
// Update expansion checkbox visibility
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
const featherSliderContainer = this.element.querySelector('#feather-slider-container');
if (featherSliderContainer) {
featherSliderContainer.style.display =
(this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none';
}
// Update checkbox texts
const checkboxes = this.element.querySelectorAll('div[style*="cursor: pointer"]');
checkboxes.forEach((checkbox, index) => {
if (index === 0) { // Main checkbox
checkbox.textContent = `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`;
}
else if (index === 1) { // Expansion checkbox
checkbox.textContent = `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`;
}
else if (index === 2) { // Feather checkbox
checkbox.textContent = `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`;
}
});
}
}