Add layer visibility toggle and icon support

Introduces a 'visible' property to layers and updates all relevant logic to support toggling layer visibility. Adds visibility toggle icons to the layers panel using a new IconLoader utility, with SVG and fallback canvas icons. Updates rendering, selection, and batch preview logic to respect layer visibility. Also improves blend mode menu UI and ensures new/pasted layers are always added on top with correct z-index.
This commit is contained in:
Dariusz L
2025-07-24 19:10:17 +02:00
parent 2778b8df9f
commit 3b1a69041c
19 changed files with 1159 additions and 62 deletions

View File

@@ -7,6 +7,7 @@ import { Canvas } from "./Canvas.js";
import { clearAllCanvasStates } from "./db.js";
import { ImageCache } from "./ImageCache.js";
import { createModuleLogger } from "./utils/LoggerUtils.js";
import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
import { setupSAMDetectorHook } from "./SAMDetectorIntegration.js";
const log = createModuleLogger('Canvas_view');
async function createCanvasWidget(node, widget, app) {
@@ -371,19 +372,32 @@ async function createCanvasWidget(node, widget, app) {
$el("div.painter-button-group", { id: "mask-controls" }, [
$el("button.painter-button.primary", {
id: `toggle-mask-btn-${node.id}`,
textContent: "Show Mask",
textContent: "M", // Fallback text until icon loads
title: "Toggle mask overlay visibility",
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '32px',
maxWidth: '32px',
padding: '4px',
fontSize: '12px',
fontWeight: 'bold'
},
onclick: (e) => {
const button = e.target;
const button = e.currentTarget;
canvas.maskTool.toggleOverlayVisibility();
canvas.render();
if (canvas.maskTool.isOverlayVisible) {
button.classList.add('primary');
button.textContent = "Show Mask";
}
else {
button.classList.remove('primary');
button.textContent = "Hide Mask";
const iconContainer = button.querySelector('.mask-icon-container');
if (iconContainer) {
if (canvas.maskTool.isOverlayVisible) {
button.classList.add('primary');
iconContainer.style.opacity = '1';
}
else {
button.classList.remove('primary');
iconContainer.style.opacity = '0.5';
}
}
}
}),
@@ -503,6 +517,47 @@ async function createCanvasWidget(node, widget, app) {
]),
$el("div.painter-separator")
]);
// Function to create mask icon
const createMaskIcon = () => {
const iconContainer = document.createElement('div');
iconContainer.className = 'mask-icon-container';
iconContainer.style.cssText = `
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
`;
const icon = iconLoader.getIcon(LAYERFORGE_TOOLS.MASK);
if (icon) {
if (icon instanceof HTMLImageElement) {
const img = icon.cloneNode();
img.style.cssText = `
width: 16px;
height: 16px;
filter: brightness(0) invert(1);
`;
iconContainer.appendChild(img);
}
else if (icon instanceof HTMLCanvasElement) {
const canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(icon, 0, 0, 16, 16);
}
iconContainer.appendChild(canvas);
}
}
else {
// Fallback text
iconContainer.textContent = 'M';
iconContainer.style.fontSize = '12px';
iconContainer.style.color = '#ffffff';
}
return iconContainer;
};
const updateButtonStates = () => {
const selectionCount = canvas.canvasSelection.selectedLayers.length;
const hasSelection = selectionCount > 0;
@@ -531,10 +586,39 @@ async function createCanvasWidget(node, widget, app) {
};
updateButtonStates();
canvas.updateHistoryButtons();
// Add mask icon to toggle mask button after icons are loaded
setTimeout(async () => {
try {
await iconLoader.preloadToolIcons();
const toggleMaskBtn = controlPanel.querySelector(`#toggle-mask-btn-${node.id}`);
if (toggleMaskBtn && !toggleMaskBtn.querySelector('.mask-icon-container')) {
// Clear fallback text
toggleMaskBtn.textContent = '';
const maskIcon = createMaskIcon();
toggleMaskBtn.appendChild(maskIcon);
// Set initial state based on mask visibility
if (canvas.maskTool.isOverlayVisible) {
toggleMaskBtn.classList.add('primary');
maskIcon.style.opacity = '1';
}
else {
toggleMaskBtn.classList.remove('primary');
maskIcon.style.opacity = '0.5';
}
}
}
catch (error) {
log.warn('Failed to load mask icon:', error);
}
}, 200);
// Debounce timer for updateOutput to prevent excessive updates
let updateOutputTimer = null;
const updateOutput = async (node, canvas) => {
// Check if preview is disabled - if so, skip updateOutput entirely
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
if (triggerWidget) {
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
}
const showPreviewWidget = node.widgets.find((w) => w.name === "show_preview");
if (showPreviewWidget && !showPreviewWidget.value) {
log.debug("Preview disabled, skipping updateOutput");
@@ -544,10 +628,6 @@ async function createCanvasWidget(node, widget, app) {
node.imgs = [placeholder];
return;
}
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
if (triggerWidget) {
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
}
// Clear previous timer
if (updateOutputTimer) {
clearTimeout(updateOutputTimer);