mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
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:
@@ -126,7 +126,10 @@ export class BatchPreviewManager {
|
||||
const toggleBtn = document.getElementById(`toggle-mask-btn-${this.canvas.node.id}`);
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.remove('primary');
|
||||
toggleBtn.textContent = "Hide Mask";
|
||||
const iconContainer = toggleBtn.querySelector('.mask-icon-container');
|
||||
if (iconContainer) {
|
||||
iconContainer.style.opacity = '0.5';
|
||||
}
|
||||
}
|
||||
this.canvas.render();
|
||||
}
|
||||
@@ -143,6 +146,10 @@ export class BatchPreviewManager {
|
||||
this.worldX -= menuWidthInWorld / 2;
|
||||
this.worldY += paddingInWorld;
|
||||
}
|
||||
// Hide all batch layers initially, then show only the first one
|
||||
this.layers.forEach((layer) => {
|
||||
layer.visible = false;
|
||||
});
|
||||
this._update();
|
||||
}
|
||||
hide() {
|
||||
@@ -161,11 +168,21 @@ export class BatchPreviewManager {
|
||||
const toggleBtn = document.getElementById(`toggle-mask-btn-${String(this.canvas.node.id)}`);
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.add('primary');
|
||||
toggleBtn.textContent = "Show Mask";
|
||||
const iconContainer = toggleBtn.querySelector('.mask-icon-container');
|
||||
if (iconContainer) {
|
||||
iconContainer.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
}
|
||||
this.maskWasVisible = false;
|
||||
this.canvas.layers.forEach((l) => l.visible = true);
|
||||
// Only make visible the layers that were part of the batch preview
|
||||
this.layers.forEach((layer) => {
|
||||
layer.visible = true;
|
||||
});
|
||||
// Update the layers panel to reflect visibility changes
|
||||
if (this.canvas.canvasLayersPanel) {
|
||||
this.canvas.canvasLayersPanel.onLayersChanged();
|
||||
}
|
||||
this.canvas.render();
|
||||
}
|
||||
navigate(direction) {
|
||||
@@ -203,11 +220,18 @@ export class BatchPreviewManager {
|
||||
_focusOnLayer(layer) {
|
||||
if (!layer)
|
||||
return;
|
||||
log.debug(`Focusing on layer ${layer.id}`);
|
||||
// Move the selected layer to the top of the layer stack
|
||||
this.canvas.canvasLayers.moveLayers([layer], { toIndex: 0 });
|
||||
log.debug(`Focusing on layer ${layer.id} using visibility toggle`);
|
||||
// Hide all batch layers first
|
||||
this.layers.forEach((l) => {
|
||||
l.visible = false;
|
||||
});
|
||||
// Show only the current layer
|
||||
layer.visible = true;
|
||||
this.canvas.updateSelection([layer]);
|
||||
// Render is called by moveLayers, but we call it again to be safe
|
||||
// Update the layers panel to reflect visibility changes
|
||||
if (this.canvas.canvasLayersPanel) {
|
||||
this.canvas.canvasLayersPanel.onLayersChanged();
|
||||
}
|
||||
this.canvas.render();
|
||||
}
|
||||
}
|
||||
|
||||
20
js/Canvas.js
20
js/Canvas.js
@@ -319,6 +319,26 @@ export class Canvas {
|
||||
layer.y -= finalY;
|
||||
});
|
||||
this.maskTool.updatePosition(-finalX, -finalY);
|
||||
// Update batch preview managers like in canvas resize/move operations
|
||||
if (this.pendingBatchContext) {
|
||||
this.pendingBatchContext.outputArea.x -= finalX;
|
||||
this.pendingBatchContext.outputArea.y -= finalY;
|
||||
// Also update the menu spawn position to keep it relative
|
||||
this.pendingBatchContext.spawnPosition.x -= finalX;
|
||||
this.pendingBatchContext.spawnPosition.y -= finalY;
|
||||
log.debug("Updated pending batch context during shape definition:", this.pendingBatchContext);
|
||||
}
|
||||
// Also move any active batch preview menus
|
||||
if (this.batchPreviewManagers && this.batchPreviewManagers.length > 0) {
|
||||
this.batchPreviewManagers.forEach((manager) => {
|
||||
manager.worldX -= finalX;
|
||||
manager.worldY -= finalY;
|
||||
if (manager.generationArea) {
|
||||
manager.generationArea.x -= finalX;
|
||||
manager.generationArea.y -= finalY;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.viewport.x -= finalX;
|
||||
this.viewport.y -= finalY;
|
||||
this.saveState();
|
||||
|
||||
@@ -451,7 +451,8 @@ export class CanvasIO {
|
||||
originalWidth: image.width,
|
||||
originalHeight: image.height,
|
||||
blendMode: 'normal',
|
||||
opacity: 1
|
||||
opacity: 1,
|
||||
visible: true
|
||||
};
|
||||
this.canvas.layers.push(layer);
|
||||
this.canvas.updateSelection([layer]);
|
||||
@@ -610,6 +611,7 @@ export class CanvasIO {
|
||||
zIndex: this.canvas.layers.length,
|
||||
blendMode: 'normal',
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
};
|
||||
this.canvas.layers.push(layer);
|
||||
this.canvas.updateSelection([layer]);
|
||||
|
||||
@@ -38,6 +38,10 @@ export class CanvasLayers {
|
||||
finalX = area.x + (area.width - finalWidth) / 2;
|
||||
finalY = area.y + (area.height - finalHeight) / 2;
|
||||
}
|
||||
// Find the highest zIndex among existing layers
|
||||
const maxZIndex = this.canvas.layers.length > 0
|
||||
? Math.max(...this.canvas.layers.map(l => l.zIndex))
|
||||
: -1;
|
||||
const layer = {
|
||||
id: generateUUID(),
|
||||
image: image,
|
||||
@@ -50,9 +54,10 @@ export class CanvasLayers {
|
||||
originalWidth: image.width,
|
||||
originalHeight: image.height,
|
||||
rotation: 0,
|
||||
zIndex: this.canvas.layers.length,
|
||||
zIndex: maxZIndex + 1, // Always add new layer on top
|
||||
blendMode: 'normal',
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
...layerProps
|
||||
};
|
||||
if (layer.mask) {
|
||||
@@ -189,12 +194,16 @@ export class CanvasLayers {
|
||||
const { x: mouseX, y: mouseY } = this.canvas.lastMousePosition;
|
||||
const offsetX = mouseX - centerX;
|
||||
const offsetY = mouseY - centerY;
|
||||
this.internalClipboard.forEach((clipboardLayer) => {
|
||||
// Find the highest zIndex among existing layers
|
||||
const maxZIndex = this.canvas.layers.length > 0
|
||||
? Math.max(...this.canvas.layers.map(l => l.zIndex))
|
||||
: -1;
|
||||
this.internalClipboard.forEach((clipboardLayer, index) => {
|
||||
const newLayer = {
|
||||
...clipboardLayer,
|
||||
x: clipboardLayer.x + offsetX,
|
||||
y: clipboardLayer.y + offsetY,
|
||||
zIndex: this.canvas.layers.length
|
||||
zIndex: maxZIndex + 1 + index // Ensure pasted layers maintain their relative order
|
||||
};
|
||||
this.canvas.layers.push(newLayer);
|
||||
newLayers.push(newLayer);
|
||||
@@ -314,6 +323,9 @@ export class CanvasLayers {
|
||||
getLayerAtPosition(worldX, worldY) {
|
||||
for (let i = this.canvas.layers.length - 1; i >= 0; i--) {
|
||||
const layer = this.canvas.layers[i];
|
||||
// Skip invisible layers
|
||||
if (!layer.visible)
|
||||
continue;
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
const dx = worldX - centerX;
|
||||
@@ -354,7 +366,11 @@ export class CanvasLayers {
|
||||
}
|
||||
_drawLayers(ctx, layers, options = {}) {
|
||||
const sortedLayers = [...layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
sortedLayers.forEach(layer => this._drawLayer(ctx, layer, options));
|
||||
sortedLayers.forEach(layer => {
|
||||
if (layer.visible) {
|
||||
this._drawLayer(ctx, layer, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
drawLayersToContext(ctx, layers, options = {}) {
|
||||
this._drawLayers(ctx, layers, options);
|
||||
@@ -491,8 +507,46 @@ export class CanvasLayers {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
titleBar.textContent = 'Blend Mode';
|
||||
const titleText = document.createElement('span');
|
||||
titleText.textContent = 'Blend Mode';
|
||||
titleText.style.cssText = `
|
||||
flex: 1;
|
||||
cursor: move;
|
||||
`;
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.textContent = '×';
|
||||
closeButton.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s;
|
||||
`;
|
||||
closeButton.onmouseover = () => {
|
||||
closeButton.style.backgroundColor = '#4a4a4a';
|
||||
};
|
||||
closeButton.onmouseout = () => {
|
||||
closeButton.style.backgroundColor = 'transparent';
|
||||
};
|
||||
closeButton.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.closeBlendModeMenu();
|
||||
};
|
||||
titleBar.appendChild(titleText);
|
||||
titleBar.appendChild(closeButton);
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `padding: 5px;`;
|
||||
menu.appendChild(titleBar);
|
||||
@@ -823,7 +877,8 @@ export class CanvasLayers {
|
||||
rotation: 0,
|
||||
zIndex: minZIndex,
|
||||
blendMode: 'normal',
|
||||
opacity: 1
|
||||
opacity: 1,
|
||||
visible: true
|
||||
};
|
||||
this.canvas.layers = this.canvas.layers.filter((layer) => !this.canvas.canvasSelection.selectedLayers.includes(layer));
|
||||
this.canvas.layers.push(fusedLayer);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||
import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
|
||||
const log = createModuleLogger('CanvasLayersPanel');
|
||||
export class CanvasLayersPanel {
|
||||
constructor(canvas) {
|
||||
@@ -14,8 +15,106 @@ export class CanvasLayersPanel {
|
||||
this.handleDragOver = this.handleDragOver.bind(this);
|
||||
this.handleDragEnd = this.handleDragEnd.bind(this);
|
||||
this.handleDrop = this.handleDrop.bind(this);
|
||||
// Preload icons
|
||||
this.initializeIcons();
|
||||
log.info('CanvasLayersPanel initialized');
|
||||
}
|
||||
async initializeIcons() {
|
||||
try {
|
||||
await iconLoader.preloadToolIcons();
|
||||
log.debug('Icons preloaded successfully');
|
||||
}
|
||||
catch (error) {
|
||||
log.warn('Failed to preload icons, using fallbacks:', error);
|
||||
}
|
||||
}
|
||||
createIconElement(toolName, size = 16) {
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.style.cssText = `
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
const icon = iconLoader.getIcon(toolName);
|
||||
if (icon) {
|
||||
if (icon instanceof HTMLImageElement) {
|
||||
const img = icon.cloneNode();
|
||||
img.style.cssText = `
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
filter: brightness(0) invert(1);
|
||||
`;
|
||||
iconContainer.appendChild(img);
|
||||
}
|
||||
else if (icon instanceof HTMLCanvasElement) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(icon, 0, 0, size, size);
|
||||
}
|
||||
iconContainer.appendChild(canvas);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Fallback text
|
||||
iconContainer.textContent = toolName.charAt(0).toUpperCase();
|
||||
iconContainer.style.fontSize = `${size * 0.6}px`;
|
||||
iconContainer.style.color = '#ffffff';
|
||||
}
|
||||
return iconContainer;
|
||||
}
|
||||
createVisibilityIcon(isVisible) {
|
||||
if (isVisible) {
|
||||
return this.createIconElement(LAYERFORGE_TOOLS.VISIBILITY, 16);
|
||||
}
|
||||
else {
|
||||
// Create a "hidden" version of the visibility icon
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.style.cssText = `
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
`;
|
||||
const icon = iconLoader.getIcon(LAYERFORGE_TOOLS.VISIBILITY);
|
||||
if (icon) {
|
||||
if (icon instanceof HTMLImageElement) {
|
||||
const img = icon.cloneNode();
|
||||
img.style.cssText = `
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.3;
|
||||
`;
|
||||
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.globalAlpha = 0.3;
|
||||
ctx.drawImage(icon, 0, 0, 16, 16);
|
||||
}
|
||||
iconContainer.appendChild(canvas);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Fallback
|
||||
iconContainer.textContent = 'H';
|
||||
iconContainer.style.fontSize = '10px';
|
||||
iconContainer.style.color = '#888888';
|
||||
}
|
||||
return iconContainer;
|
||||
}
|
||||
}
|
||||
createPanelStructure() {
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'layers-panel';
|
||||
@@ -24,7 +123,7 @@ export class CanvasLayersPanel {
|
||||
<div class="layers-panel-header">
|
||||
<span class="layers-panel-title">Layers</span>
|
||||
<div class="layers-panel-controls">
|
||||
<button class="layers-btn" id="delete-layer-btn" title="Delete layer">🗑</button>
|
||||
<button class="layers-btn" id="delete-layer-btn" title="Delete layer"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layers-container" id="layers-container">
|
||||
@@ -231,6 +330,23 @@ export class CanvasLayersPanel {
|
||||
.layers-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
.layer-visibility-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.layer-visibility-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
log.debug('Styles injected');
|
||||
@@ -239,6 +355,11 @@ export class CanvasLayersPanel {
|
||||
if (!this.container)
|
||||
return;
|
||||
const deleteBtn = this.container.querySelector('#delete-layer-btn');
|
||||
// Add delete icon to button
|
||||
if (deleteBtn) {
|
||||
const deleteIcon = this.createIconElement(LAYERFORGE_TOOLS.DELETE, 16);
|
||||
deleteBtn.appendChild(deleteIcon);
|
||||
}
|
||||
deleteBtn?.addEventListener('click', () => {
|
||||
log.info('Delete layer button clicked');
|
||||
this.deleteSelectedLayers();
|
||||
@@ -280,9 +401,16 @@ export class CanvasLayersPanel {
|
||||
layer.name = this.ensureUniqueName(layer.name, layer);
|
||||
}
|
||||
layerRow.innerHTML = `
|
||||
<div class="layer-visibility-toggle" data-layer-index="${index}" title="Toggle layer visibility"></div>
|
||||
<div class="layer-thumbnail" data-layer-index="${index}"></div>
|
||||
<span class="layer-name" data-layer-index="${index}">${layer.name}</span>
|
||||
`;
|
||||
// Add visibility icon
|
||||
const visibilityToggle = layerRow.querySelector('.layer-visibility-toggle');
|
||||
if (visibilityToggle) {
|
||||
const visibilityIcon = this.createVisibilityIcon(layer.visible);
|
||||
visibilityToggle.appendChild(visibilityIcon);
|
||||
}
|
||||
const thumbnailContainer = layerRow.querySelector('.layer-thumbnail');
|
||||
if (thumbnailContainer) {
|
||||
this.generateThumbnail(layer, thumbnailContainer);
|
||||
@@ -328,6 +456,15 @@ export class CanvasLayersPanel {
|
||||
this.startEditingLayerName(nameElement, layer);
|
||||
}
|
||||
});
|
||||
// Add visibility toggle event listener
|
||||
const visibilityToggle = layerRow.querySelector('.layer-visibility-toggle');
|
||||
if (visibilityToggle) {
|
||||
visibilityToggle.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.toggleLayerVisibility(layer);
|
||||
});
|
||||
}
|
||||
layerRow.addEventListener('dragstart', (e) => this.handleDragStart(e, layer, index));
|
||||
layerRow.addEventListener('dragover', this.handleDragOver.bind(this));
|
||||
layerRow.addEventListener('dragend', this.handleDragEnd.bind(this));
|
||||
@@ -401,6 +538,19 @@ export class CanvasLayersPanel {
|
||||
} while (existingNames.includes(uniqueName));
|
||||
return uniqueName;
|
||||
}
|
||||
toggleLayerVisibility(layer) {
|
||||
layer.visible = !layer.visible;
|
||||
// If layer became invisible and is selected, deselect it
|
||||
if (!layer.visible && this.canvas.canvasSelection.selectedLayers.includes(layer)) {
|
||||
const newSelection = this.canvas.canvasSelection.selectedLayers.filter((l) => l !== layer);
|
||||
this.canvas.updateSelection(newSelection);
|
||||
}
|
||||
this.canvas.render();
|
||||
this.canvas.requestSaveState();
|
||||
// Update the eye icon in the panel
|
||||
this.renderLayers();
|
||||
log.info(`Layer "${layer.name}" visibility toggled to: ${layer.visible}`);
|
||||
}
|
||||
deleteSelectedLayers() {
|
||||
if (this.canvas.canvasSelection.selectedLayers.length === 0) {
|
||||
log.debug('No layers selected for deletion');
|
||||
|
||||
@@ -46,7 +46,7 @@ export class CanvasRenderer {
|
||||
this.drawGrid(ctx);
|
||||
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
sortedLayers.forEach(layer => {
|
||||
if (!layer.image)
|
||||
if (!layer.image || !layer.visible)
|
||||
return;
|
||||
ctx.save();
|
||||
const currentTransform = ctx.getTransform();
|
||||
@@ -171,7 +171,7 @@ export class CanvasRenderer {
|
||||
renderLayerInfo(ctx) {
|
||||
if (this.canvas.canvasSelection.selectedLayer) {
|
||||
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
||||
if (!layer.image)
|
||||
if (!layer.image || !layer.visible)
|
||||
return;
|
||||
const layerIndex = this.canvas.layers.indexOf(layer);
|
||||
const currentWidth = Math.round(layer.width);
|
||||
|
||||
@@ -40,7 +40,8 @@ export class CanvasSelection {
|
||||
*/
|
||||
updateSelection(newSelection) {
|
||||
const previousSelection = this.selectedLayers.length;
|
||||
this.selectedLayers = newSelection || [];
|
||||
// Filter out invisible layers from selection
|
||||
this.selectedLayers = (newSelection || []).filter((layer) => layer.visible !== false);
|
||||
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
|
||||
// Sprawdź, czy zaznaczenie faktycznie się zmieniło, aby uniknąć pętli
|
||||
const hasChanged = previousSelection !== this.selectedLayers.length ||
|
||||
|
||||
106
js/CanvasView.js
106
js/CanvasView.js
@@ -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);
|
||||
|
||||
178
js/utils/IconLoader.js
Normal file
178
js/utils/IconLoader.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import { createModuleLogger } from "./LoggerUtils.js";
|
||||
const log = createModuleLogger('IconLoader');
|
||||
// Define tool constants for LayerForge
|
||||
export const LAYERFORGE_TOOLS = {
|
||||
VISIBILITY: 'visibility',
|
||||
MOVE: 'move',
|
||||
ROTATE: 'rotate',
|
||||
SCALE: 'scale',
|
||||
DELETE: 'delete',
|
||||
DUPLICATE: 'duplicate',
|
||||
BLEND_MODE: 'blend_mode',
|
||||
OPACITY: 'opacity',
|
||||
MASK: 'mask',
|
||||
BRUSH: 'brush',
|
||||
ERASER: 'eraser',
|
||||
SHAPE: 'shape',
|
||||
SETTINGS: 'settings'
|
||||
};
|
||||
// SVG Icons for LayerForge tools
|
||||
const LAYERFORGE_TOOL_ICONS = {
|
||||
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.SCALE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M22,18V22H18V20H20V18H22M22,6V10H20V8H18V6H22M2,6V10H4V8H6V6H2M2,18V22H6V20H4V18H2M16,8V10H14V12H16V14H14V16H12V14H10V12H12V10H10V8H12V6H14V8H16Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.DELETE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.DUPLICATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.BLEND_MODE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20V4Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.OPACITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,20A6,6 0 0,1 6,14C6,10 12,3.25 12,3.25S18,10 18,14A6,6 0 0,1 12,20Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.MASK]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><rect x="3" y="3" width="18" height="18" rx="2" fill="none" stroke="#ffffff" stroke-width="2"/><circle cx="12" cy="12" r="5" fill="#ffffff"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.BRUSH]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M15.4565 9.67503L15.3144 9.53297C14.6661 8.90796 13.8549 8.43369 12.9235 8.18412C10.0168 7.40527 7.22541 9.05273 6.43185 12.0143C6.38901 12.1742 6.36574 12.3537 6.3285 12.8051C6.17423 14.6752 5.73449 16.0697 4.5286 17.4842C6.78847 18.3727 9.46572 18.9986 11.5016 18.9986C13.9702 18.9986 16.1644 17.3394 16.8126 14.9202C17.3306 12.9869 16.7513 11.0181 15.4565 9.67503ZM13.2886 6.21301L18.2278 2.37142C18.6259 2.0618 19.1922 2.09706 19.5488 2.45367L22.543 5.44787C22.8997 5.80448 22.9349 6.37082 22.6253 6.76891L18.7847 11.7068C19.0778 12.8951 19.0836 14.1721 18.7444 15.4379C17.8463 18.7897 14.8142 20.9986 11.5016 20.9986C8 20.9986 3.5 19.4967 1 17.9967C4.97978 14.9967 4.04722 13.1865 4.5 11.4967C5.55843 7.54658 9.34224 5.23935 13.2886 6.21301ZM16.7015 8.09161C16.7673 8.15506 16.8319 8.21964 16.8952 8.28533L18.0297 9.41984L20.5046 6.23786L18.7589 4.4921L15.5769 6.96698L16.7015 8.09161Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.ERASER]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M8.58564 8.85449L3.63589 13.8042L8.83021 18.9985L9.99985 18.9978V18.9966H11.1714L14.9496 15.2184L8.58564 8.85449ZM9.99985 7.44027L16.3638 13.8042L19.1922 10.9758L12.8283 4.61185L9.99985 7.44027ZM13.9999 18.9966H20.9999V20.9966H11.9999L8.00229 20.9991L1.51457 14.5113C1.12405 14.1208 1.12405 13.4877 1.51457 13.0971L12.1212 2.49053C12.5117 2.1 13.1449 2.1 13.5354 2.49053L21.3136 10.2687C21.7041 10.6592 21.7041 11.2924 21.3136 11.6829L13.9999 18.9966Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.SHAPE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M3 4H21C21.5523 4 22 4.44772 22 5V19C22 19.5523 21.5523 20 21 20H3C2.44772 20 2 19.5523 2 19V5C2 4.44772 2.44772 4 3 4ZM4 6V18H20V6H4Z"/></svg>')}`,
|
||||
[LAYERFORGE_TOOLS.SETTINGS]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.22,8.95 2.27,9.22 2.46,9.37L4.5,11L5.13,18.93C5.17,19.18 5.38,19.36 5.63,19.36H18.37C18.62,19.36 18.83,19.18 18.87,18.93L19.5,11L21.54,9.37Z"/></svg>')}`
|
||||
};
|
||||
// Tool colors for LayerForge
|
||||
const LAYERFORGE_TOOL_COLORS = {
|
||||
[LAYERFORGE_TOOLS.VISIBILITY]: '#4285F4',
|
||||
[LAYERFORGE_TOOLS.MOVE]: '#34A853',
|
||||
[LAYERFORGE_TOOLS.ROTATE]: '#FBBC05',
|
||||
[LAYERFORGE_TOOLS.SCALE]: '#EA4335',
|
||||
[LAYERFORGE_TOOLS.DELETE]: '#FF6D01',
|
||||
[LAYERFORGE_TOOLS.DUPLICATE]: '#46BDC6',
|
||||
[LAYERFORGE_TOOLS.BLEND_MODE]: '#9C27B0',
|
||||
[LAYERFORGE_TOOLS.OPACITY]: '#8BC34A',
|
||||
[LAYERFORGE_TOOLS.MASK]: '#607D8B',
|
||||
[LAYERFORGE_TOOLS.BRUSH]: '#4285F4',
|
||||
[LAYERFORGE_TOOLS.ERASER]: '#FBBC05',
|
||||
[LAYERFORGE_TOOLS.SHAPE]: '#FF6D01',
|
||||
[LAYERFORGE_TOOLS.SETTINGS]: '#F06292'
|
||||
};
|
||||
export class IconLoader {
|
||||
constructor() {
|
||||
this._iconCache = {};
|
||||
this._loadingPromises = new Map();
|
||||
log.info('IconLoader initialized');
|
||||
}
|
||||
/**
|
||||
* Preload all LayerForge tool icons
|
||||
*/
|
||||
preloadToolIcons() {
|
||||
log.info('Starting to preload LayerForge tool icons');
|
||||
const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => {
|
||||
return this.loadIcon(tool);
|
||||
});
|
||||
return Promise.all(loadPromises).then(() => {
|
||||
log.info(`Successfully preloaded ${loadPromises.length} tool icons`);
|
||||
}).catch(error => {
|
||||
log.error('Error preloading tool icons:', error);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Load a specific icon by tool name
|
||||
*/
|
||||
async loadIcon(tool) {
|
||||
// Check if already cached
|
||||
if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) {
|
||||
return this._iconCache[tool];
|
||||
}
|
||||
// Check if already loading
|
||||
if (this._loadingPromises.has(tool)) {
|
||||
return this._loadingPromises.get(tool);
|
||||
}
|
||||
// Create fallback canvas first
|
||||
const fallbackCanvas = this.createFallbackIcon(tool);
|
||||
this._iconCache[tool] = fallbackCanvas;
|
||||
// Start loading the SVG icon
|
||||
const loadPromise = new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
this._iconCache[tool] = img;
|
||||
this._loadingPromises.delete(tool);
|
||||
log.debug(`Successfully loaded icon for tool: ${tool}`);
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = (error) => {
|
||||
log.warn(`Failed to load SVG icon for tool: ${tool}, using fallback`);
|
||||
this._loadingPromises.delete(tool);
|
||||
// Keep the fallback canvas in cache
|
||||
reject(error);
|
||||
};
|
||||
const iconData = LAYERFORGE_TOOL_ICONS[tool];
|
||||
if (iconData) {
|
||||
img.src = iconData;
|
||||
}
|
||||
else {
|
||||
log.warn(`No icon data found for tool: ${tool}`);
|
||||
reject(new Error(`No icon data for tool: ${tool}`));
|
||||
}
|
||||
});
|
||||
this._loadingPromises.set(tool, loadPromise);
|
||||
return loadPromise;
|
||||
}
|
||||
/**
|
||||
* Create a fallback canvas icon with colored background and text
|
||||
*/
|
||||
createFallbackIcon(tool) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 24;
|
||||
canvas.height = 24;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
log.error('Failed to get canvas context for fallback icon');
|
||||
return canvas;
|
||||
}
|
||||
// Fill background with tool color
|
||||
const color = LAYERFORGE_TOOL_COLORS[tool] || '#888888';
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 24, 24);
|
||||
// Add border
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(0.5, 0.5, 23, 23);
|
||||
// Add text
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const firstChar = tool.charAt(0).toUpperCase();
|
||||
ctx.fillText(firstChar, 12, 12);
|
||||
return canvas;
|
||||
}
|
||||
/**
|
||||
* Get cached icon (canvas or image)
|
||||
*/
|
||||
getIcon(tool) {
|
||||
return this._iconCache[tool] || null;
|
||||
}
|
||||
/**
|
||||
* Check if icon is loaded (as image, not fallback canvas)
|
||||
*/
|
||||
isIconLoaded(tool) {
|
||||
return this._iconCache[tool] instanceof HTMLImageElement;
|
||||
}
|
||||
/**
|
||||
* Clear all cached icons
|
||||
*/
|
||||
clearCache() {
|
||||
this._iconCache = {};
|
||||
this._loadingPromises.clear();
|
||||
log.info('Icon cache cleared');
|
||||
}
|
||||
/**
|
||||
* Get all available tool names
|
||||
*/
|
||||
getAvailableTools() {
|
||||
return Object.values(LAYERFORGE_TOOLS);
|
||||
}
|
||||
/**
|
||||
* Get tool color
|
||||
*/
|
||||
getToolColor(tool) {
|
||||
return LAYERFORGE_TOOL_COLORS[tool] || '#888888';
|
||||
}
|
||||
}
|
||||
// Export singleton instance
|
||||
export const iconLoader = new IconLoader();
|
||||
// Export for external use
|
||||
export { LAYERFORGE_TOOL_ICONS, LAYERFORGE_TOOL_COLORS };
|
||||
@@ -169,7 +169,10 @@ export class BatchPreviewManager {
|
||||
const toggleBtn = document.getElementById(`toggle-mask-btn-${this.canvas.node.id}`);
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.remove('primary');
|
||||
toggleBtn.textContent = "Hide Mask";
|
||||
const iconContainer = toggleBtn.querySelector('.mask-icon-container') as HTMLElement;
|
||||
if (iconContainer) {
|
||||
iconContainer.style.opacity = '0.5';
|
||||
}
|
||||
}
|
||||
this.canvas.render();
|
||||
}
|
||||
@@ -191,6 +194,11 @@ export class BatchPreviewManager {
|
||||
this.worldY += paddingInWorld;
|
||||
}
|
||||
|
||||
// Hide all batch layers initially, then show only the first one
|
||||
this.layers.forEach((layer: Layer) => {
|
||||
layer.visible = false;
|
||||
});
|
||||
|
||||
this._update();
|
||||
}
|
||||
|
||||
@@ -213,12 +221,24 @@ export class BatchPreviewManager {
|
||||
const toggleBtn = document.getElementById(`toggle-mask-btn-${String(this.canvas.node.id)}`);
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.add('primary');
|
||||
toggleBtn.textContent = "Show Mask";
|
||||
const iconContainer = toggleBtn.querySelector('.mask-icon-container') as HTMLElement;
|
||||
if (iconContainer) {
|
||||
iconContainer.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
}
|
||||
this.maskWasVisible = false;
|
||||
|
||||
this.canvas.layers.forEach((l: Layer) => (l as any).visible = true);
|
||||
// Only make visible the layers that were part of the batch preview
|
||||
this.layers.forEach((layer: Layer) => {
|
||||
layer.visible = true;
|
||||
});
|
||||
|
||||
// Update the layers panel to reflect visibility changes
|
||||
if (this.canvas.canvasLayersPanel) {
|
||||
this.canvas.canvasLayersPanel.onLayersChanged();
|
||||
}
|
||||
|
||||
this.canvas.render();
|
||||
}
|
||||
|
||||
@@ -264,14 +284,23 @@ export class BatchPreviewManager {
|
||||
|
||||
private _focusOnLayer(layer: Layer): void {
|
||||
if (!layer) return;
|
||||
log.debug(`Focusing on layer ${layer.id}`);
|
||||
log.debug(`Focusing on layer ${layer.id} using visibility toggle`);
|
||||
|
||||
// Move the selected layer to the top of the layer stack
|
||||
this.canvas.canvasLayers.moveLayers([layer], { toIndex: 0 });
|
||||
// Hide all batch layers first
|
||||
this.layers.forEach((l: Layer) => {
|
||||
l.visible = false;
|
||||
});
|
||||
|
||||
// Show only the current layer
|
||||
layer.visible = true;
|
||||
|
||||
this.canvas.updateSelection([layer]);
|
||||
|
||||
// Render is called by moveLayers, but we call it again to be safe
|
||||
// Update the layers panel to reflect visibility changes
|
||||
if (this.canvas.canvasLayersPanel) {
|
||||
this.canvas.canvasLayersPanel.onLayersChanged();
|
||||
}
|
||||
|
||||
this.canvas.render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,6 +413,29 @@ export class Canvas {
|
||||
|
||||
this.maskTool.updatePosition(-finalX, -finalY);
|
||||
|
||||
// Update batch preview managers like in canvas resize/move operations
|
||||
if (this.pendingBatchContext) {
|
||||
this.pendingBatchContext.outputArea.x -= finalX;
|
||||
this.pendingBatchContext.outputArea.y -= finalY;
|
||||
|
||||
// Also update the menu spawn position to keep it relative
|
||||
this.pendingBatchContext.spawnPosition.x -= finalX;
|
||||
this.pendingBatchContext.spawnPosition.y -= finalY;
|
||||
log.debug("Updated pending batch context during shape definition:", this.pendingBatchContext);
|
||||
}
|
||||
|
||||
// Also move any active batch preview menus
|
||||
if (this.batchPreviewManagers && this.batchPreviewManagers.length > 0) {
|
||||
this.batchPreviewManagers.forEach((manager: any) => {
|
||||
manager.worldX -= finalX;
|
||||
manager.worldY -= finalY;
|
||||
if (manager.generationArea) {
|
||||
manager.generationArea.x -= finalX;
|
||||
manager.generationArea.y -= finalY;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.viewport.x -= finalX;
|
||||
this.viewport.y -= finalY;
|
||||
|
||||
|
||||
@@ -546,7 +546,8 @@ export class CanvasIO {
|
||||
originalWidth: image.width,
|
||||
originalHeight: image.height,
|
||||
blendMode: 'normal',
|
||||
opacity: 1
|
||||
opacity: 1,
|
||||
visible: true
|
||||
};
|
||||
|
||||
this.canvas.layers.push(layer);
|
||||
@@ -729,6 +730,7 @@ export class CanvasIO {
|
||||
zIndex: this.canvas.layers.length,
|
||||
blendMode: 'normal',
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
this.canvas.layers.push(layer);
|
||||
|
||||
@@ -128,12 +128,17 @@ export class CanvasLayers {
|
||||
const offsetX = mouseX - centerX;
|
||||
const offsetY = mouseY - centerY;
|
||||
|
||||
this.internalClipboard.forEach((clipboardLayer: Layer) => {
|
||||
// Find the highest zIndex among existing layers
|
||||
const maxZIndex = this.canvas.layers.length > 0
|
||||
? Math.max(...this.canvas.layers.map(l => l.zIndex))
|
||||
: -1;
|
||||
|
||||
this.internalClipboard.forEach((clipboardLayer: Layer, index: number) => {
|
||||
const newLayer: Layer = {
|
||||
...clipboardLayer,
|
||||
x: clipboardLayer.x + offsetX,
|
||||
y: clipboardLayer.y + offsetY,
|
||||
zIndex: this.canvas.layers.length
|
||||
zIndex: maxZIndex + 1 + index // Ensure pasted layers maintain their relative order
|
||||
};
|
||||
this.canvas.layers.push(newLayer);
|
||||
newLayers.push(newLayer);
|
||||
@@ -189,6 +194,11 @@ export class CanvasLayers {
|
||||
finalY = area.y + (area.height - finalHeight) / 2;
|
||||
}
|
||||
|
||||
// Find the highest zIndex among existing layers
|
||||
const maxZIndex = this.canvas.layers.length > 0
|
||||
? Math.max(...this.canvas.layers.map(l => l.zIndex))
|
||||
: -1;
|
||||
|
||||
const layer: Layer = {
|
||||
id: generateUUID(),
|
||||
image: image,
|
||||
@@ -201,9 +211,10 @@ export class CanvasLayers {
|
||||
originalWidth: image.width,
|
||||
originalHeight: image.height,
|
||||
rotation: 0,
|
||||
zIndex: this.canvas.layers.length,
|
||||
zIndex: maxZIndex + 1, // Always add new layer on top
|
||||
blendMode: 'normal',
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
...layerProps
|
||||
};
|
||||
|
||||
@@ -360,6 +371,9 @@ export class CanvasLayers {
|
||||
for (let i = this.canvas.layers.length - 1; i >= 0; i--) {
|
||||
const layer = this.canvas.layers[i];
|
||||
|
||||
// Skip invisible layers
|
||||
if (!layer.visible) continue;
|
||||
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
|
||||
@@ -414,7 +428,11 @@ export class CanvasLayers {
|
||||
|
||||
private _drawLayers(ctx: CanvasRenderingContext2D, layers: Layer[], options: { offsetX?: number, offsetY?: number } = {}): void {
|
||||
const sortedLayers = [...layers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
|
||||
sortedLayers.forEach(layer => this._drawLayer(ctx, layer, options));
|
||||
sortedLayers.forEach(layer => {
|
||||
if (layer.visible) {
|
||||
this._drawLayer(ctx, layer, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public drawLayersToContext(ctx: CanvasRenderingContext2D, layers: Layer[], options: { offsetX?: number, offsetY?: number } = {}): void {
|
||||
@@ -567,8 +585,52 @@ export class CanvasLayers {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
titleBar.textContent = 'Blend Mode';
|
||||
|
||||
const titleText = document.createElement('span');
|
||||
titleText.textContent = 'Blend Mode';
|
||||
titleText.style.cssText = `
|
||||
flex: 1;
|
||||
cursor: move;
|
||||
`;
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.textContent = '×';
|
||||
closeButton.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s;
|
||||
`;
|
||||
|
||||
closeButton.onmouseover = () => {
|
||||
closeButton.style.backgroundColor = '#4a4a4a';
|
||||
};
|
||||
|
||||
closeButton.onmouseout = () => {
|
||||
closeButton.style.backgroundColor = 'transparent';
|
||||
};
|
||||
|
||||
closeButton.onclick = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.closeBlendModeMenu();
|
||||
};
|
||||
|
||||
titleBar.appendChild(titleText);
|
||||
titleBar.appendChild(closeButton);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `padding: 5px;`;
|
||||
@@ -963,7 +1025,8 @@ export class CanvasLayers {
|
||||
rotation: 0,
|
||||
zIndex: minZIndex,
|
||||
blendMode: 'normal',
|
||||
opacity: 1
|
||||
opacity: 1,
|
||||
visible: true
|
||||
};
|
||||
|
||||
this.canvas.layers = this.canvas.layers.filter((layer: Layer) => !this.canvas.canvasSelection.selectedLayers.includes(layer));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
||||
import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
|
||||
import type { Canvas } from './Canvas';
|
||||
import type { Layer } from './types';
|
||||
|
||||
@@ -28,9 +29,109 @@ export class CanvasLayersPanel {
|
||||
this.handleDragEnd = this.handleDragEnd.bind(this);
|
||||
this.handleDrop = this.handleDrop.bind(this);
|
||||
|
||||
// Preload icons
|
||||
this.initializeIcons();
|
||||
|
||||
log.info('CanvasLayersPanel initialized');
|
||||
}
|
||||
|
||||
private async initializeIcons(): Promise<void> {
|
||||
try {
|
||||
await iconLoader.preloadToolIcons();
|
||||
log.debug('Icons preloaded successfully');
|
||||
} catch (error) {
|
||||
log.warn('Failed to preload icons, using fallbacks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private createIconElement(toolName: string, size: number = 16): HTMLElement {
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.style.cssText = `
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const icon = iconLoader.getIcon(toolName);
|
||||
if (icon) {
|
||||
if (icon instanceof HTMLImageElement) {
|
||||
const img = icon.cloneNode() as HTMLImageElement;
|
||||
img.style.cssText = `
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
filter: brightness(0) invert(1);
|
||||
`;
|
||||
iconContainer.appendChild(img);
|
||||
} else if (icon instanceof HTMLCanvasElement) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(icon, 0, 0, size, size);
|
||||
}
|
||||
iconContainer.appendChild(canvas);
|
||||
}
|
||||
} else {
|
||||
// Fallback text
|
||||
iconContainer.textContent = toolName.charAt(0).toUpperCase();
|
||||
iconContainer.style.fontSize = `${size * 0.6}px`;
|
||||
iconContainer.style.color = '#ffffff';
|
||||
}
|
||||
|
||||
return iconContainer;
|
||||
}
|
||||
|
||||
private createVisibilityIcon(isVisible: boolean): HTMLElement {
|
||||
if (isVisible) {
|
||||
return this.createIconElement(LAYERFORGE_TOOLS.VISIBILITY, 16);
|
||||
} else {
|
||||
// Create a "hidden" version of the visibility icon
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.style.cssText = `
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
`;
|
||||
|
||||
const icon = iconLoader.getIcon(LAYERFORGE_TOOLS.VISIBILITY);
|
||||
if (icon) {
|
||||
if (icon instanceof HTMLImageElement) {
|
||||
const img = icon.cloneNode() as HTMLImageElement;
|
||||
img.style.cssText = `
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.3;
|
||||
`;
|
||||
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.globalAlpha = 0.3;
|
||||
ctx.drawImage(icon, 0, 0, 16, 16);
|
||||
}
|
||||
iconContainer.appendChild(canvas);
|
||||
}
|
||||
} else {
|
||||
// Fallback
|
||||
iconContainer.textContent = 'H';
|
||||
iconContainer.style.fontSize = '10px';
|
||||
iconContainer.style.color = '#888888';
|
||||
}
|
||||
|
||||
return iconContainer;
|
||||
}
|
||||
}
|
||||
|
||||
createPanelStructure(): HTMLElement {
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'layers-panel';
|
||||
@@ -39,7 +140,7 @@ export class CanvasLayersPanel {
|
||||
<div class="layers-panel-header">
|
||||
<span class="layers-panel-title">Layers</span>
|
||||
<div class="layers-panel-controls">
|
||||
<button class="layers-btn" id="delete-layer-btn" title="Delete layer">🗑</button>
|
||||
<button class="layers-btn" id="delete-layer-btn" title="Delete layer"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layers-container" id="layers-container">
|
||||
@@ -253,6 +354,23 @@ export class CanvasLayersPanel {
|
||||
.layers-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
.layer-visibility-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.layer-visibility-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
@@ -263,6 +381,12 @@ export class CanvasLayersPanel {
|
||||
if (!this.container) return;
|
||||
const deleteBtn = this.container.querySelector('#delete-layer-btn');
|
||||
|
||||
// Add delete icon to button
|
||||
if (deleteBtn) {
|
||||
const deleteIcon = this.createIconElement(LAYERFORGE_TOOLS.DELETE, 16);
|
||||
deleteBtn.appendChild(deleteIcon);
|
||||
}
|
||||
|
||||
deleteBtn?.addEventListener('click', () => {
|
||||
log.info('Delete layer button clicked');
|
||||
this.deleteSelectedLayers();
|
||||
@@ -313,10 +437,18 @@ export class CanvasLayersPanel {
|
||||
}
|
||||
|
||||
layerRow.innerHTML = `
|
||||
<div class="layer-visibility-toggle" data-layer-index="${index}" title="Toggle layer visibility"></div>
|
||||
<div class="layer-thumbnail" data-layer-index="${index}"></div>
|
||||
<span class="layer-name" data-layer-index="${index}">${layer.name}</span>
|
||||
`;
|
||||
|
||||
// Add visibility icon
|
||||
const visibilityToggle = layerRow.querySelector<HTMLElement>('.layer-visibility-toggle');
|
||||
if (visibilityToggle) {
|
||||
const visibilityIcon = this.createVisibilityIcon(layer.visible);
|
||||
visibilityToggle.appendChild(visibilityIcon);
|
||||
}
|
||||
|
||||
const thumbnailContainer = layerRow.querySelector<HTMLElement>('.layer-thumbnail');
|
||||
if (thumbnailContainer) {
|
||||
this.generateThumbnail(layer, thumbnailContainer);
|
||||
@@ -372,6 +504,16 @@ export class CanvasLayersPanel {
|
||||
}
|
||||
});
|
||||
|
||||
// Add visibility toggle event listener
|
||||
const visibilityToggle = layerRow.querySelector<HTMLElement>('.layer-visibility-toggle');
|
||||
if (visibilityToggle) {
|
||||
visibilityToggle.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.toggleLayerVisibility(layer);
|
||||
});
|
||||
}
|
||||
|
||||
layerRow.addEventListener('dragstart', (e: DragEvent) => this.handleDragStart(e, layer, index));
|
||||
layerRow.addEventListener('dragover', this.handleDragOver.bind(this));
|
||||
layerRow.addEventListener('dragend', this.handleDragEnd.bind(this));
|
||||
@@ -464,6 +606,24 @@ export class CanvasLayersPanel {
|
||||
return uniqueName;
|
||||
}
|
||||
|
||||
toggleLayerVisibility(layer: Layer): void {
|
||||
layer.visible = !layer.visible;
|
||||
|
||||
// If layer became invisible and is selected, deselect it
|
||||
if (!layer.visible && this.canvas.canvasSelection.selectedLayers.includes(layer)) {
|
||||
const newSelection = this.canvas.canvasSelection.selectedLayers.filter((l: Layer) => l !== layer);
|
||||
this.canvas.updateSelection(newSelection);
|
||||
}
|
||||
|
||||
this.canvas.render();
|
||||
this.canvas.requestSaveState();
|
||||
|
||||
// Update the eye icon in the panel
|
||||
this.renderLayers();
|
||||
|
||||
log.info(`Layer "${layer.name}" visibility toggled to: ${layer.visible}`);
|
||||
}
|
||||
|
||||
deleteSelectedLayers(): void {
|
||||
if (this.canvas.canvasSelection.selectedLayers.length === 0) {
|
||||
log.debug('No layers selected for deletion');
|
||||
|
||||
@@ -60,7 +60,7 @@ export class CanvasRenderer {
|
||||
|
||||
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
sortedLayers.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
if (!layer.image || !layer.visible) return;
|
||||
ctx.save();
|
||||
const currentTransform = ctx.getTransform();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
@@ -205,7 +205,7 @@ export class CanvasRenderer {
|
||||
renderLayerInfo(ctx: any) {
|
||||
if (this.canvas.canvasSelection.selectedLayer) {
|
||||
this.canvas.canvasSelection.selectedLayers.forEach((layer: any) => {
|
||||
if (!layer.image) return;
|
||||
if (!layer.image || !layer.visible) return;
|
||||
|
||||
const layerIndex = this.canvas.layers.indexOf(layer);
|
||||
const currentWidth = Math.round(layer.width);
|
||||
|
||||
@@ -52,7 +52,8 @@ export class CanvasSelection {
|
||||
*/
|
||||
updateSelection(newSelection: any) {
|
||||
const previousSelection = this.selectedLayers.length;
|
||||
this.selectedLayers = newSelection || [];
|
||||
// Filter out invisible layers from selection
|
||||
this.selectedLayers = (newSelection || []).filter((layer: any) => layer.visible !== false);
|
||||
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
|
||||
|
||||
// Sprawdź, czy zaznaczenie faktycznie się zmieniło, aby uniknąć pętli
|
||||
|
||||
@@ -14,6 +14,7 @@ import {clearAllCanvasStates} from "./db.js";
|
||||
import {ImageCache} from "./ImageCache.js";
|
||||
import {generateUniqueFileName} from "./utils/CommonUtils.js";
|
||||
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||
import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
|
||||
import { registerImageInClipspace, startSAMDetectorMonitoring, setupSAMDetectorHook } from "./SAMDetectorIntegration.js";
|
||||
import type { ComfyNode, Layer, AddMode } from './types';
|
||||
|
||||
@@ -402,19 +403,32 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
||||
$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: MouseEvent) => {
|
||||
const button = e.target as HTMLButtonElement;
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
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') as HTMLElement;
|
||||
if (iconContainer) {
|
||||
if (canvas.maskTool.isOverlayVisible) {
|
||||
button.classList.add('primary');
|
||||
iconContainer.style.opacity = '1';
|
||||
} else {
|
||||
button.classList.remove('primary');
|
||||
iconContainer.style.opacity = '0.5';
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -539,6 +553,48 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
||||
]);
|
||||
|
||||
|
||||
// Function to create mask icon
|
||||
const createMaskIcon = (): HTMLElement => {
|
||||
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() as HTMLImageElement;
|
||||
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;
|
||||
@@ -569,11 +625,44 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
||||
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}`) as HTMLButtonElement;
|
||||
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: NodeJS.Timeout | null = null;
|
||||
|
||||
const updateOutput = async (node: ComfyNode, canvas: 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");
|
||||
@@ -584,11 +673,6 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
||||
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);
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Layer {
|
||||
zIndex: number;
|
||||
blendMode: string;
|
||||
opacity: number;
|
||||
visible: boolean;
|
||||
mask?: Float32Array;
|
||||
flipH?: boolean;
|
||||
flipV?: boolean;
|
||||
|
||||
224
src/utils/IconLoader.ts
Normal file
224
src/utils/IconLoader.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { createModuleLogger } from "./LoggerUtils.js";
|
||||
|
||||
const log = createModuleLogger('IconLoader');
|
||||
|
||||
// Define tool constants for LayerForge
|
||||
export const LAYERFORGE_TOOLS = {
|
||||
VISIBILITY: 'visibility',
|
||||
MOVE: 'move',
|
||||
ROTATE: 'rotate',
|
||||
SCALE: 'scale',
|
||||
DELETE: 'delete',
|
||||
DUPLICATE: 'duplicate',
|
||||
BLEND_MODE: 'blend_mode',
|
||||
OPACITY: 'opacity',
|
||||
MASK: 'mask',
|
||||
BRUSH: 'brush',
|
||||
ERASER: 'eraser',
|
||||
SHAPE: 'shape',
|
||||
SETTINGS: 'settings'
|
||||
} as const;
|
||||
|
||||
// SVG Icons for LayerForge tools
|
||||
const LAYERFORGE_TOOL_ICONS = {
|
||||
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.SCALE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M22,18V22H18V20H20V18H22M22,6V10H20V8H18V6H22M2,6V10H4V8H6V6H2M2,18V22H6V20H4V18H2M16,8V10H14V12H16V14H14V16H12V14H10V12H12V10H10V8H12V6H14V8H16Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.DELETE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.DUPLICATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.BLEND_MODE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20V4Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.OPACITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,20A6,6 0 0,1 6,14C6,10 12,3.25 12,3.25S18,10 18,14A6,6 0 0,1 12,20Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.MASK]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><rect x="3" y="3" width="18" height="18" rx="2" fill="none" stroke="#ffffff" stroke-width="2"/><circle cx="12" cy="12" r="5" fill="#ffffff"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.BRUSH]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M15.4565 9.67503L15.3144 9.53297C14.6661 8.90796 13.8549 8.43369 12.9235 8.18412C10.0168 7.40527 7.22541 9.05273 6.43185 12.0143C6.38901 12.1742 6.36574 12.3537 6.3285 12.8051C6.17423 14.6752 5.73449 16.0697 4.5286 17.4842C6.78847 18.3727 9.46572 18.9986 11.5016 18.9986C13.9702 18.9986 16.1644 17.3394 16.8126 14.9202C17.3306 12.9869 16.7513 11.0181 15.4565 9.67503ZM13.2886 6.21301L18.2278 2.37142C18.6259 2.0618 19.1922 2.09706 19.5488 2.45367L22.543 5.44787C22.8997 5.80448 22.9349 6.37082 22.6253 6.76891L18.7847 11.7068C19.0778 12.8951 19.0836 14.1721 18.7444 15.4379C17.8463 18.7897 14.8142 20.9986 11.5016 20.9986C8 20.9986 3.5 19.4967 1 17.9967C4.97978 14.9967 4.04722 13.1865 4.5 11.4967C5.55843 7.54658 9.34224 5.23935 13.2886 6.21301ZM16.7015 8.09161C16.7673 8.15506 16.8319 8.21964 16.8952 8.28533L18.0297 9.41984L20.5046 6.23786L18.7589 4.4921L15.5769 6.96698L16.7015 8.09161Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.ERASER]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M8.58564 8.85449L3.63589 13.8042L8.83021 18.9985L9.99985 18.9978V18.9966H11.1714L14.9496 15.2184L8.58564 8.85449ZM9.99985 7.44027L16.3638 13.8042L19.1922 10.9758L12.8283 4.61185L9.99985 7.44027ZM13.9999 18.9966H20.9999V20.9966H11.9999L8.00229 20.9991L1.51457 14.5113C1.12405 14.1208 1.12405 13.4877 1.51457 13.0971L12.1212 2.49053C12.5117 2.1 13.1449 2.1 13.5354 2.49053L21.3136 10.2687C21.7041 10.6592 21.7041 11.2924 21.3136 11.6829L13.9999 18.9966Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.SHAPE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M3 4H21C21.5523 4 22 4.44772 22 5V19C22 19.5523 21.5523 20 21 20H3C2.44772 20 2 19.5523 2 19V5C2 4.44772 2.44772 4 3 4ZM4 6V18H20V6H4Z"/></svg>')}`,
|
||||
|
||||
[LAYERFORGE_TOOLS.SETTINGS]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.22,8.95 2.27,9.22 2.46,9.37L4.5,11L5.13,18.93C5.17,19.18 5.38,19.36 5.63,19.36H18.37C18.62,19.36 18.83,19.18 18.87,18.93L19.5,11L21.54,9.37Z"/></svg>')}`
|
||||
};
|
||||
|
||||
// Tool colors for LayerForge
|
||||
const LAYERFORGE_TOOL_COLORS = {
|
||||
[LAYERFORGE_TOOLS.VISIBILITY]: '#4285F4',
|
||||
[LAYERFORGE_TOOLS.MOVE]: '#34A853',
|
||||
[LAYERFORGE_TOOLS.ROTATE]: '#FBBC05',
|
||||
[LAYERFORGE_TOOLS.SCALE]: '#EA4335',
|
||||
[LAYERFORGE_TOOLS.DELETE]: '#FF6D01',
|
||||
[LAYERFORGE_TOOLS.DUPLICATE]: '#46BDC6',
|
||||
[LAYERFORGE_TOOLS.BLEND_MODE]: '#9C27B0',
|
||||
[LAYERFORGE_TOOLS.OPACITY]: '#8BC34A',
|
||||
[LAYERFORGE_TOOLS.MASK]: '#607D8B',
|
||||
[LAYERFORGE_TOOLS.BRUSH]: '#4285F4',
|
||||
[LAYERFORGE_TOOLS.ERASER]: '#FBBC05',
|
||||
[LAYERFORGE_TOOLS.SHAPE]: '#FF6D01',
|
||||
[LAYERFORGE_TOOLS.SETTINGS]: '#F06292'
|
||||
};
|
||||
|
||||
export interface IconCache {
|
||||
[key: string]: HTMLCanvasElement | HTMLImageElement;
|
||||
}
|
||||
|
||||
export class IconLoader {
|
||||
private _iconCache: IconCache = {};
|
||||
private _loadingPromises: Map<string, Promise<HTMLImageElement>> = new Map();
|
||||
|
||||
constructor() {
|
||||
log.info('IconLoader initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload all LayerForge tool icons
|
||||
*/
|
||||
preloadToolIcons(): Promise<void> {
|
||||
log.info('Starting to preload LayerForge tool icons');
|
||||
|
||||
const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => {
|
||||
return this.loadIcon(tool);
|
||||
});
|
||||
|
||||
return Promise.all(loadPromises).then(() => {
|
||||
log.info(`Successfully preloaded ${loadPromises.length} tool icons`);
|
||||
}).catch(error => {
|
||||
log.error('Error preloading tool icons:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specific icon by tool name
|
||||
*/
|
||||
async loadIcon(tool: string): Promise<HTMLImageElement> {
|
||||
// Check if already cached
|
||||
if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) {
|
||||
return this._iconCache[tool] as HTMLImageElement;
|
||||
}
|
||||
|
||||
// Check if already loading
|
||||
if (this._loadingPromises.has(tool)) {
|
||||
return this._loadingPromises.get(tool)!;
|
||||
}
|
||||
|
||||
// Create fallback canvas first
|
||||
const fallbackCanvas = this.createFallbackIcon(tool);
|
||||
this._iconCache[tool] = fallbackCanvas;
|
||||
|
||||
// Start loading the SVG icon
|
||||
const loadPromise = new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
this._iconCache[tool] = img;
|
||||
this._loadingPromises.delete(tool);
|
||||
log.debug(`Successfully loaded icon for tool: ${tool}`);
|
||||
resolve(img);
|
||||
};
|
||||
|
||||
img.onerror = (error) => {
|
||||
log.warn(`Failed to load SVG icon for tool: ${tool}, using fallback`);
|
||||
this._loadingPromises.delete(tool);
|
||||
// Keep the fallback canvas in cache
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const iconData = LAYERFORGE_TOOL_ICONS[tool as keyof typeof LAYERFORGE_TOOL_ICONS];
|
||||
if (iconData) {
|
||||
img.src = iconData;
|
||||
} else {
|
||||
log.warn(`No icon data found for tool: ${tool}`);
|
||||
reject(new Error(`No icon data for tool: ${tool}`));
|
||||
}
|
||||
});
|
||||
|
||||
this._loadingPromises.set(tool, loadPromise);
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fallback canvas icon with colored background and text
|
||||
*/
|
||||
private createFallbackIcon(tool: string): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 24;
|
||||
canvas.height = 24;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
log.error('Failed to get canvas context for fallback icon');
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Fill background with tool color
|
||||
const color = LAYERFORGE_TOOL_COLORS[tool as keyof typeof LAYERFORGE_TOOL_COLORS] || '#888888';
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 24, 24);
|
||||
|
||||
// Add border
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(0.5, 0.5, 23, 23);
|
||||
|
||||
// Add text
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
const firstChar = tool.charAt(0).toUpperCase();
|
||||
ctx.fillText(firstChar, 12, 12);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached icon (canvas or image)
|
||||
*/
|
||||
getIcon(tool: string): HTMLCanvasElement | HTMLImageElement | null {
|
||||
return this._iconCache[tool] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if icon is loaded (as image, not fallback canvas)
|
||||
*/
|
||||
isIconLoaded(tool: string): boolean {
|
||||
return this._iconCache[tool] instanceof HTMLImageElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached icons
|
||||
*/
|
||||
clearCache(): void {
|
||||
this._iconCache = {};
|
||||
this._loadingPromises.clear();
|
||||
log.info('Icon cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available tool names
|
||||
*/
|
||||
getAvailableTools(): string[] {
|
||||
return Object.values(LAYERFORGE_TOOLS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool color
|
||||
*/
|
||||
getToolColor(tool: string): string {
|
||||
return LAYERFORGE_TOOL_COLORS[tool as keyof typeof LAYERFORGE_TOOL_COLORS] || '#888888';
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const iconLoader = new IconLoader();
|
||||
|
||||
// Export for external use
|
||||
export { LAYERFORGE_TOOL_ICONS, LAYERFORGE_TOOL_COLORS };
|
||||
Reference in New Issue
Block a user