diff --git a/js/BatchPreviewManager.js b/js/BatchPreviewManager.js
index 9a3ceee..c84446f 100644
--- a/js/BatchPreviewManager.js
+++ b/js/BatchPreviewManager.js
@@ -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();
}
}
diff --git a/js/Canvas.js b/js/Canvas.js
index 9a536bd..1f6f2de 100644
--- a/js/Canvas.js
+++ b/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();
diff --git a/js/CanvasIO.js b/js/CanvasIO.js
index 605d113..fd1f4f8 100644
--- a/js/CanvasIO.js
+++ b/js/CanvasIO.js
@@ -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]);
diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js
index 0665d31..5381137 100644
--- a/js/CanvasLayers.js
+++ b/js/CanvasLayers.js
@@ -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);
diff --git a/js/CanvasLayersPanel.js b/js/CanvasLayersPanel.js
index 08c7fe2..c8128e3 100644
--- a/js/CanvasLayersPanel.js
+++ b/js/CanvasLayersPanel.js
@@ -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 {
@@ -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 = `
+
${layer.name}
`;
+ // 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');
diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js
index 0930373..451267a 100644
--- a/js/CanvasRenderer.js
+++ b/js/CanvasRenderer.js
@@ -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);
diff --git a/js/CanvasSelection.js b/js/CanvasSelection.js
index f691ef5..0836435 100644
--- a/js/CanvasSelection.js
+++ b/js/CanvasSelection.js
@@ -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 ||
diff --git a/js/CanvasView.js b/js/CanvasView.js
index ff5bf70..019405b 100644
--- a/js/CanvasView.js
+++ b/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);
diff --git a/js/utils/IconLoader.js b/js/utils/IconLoader.js
new file mode 100644
index 0000000..f572702
--- /dev/null
+++ b/js/utils/IconLoader.js
@@ -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('
')}`,
+ [LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.SCALE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.DELETE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.DUPLICATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.BLEND_MODE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.OPACITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.MASK]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.BRUSH]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.ERASER]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.SHAPE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`,
+ [LAYERFORGE_TOOLS.SETTINGS]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('
')}`
+};
+// 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 };
diff --git a/src/BatchPreviewManager.ts b/src/BatchPreviewManager.ts
index e1ff0d8..1236726 100644
--- a/src/BatchPreviewManager.ts
+++ b/src/BatchPreviewManager.ts
@@ -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();
}
}
diff --git a/src/Canvas.ts b/src/Canvas.ts
index 576d69a..67068ee 100644
--- a/src/Canvas.ts
+++ b/src/Canvas.ts
@@ -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;
diff --git a/src/CanvasIO.ts b/src/CanvasIO.ts
index 1fb2f2f..a0b2342 100644
--- a/src/CanvasIO.ts
+++ b/src/CanvasIO.ts
@@ -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);
diff --git a/src/CanvasLayers.ts b/src/CanvasLayers.ts
index 2d7ecb0..eb34482 100644
--- a/src/CanvasLayers.ts
+++ b/src/CanvasLayers.ts
@@ -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));
diff --git a/src/CanvasLayersPanel.ts b/src/CanvasLayersPanel.ts
index 464bdb5..d52a18c 100644
--- a/src/CanvasLayersPanel.ts
+++ b/src/CanvasLayersPanel.ts
@@ -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
{
+ 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 {
@@ -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 = `
+
${layer.name}
`;
+ // 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);
@@ -372,6 +504,16 @@ export class CanvasLayersPanel {
}
});
+ // Add visibility toggle event listener
+ const visibilityToggle = layerRow.querySelector('.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');
diff --git a/src/CanvasRenderer.ts b/src/CanvasRenderer.ts
index 8e51a42..d053542 100644
--- a/src/CanvasRenderer.ts
+++ b/src/CanvasRenderer.ts
@@ -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);
diff --git a/src/CanvasSelection.ts b/src/CanvasSelection.ts
index c0151f1..a56ccae 100644
--- a/src/CanvasSelection.ts
+++ b/src/CanvasSelection.ts
@@ -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
diff --git a/src/CanvasView.ts b/src/CanvasView.ts
index 6cf9d08..daaeae1 100644
--- a/src/CanvasView.ts
+++ b/src/CanvasView.ts
@@ -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);
diff --git a/src/types.ts b/src/types.ts
index 3be21dd..cac196e 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -16,6 +16,7 @@ export interface Layer {
zIndex: number;
blendMode: string;
opacity: number;
+ visible: boolean;
mask?: Float32Array;
flipH?: boolean;
flipV?: boolean;
diff --git a/src/utils/IconLoader.ts b/src/utils/IconLoader.ts
new file mode 100644
index 0000000..7eb82cb
--- /dev/null
+++ b/src/utils/IconLoader.ts
@@ -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('')}`,
+
+ [LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.ROTATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.SCALE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.DELETE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.DUPLICATE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.BLEND_MODE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.OPACITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.MASK]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.BRUSH]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.ERASER]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.SHAPE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
+
+ [LAYERFORGE_TOOLS.SETTINGS]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`
+};
+
+// 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> = new Map();
+
+ constructor() {
+ log.info('IconLoader initialized');
+ }
+
+ /**
+ * Preload all LayerForge tool icons
+ */
+ preloadToolIcons(): Promise {
+ 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 {
+ // 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((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 };