From 3d6e3901d05cf73ab1ff6c52c7aeaf01f0b24d41 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 3 Aug 2025 02:19:52 +0200 Subject: [PATCH] Fix button crop icon display and update functionality --- js/CanvasLayers.js | 96 +++++++++++++++------ js/CanvasView.js | 136 ++++++++++++++++------------- js/utils/IconLoader.js | 4 +- src/CanvasLayers.ts | 109 +++++++++++++++++------- src/CanvasView.ts | 183 +++++++++++++++++++++++++++------------- src/utils/IconLoader.ts | 5 +- 6 files changed, 354 insertions(+), 179 deletions(-) diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 7e90292..323b9d7 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -12,6 +12,7 @@ import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js"; const log = createModuleLogger('CanvasLayers'); export class CanvasLayers { constructor(canvas) { + this._canvasMaskCache = new Map(); this.blendMenuElement = null; this.blendMenuWorldX = 0; this.blendMenuWorldY = 0; @@ -368,13 +369,32 @@ export class CanvasLayers { const needsBlendAreaEffect = blendArea > 0; if (needsBlendAreaEffect) { log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`); - // Get or create distance field mask - const maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea); + // --- BLEND AREA MASK: Use cropped region if cropBounds is set --- + let maskCanvas = null; + let maskWidth = layer.width; + let maskHeight = layer.height; + if (layer.cropBounds && layer.originalWidth && layer.originalHeight) { + // Create a cropped canvas + const s = layer.cropBounds; + const { canvas: cropCanvas, ctx: cropCtx } = createCanvas(s.width, s.height); + if (cropCtx) { + cropCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, 0, 0, s.width, s.height); + // Generate distance field mask for the cropped region + maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea); + maskWidth = s.width; + maskHeight = s.height; + } + } + else { + // No crop, use full image + maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea); + maskWidth = layer.originalWidth || layer.width; + maskHeight = layer.originalHeight || layer.height; + } if (maskCanvas) { // Create a temporary canvas for the masked layer const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height); if (tempCtx) { - // This logic is now unified to handle both cropped and non-cropped images correctly. const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight }; if (!layer.originalWidth || !layer.originalHeight) { tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height); @@ -384,17 +404,14 @@ export class CanvasLayers { const layerScaleY = layer.height / layer.originalHeight; const dWidth = s.width * layerScaleX; const dHeight = s.height * layerScaleY; - // The destination is the top-left of the temp canvas, plus the scaled offset of the crop area. const dX = s.x * layerScaleX; const dY = s.y * layerScaleY; - // We draw into a temp canvas of size layer.width x layer.height. - // The destination rect must be positioned correctly within this temp canvas. - // The dX/dY here are offsets from the top-left of the transform frame. tempCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, dX, dY, dWidth, dHeight); + // --- Apply the distance field mask only to the visible (cropped) area --- + tempCtx.globalCompositeOperation = 'destination-in'; + // Scale the mask to match the drawn area + tempCtx.drawImage(maskCanvas, 0, 0, maskWidth, maskHeight, dX, dY, dWidth, dHeight); } - // Apply the distance field mask using destination-in for transparency effect - tempCtx.globalCompositeOperation = 'destination-in'; - tempCtx.drawImage(maskCanvas, 0, 0, layer.width, layer.height); // Draw the result ctx.globalCompositeOperation = layer.blendMode || 'normal'; ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; @@ -440,30 +457,59 @@ export class CanvasLayers { dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the transform frame) ); } - getDistanceFieldMaskSync(image, blendArea) { - // Check cache first - let imageCache = this.distanceFieldCache.get(image); - if (!imageCache) { - imageCache = new Map(); - this.distanceFieldCache.set(image, imageCache); - } - let maskCanvas = imageCache.get(blendArea); - if (!maskCanvas) { + getDistanceFieldMaskSync(imageOrCanvas, blendArea) { + // Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references) + let cacheKey = imageOrCanvas; + if (imageOrCanvas instanceof HTMLCanvasElement) { + // For canvases, use a Map on this instance (not WeakMap) + if (!this._canvasMaskCache) + this._canvasMaskCache = new Map(); + let canvasCache = this._canvasMaskCache.get(imageOrCanvas); + if (!canvasCache) { + canvasCache = new Map(); + this._canvasMaskCache.set(imageOrCanvas, canvasCache); + } + if (canvasCache.has(blendArea)) { + log.info(`Using cached distance field mask for blendArea: ${blendArea}% (canvas)`); + return canvasCache.get(blendArea) || null; + } try { - log.info(`Creating distance field mask for blendArea: ${blendArea}%`); - maskCanvas = createDistanceFieldMaskSync(image, blendArea); + log.info(`Creating distance field mask for blendArea: ${blendArea}% (canvas)`); + const maskCanvas = createDistanceFieldMaskSync(imageOrCanvas, blendArea); log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`); - imageCache.set(blendArea, maskCanvas); + canvasCache.set(blendArea, maskCanvas); + return maskCanvas; } catch (error) { - log.error('Failed to create distance field mask:', error); + log.error('Failed to create distance field mask (canvas):', error); return null; } } else { - log.info(`Using cached distance field mask for blendArea: ${blendArea}%`); + // For images, use the original WeakMap cache + let imageCache = this.distanceFieldCache.get(imageOrCanvas); + if (!imageCache) { + imageCache = new Map(); + this.distanceFieldCache.set(imageOrCanvas, imageCache); + } + let maskCanvas = imageCache.get(blendArea); + if (!maskCanvas) { + try { + log.info(`Creating distance field mask for blendArea: ${blendArea}%`); + maskCanvas = createDistanceFieldMaskSync(imageOrCanvas, blendArea); + log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`); + imageCache.set(blendArea, maskCanvas); + } + catch (error) { + log.error('Failed to create distance field mask:', error); + return null; + } + } + else { + log.info(`Using cached distance field mask for blendArea: ${blendArea}%`); + } + return maskCanvas; } - return maskCanvas; } _drawLayers(ctx, layers, options = {}) { const sortedLayers = [...layers].sort((a, b) => a.zIndex - b.zIndex); diff --git a/js/CanvasView.js b/js/CanvasView.js index c328d44..f695ca5 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -17,6 +17,32 @@ async function createCanvasWidget(node, widget, app) { onStateChange: () => updateOutput(node, canvas) }); const imageCache = new ImageCache(); + /** + * Helper function to update the icon of a switch component. + * @param knobIconEl The HTML element for the switch's knob icon. + * @param isChecked The current state of the switch (e.g., checkbox.checked). + * @param iconToolTrue The icon tool name for the 'true' state. + * @param iconToolFalse The icon tool name for the 'false' state. + * @param fallbackTrue The text fallback for the 'true' state. + * @param fallbackFalse The text fallback for the 'false' state. + */ + const updateSwitchIcon = (knobIconEl, isChecked, iconToolTrue, iconToolFalse, fallbackTrue, fallbackFalse) => { + if (!knobIconEl) + return; + const iconTool = isChecked ? iconToolTrue : iconToolFalse; + const fallbackText = isChecked ? fallbackTrue : fallbackFalse; + const icon = iconLoader.getIcon(iconTool); + knobIconEl.innerHTML = ''; // Clear previous icon + if (icon instanceof HTMLImageElement) { + const clonedIcon = icon.cloneNode(); + clonedIcon.style.width = '20px'; + clonedIcon.style.height = '20px'; + knobIconEl.appendChild(clonedIcon); + } + else { + knobIconEl.textContent = fallbackText; + } + }; const helpTooltip = $el("div.painter-tooltip", { id: `painter-help-tooltip-${node.id}`, }); @@ -158,27 +184,15 @@ async function createCanvasWidget(node, widget, app) { showTooltip(switchEl, tooltipContent); }); switchEl.addEventListener("mouseleave", hideTooltip); - // Dynamic icon and text update on toggle + // Dynamic icon update on toggle const input = switchEl.querySelector('input[type="checkbox"]'); const knobIcon = switchEl.querySelector('.switch-knob .switch-icon'); - const updateSwitchView = (isClipspace) => { - const iconTool = isClipspace ? LAYERFORGE_TOOLS.CLIPSPACE : LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD; - const icon = iconLoader.getIcon(iconTool); - if (icon instanceof HTMLImageElement) { - knobIcon.innerHTML = ''; - const clonedIcon = icon.cloneNode(); - clonedIcon.style.width = '20px'; - clonedIcon.style.height = '20px'; - knobIcon.appendChild(clonedIcon); - } - else { - knobIcon.textContent = isClipspace ? "🗂️" : "📋"; - } - }; - input.addEventListener('change', () => updateSwitchView(input.checked)); + input.addEventListener('change', () => { + updateSwitchIcon(knobIcon, input.checked, LAYERFORGE_TOOLS.CLIPSPACE, LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD, "🗂️", "📋"); + }); // Initial state iconLoader.preloadToolIcons().then(() => { - updateSwitchView(isClipspace); + updateSwitchIcon(knobIcon, isClipspace, LAYERFORGE_TOOLS.CLIPSPACE, LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD, "🗂️", "📋"); }); return switchEl; })() @@ -293,37 +307,50 @@ async function createCanvasWidget(node, widget, app) { ]), $el("div.painter-separator"), $el("div.painter-button-group", {}, [ - $el("label.clipboard-switch.requires-selection", { - id: `crop-transform-switch-${node.id}`, - title: "Toggle between Transform and Crop mode for selected layer(s)" - }, [ - $el("input", { - type: "checkbox", - checked: false, - onchange: (e) => { - const isCropMode = e.target.checked; - const selectedLayers = canvas.canvasSelection.selectedLayers; - if (selectedLayers.length === 0) - return; - selectedLayers.forEach((layer) => { - layer.cropMode = isCropMode; - if (isCropMode && !layer.cropBounds) { - layer.cropBounds = { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight }; - } - }); - canvas.saveState(); - canvas.render(); - } - }), - $el("span.switch-track"), - $el("span.switch-labels", { style: { fontSize: "11px" } }, [ - $el("span.text-clipspace", {}, ["Crop"]), - $el("span.text-system", {}, ["Transform"]) - ]), - $el("span.switch-knob", {}, [ - $el("span.switch-icon", { id: `crop-transform-icon-${node.id}` }) - ]) - ]), + (() => { + const switchEl = $el("label.clipboard-switch.requires-selection", { + id: `crop-transform-switch-${node.id}`, + title: "Toggle between Transform and Crop mode for selected layer(s)" + }, [ + $el("input", { + type: "checkbox", + checked: false, + onchange: (e) => { + const isCropMode = e.target.checked; + const selectedLayers = canvas.canvasSelection.selectedLayers; + if (selectedLayers.length === 0) + return; + selectedLayers.forEach((layer) => { + layer.cropMode = isCropMode; + if (isCropMode && !layer.cropBounds) { + layer.cropBounds = { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight }; + } + }); + canvas.saveState(); + canvas.render(); + } + }), + $el("span.switch-track"), + $el("span.switch-labels", { style: { fontSize: "11px" } }, [ + $el("span.text-clipspace", {}, ["Crop"]), + $el("span.text-system", {}, ["Transform"]) + ]), + $el("span.switch-knob", {}, [ + $el("span.switch-icon", { id: `crop-transform-icon-${node.id}` }) + ]) + ]); + const input = switchEl.querySelector('input[type="checkbox"]'); + const knobIcon = switchEl.querySelector('.switch-icon'); + input.addEventListener('change', () => { + updateSwitchIcon(knobIcon, input.checked, LAYERFORGE_TOOLS.CROP, LAYERFORGE_TOOLS.TRANSFORM, "✂️", "✥"); + }); + // Initial state + iconLoader.preloadToolIcons().then(() => { + updateSwitchIcon(knobIcon, false, // Initial state is transform + LAYERFORGE_TOOLS.CROP, LAYERFORGE_TOOLS.TRANSFORM, "✂️", "✥"); + }); + return switchEl; + })(), $el("button.painter-button.requires-selection", { textContent: "Rotate +90°", title: "Rotate selected layer(s) by +90 degrees", @@ -689,18 +716,7 @@ async function createCanvasWidget(node, widget, app) { input.checked = isCropMode; } // Update icon view - const iconTool = isCropMode ? LAYERFORGE_TOOLS.CROP : LAYERFORGE_TOOLS.TRANSFORM; - const icon = iconLoader.getIcon(iconTool); - if (icon instanceof HTMLImageElement) { - knobIcon.innerHTML = ''; - const clonedIcon = icon.cloneNode(); - clonedIcon.style.width = '20px'; - clonedIcon.style.height = '20px'; - knobIcon.appendChild(clonedIcon); - } - else { - knobIcon.textContent = isCropMode ? "✂️" : "✥"; - } + updateSwitchIcon(knobIcon, isCropMode, LAYERFORGE_TOOLS.CROP, LAYERFORGE_TOOLS.TRANSFORM, "✂️", "✥"); } } }; diff --git a/js/utils/IconLoader.js b/js/utils/IconLoader.js index 8145f09..3d51b23 100644 --- a/js/utils/IconLoader.js +++ b/js/utils/IconLoader.js @@ -25,8 +25,8 @@ export const LAYERFORGE_TOOLS = { // SVG Icons for LayerForge tools const SYSTEM_CLIPBOARD_ICON_SVG = ``; const CLIPSPACE_ICON_SVG = ` `; -const CROP_ICON_SVG = ``; -const TRANSFORM_ICON_SVG = ``; +const CROP_ICON_SVG = ''; +const TRANSFORM_ICON_SVG = ''; const LAYERFORGE_TOOL_ICONS = { [LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(SYSTEM_CLIPBOARD_ICON_SVG)}`, [LAYERFORGE_TOOLS.CLIPSPACE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(CLIPSPACE_ICON_SVG)}`, diff --git a/src/CanvasLayers.ts b/src/CanvasLayers.ts index 66fc814..bcec70f 100644 --- a/src/CanvasLayers.ts +++ b/src/CanvasLayers.ts @@ -21,6 +21,7 @@ interface BlendMode { export class CanvasLayers { private canvas: Canvas; + private _canvasMaskCache: Map> = new Map(); public clipboardManager: ClipboardManager; private blendModes: BlendMode[]; private selectedBlendMode: string | null; @@ -428,15 +429,39 @@ export class CanvasLayers { if (needsBlendAreaEffect) { log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`); - // Get or create distance field mask - const maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea); - + + // --- BLEND AREA MASK: Use cropped region if cropBounds is set --- + let maskCanvas: HTMLCanvasElement | null = null; + let maskWidth = layer.width; + let maskHeight = layer.height; + + if (layer.cropBounds && layer.originalWidth && layer.originalHeight) { + // Create a cropped canvas + const s = layer.cropBounds; + const { canvas: cropCanvas, ctx: cropCtx } = createCanvas(s.width, s.height); + if (cropCtx) { + cropCtx.drawImage( + layer.image, + s.x, s.y, s.width, s.height, + 0, 0, s.width, s.height + ); + // Generate distance field mask for the cropped region + maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea); + maskWidth = s.width; + maskHeight = s.height; + } + } else { + // No crop, use full image + maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea); + maskWidth = layer.originalWidth || layer.width; + maskHeight = layer.originalHeight || layer.height; + } + if (maskCanvas) { // Create a temporary canvas for the masked layer const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height); if (tempCtx) { - // This logic is now unified to handle both cropped and non-cropped images correctly. const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight }; if (!layer.originalWidth || !layer.originalHeight) { @@ -447,25 +472,25 @@ export class CanvasLayers { const dWidth = s.width * layerScaleX; const dHeight = s.height * layerScaleY; - - // The destination is the top-left of the temp canvas, plus the scaled offset of the crop area. const dX = s.x * layerScaleX; const dY = s.y * layerScaleY; - // We draw into a temp canvas of size layer.width x layer.height. - // The destination rect must be positioned correctly within this temp canvas. - // The dX/dY here are offsets from the top-left of the transform frame. tempCtx.drawImage( layer.image, s.x, s.y, s.width, s.height, dX, dY, dWidth, dHeight ); + + // --- Apply the distance field mask only to the visible (cropped) area --- + tempCtx.globalCompositeOperation = 'destination-in'; + // Scale the mask to match the drawn area + tempCtx.drawImage( + maskCanvas, + 0, 0, maskWidth, maskHeight, + dX, dY, dWidth, dHeight + ); } - // Apply the distance field mask using destination-in for transparency effect - tempCtx.globalCompositeOperation = 'destination-in'; - tempCtx.drawImage(maskCanvas, 0, 0, layer.width, layer.height); - // Draw the result ctx.globalCompositeOperation = layer.blendMode as any || 'normal'; ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; @@ -519,30 +544,54 @@ export class CanvasLayers { ); } - private getDistanceFieldMaskSync(image: HTMLImageElement, blendArea: number): HTMLCanvasElement | null { - // Check cache first - let imageCache = this.distanceFieldCache.get(image); - if (!imageCache) { - imageCache = new Map(); - this.distanceFieldCache.set(image, imageCache); - } - - let maskCanvas = imageCache.get(blendArea); - if (!maskCanvas) { + private getDistanceFieldMaskSync(imageOrCanvas: HTMLImageElement | HTMLCanvasElement, blendArea: number): HTMLCanvasElement | null { + // Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references) + let cacheKey: any = imageOrCanvas; + if (imageOrCanvas instanceof HTMLCanvasElement) { + // For canvases, use a Map on this instance (not WeakMap) + if (!this._canvasMaskCache) this._canvasMaskCache = new Map(); + let canvasCache = this._canvasMaskCache.get(imageOrCanvas); + if (!canvasCache) { + canvasCache = new Map(); + this._canvasMaskCache.set(imageOrCanvas, canvasCache); + } + if (canvasCache.has(blendArea)) { + log.info(`Using cached distance field mask for blendArea: ${blendArea}% (canvas)`); + return canvasCache.get(blendArea) || null; + } try { - log.info(`Creating distance field mask for blendArea: ${blendArea}%`); - maskCanvas = createDistanceFieldMaskSync(image, blendArea); + log.info(`Creating distance field mask for blendArea: ${blendArea}% (canvas)`); + const maskCanvas = createDistanceFieldMaskSync(imageOrCanvas as any, blendArea); log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`); - imageCache.set(blendArea, maskCanvas); + canvasCache.set(blendArea, maskCanvas); + return maskCanvas; } catch (error) { - log.error('Failed to create distance field mask:', error); + log.error('Failed to create distance field mask (canvas):', error); return null; } } else { - log.info(`Using cached distance field mask for blendArea: ${blendArea}%`); + // For images, use the original WeakMap cache + let imageCache = this.distanceFieldCache.get(imageOrCanvas); + if (!imageCache) { + imageCache = new Map(); + this.distanceFieldCache.set(imageOrCanvas, imageCache); + } + let maskCanvas = imageCache.get(blendArea); + if (!maskCanvas) { + try { + log.info(`Creating distance field mask for blendArea: ${blendArea}%`); + maskCanvas = createDistanceFieldMaskSync(imageOrCanvas, blendArea); + log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`); + imageCache.set(blendArea, maskCanvas); + } catch (error) { + log.error('Failed to create distance field mask:', error); + return null; + } + } else { + log.info(`Using cached distance field mask for blendArea: ${blendArea}%`); + } + return maskCanvas; } - - return maskCanvas; } private _drawLayers(ctx: CanvasRenderingContext2D, layers: Layer[], options: { offsetX?: number, offsetY?: number } = {}): void { diff --git a/src/CanvasView.ts b/src/CanvasView.ts index f4cbf0f..a5df625 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -33,6 +33,40 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp): }); const imageCache = new ImageCache(); + /** + * Helper function to update the icon of a switch component. + * @param knobIconEl The HTML element for the switch's knob icon. + * @param isChecked The current state of the switch (e.g., checkbox.checked). + * @param iconToolTrue The icon tool name for the 'true' state. + * @param iconToolFalse The icon tool name for the 'false' state. + * @param fallbackTrue The text fallback for the 'true' state. + * @param fallbackFalse The text fallback for the 'false' state. + */ + const updateSwitchIcon = ( + knobIconEl: HTMLElement, + isChecked: boolean, + iconToolTrue: string, + iconToolFalse: string, + fallbackTrue: string, + fallbackFalse: string + ) => { + if (!knobIconEl) return; + + const iconTool = isChecked ? iconToolTrue : iconToolFalse; + const fallbackText = isChecked ? fallbackTrue : fallbackFalse; + const icon = iconLoader.getIcon(iconTool); + + knobIconEl.innerHTML = ''; // Clear previous icon + if (icon instanceof HTMLImageElement) { + const clonedIcon = icon.cloneNode() as HTMLImageElement; + clonedIcon.style.width = '20px'; + clonedIcon.style.height = '20px'; + knobIconEl.appendChild(clonedIcon); + } else { + knobIconEl.textContent = fallbackText; + } + }; + const helpTooltip = $el("div.painter-tooltip", { id: `painter-help-tooltip-${node.id}`, }) as HTMLDivElement; @@ -184,29 +218,31 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp): }); switchEl.addEventListener("mouseleave", hideTooltip); - // Dynamic icon and text update on toggle + // Dynamic icon update on toggle const input = switchEl.querySelector('input[type="checkbox"]') as HTMLInputElement; const knobIcon = switchEl.querySelector('.switch-knob .switch-icon') as HTMLElement; - const updateSwitchView = (isClipspace: boolean) => { - const iconTool = isClipspace ? LAYERFORGE_TOOLS.CLIPSPACE : LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD; - const icon = iconLoader.getIcon(iconTool); - if (icon instanceof HTMLImageElement) { - knobIcon.innerHTML = ''; - const clonedIcon = icon.cloneNode() as HTMLImageElement; - clonedIcon.style.width = '20px'; - clonedIcon.style.height = '20px'; - knobIcon.appendChild(clonedIcon); - } else { - knobIcon.textContent = isClipspace ? "🗂️" : "📋"; - } - }; - - input.addEventListener('change', () => updateSwitchView(input.checked)); + input.addEventListener('change', () => { + updateSwitchIcon( + knobIcon, + input.checked, + LAYERFORGE_TOOLS.CLIPSPACE, + LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD, + "🗂️", + "📋" + ); + }); // Initial state iconLoader.preloadToolIcons().then(() => { - updateSwitchView(isClipspace); + updateSwitchIcon( + knobIcon, + isClipspace, + LAYERFORGE_TOOLS.CLIPSPACE, + LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD, + "🗂️", + "📋" + ); }); return switchEl; @@ -326,38 +362,68 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp): $el("div.painter-separator"), $el("div.painter-button-group", {}, [ - $el("label.clipboard-switch.requires-selection", { - id: `crop-transform-switch-${node.id}`, - title: "Toggle between Transform and Crop mode for selected layer(s)" - }, [ - $el("input", { - type: "checkbox", - checked: false, - onchange: (e: Event) => { - const isCropMode = (e.target as HTMLInputElement).checked; - const selectedLayers = canvas.canvasSelection.selectedLayers; - if (selectedLayers.length === 0) return; - - selectedLayers.forEach((layer: Layer) => { - layer.cropMode = isCropMode; - if (isCropMode && !layer.cropBounds) { - layer.cropBounds = { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight }; - } - }); - - canvas.saveState(); - canvas.render(); - } - }), - $el("span.switch-track"), - $el("span.switch-labels", { style: { fontSize: "11px" } }, [ - $el("span.text-clipspace", {}, ["Crop"]), - $el("span.text-system", {}, ["Transform"]) - ]), - $el("span.switch-knob", {}, [ - $el("span.switch-icon", { id: `crop-transform-icon-${node.id}`}) - ]) - ]), + (() => { + const switchEl = $el("label.clipboard-switch.requires-selection", { + id: `crop-transform-switch-${node.id}`, + title: "Toggle between Transform and Crop mode for selected layer(s)" + }, [ + $el("input", { + type: "checkbox", + checked: false, + onchange: (e: Event) => { + const isCropMode = (e.target as HTMLInputElement).checked; + const selectedLayers = canvas.canvasSelection.selectedLayers; + if (selectedLayers.length === 0) return; + + selectedLayers.forEach((layer: Layer) => { + layer.cropMode = isCropMode; + if (isCropMode && !layer.cropBounds) { + layer.cropBounds = { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight }; + } + }); + + canvas.saveState(); + canvas.render(); + } + }), + $el("span.switch-track"), + $el("span.switch-labels", { style: { fontSize: "11px" } }, [ + $el("span.text-clipspace", {}, ["Crop"]), + $el("span.text-system", {}, ["Transform"]) + ]), + $el("span.switch-knob", {}, [ + $el("span.switch-icon", { id: `crop-transform-icon-${node.id}`}) + ]) + ]); + + const input = switchEl.querySelector('input[type="checkbox"]') as HTMLInputElement; + const knobIcon = switchEl.querySelector('.switch-icon') as HTMLElement; + + input.addEventListener('change', () => { + updateSwitchIcon( + knobIcon, + input.checked, + LAYERFORGE_TOOLS.CROP, + LAYERFORGE_TOOLS.TRANSFORM, + "✂️", + "✥" + ); + }); + + // Initial state + iconLoader.preloadToolIcons().then(() => { + updateSwitchIcon( + knobIcon, + false, // Initial state is transform + LAYERFORGE_TOOLS.CROP, + LAYERFORGE_TOOLS.TRANSFORM, + "✂️", + "✥" + ); + }); + + return switchEl; + })(), $el("button.painter-button.requires-selection", { textContent: "Rotate +90°", title: "Rotate selected layer(s) by +90 degrees", @@ -738,17 +804,14 @@ $el("label.clipboard-switch.mask-switch", { } // Update icon view - const iconTool = isCropMode ? LAYERFORGE_TOOLS.CROP : LAYERFORGE_TOOLS.TRANSFORM; - const icon = iconLoader.getIcon(iconTool); - if (icon instanceof HTMLImageElement) { - knobIcon.innerHTML = ''; - const clonedIcon = icon.cloneNode() as HTMLImageElement; - clonedIcon.style.width = '20px'; - clonedIcon.style.height = '20px'; - knobIcon.appendChild(clonedIcon); - } else { - knobIcon.textContent = isCropMode ? "✂️" : "✥"; - } + updateSwitchIcon( + knobIcon, + isCropMode, + LAYERFORGE_TOOLS.CROP, + LAYERFORGE_TOOLS.TRANSFORM, + "✂️", + "✥" + ); } } }; diff --git a/src/utils/IconLoader.ts b/src/utils/IconLoader.ts index d9c627e..3eae143 100644 --- a/src/utils/IconLoader.ts +++ b/src/utils/IconLoader.ts @@ -28,8 +28,9 @@ export const LAYERFORGE_TOOLS = { // SVG Icons for LayerForge tools const SYSTEM_CLIPBOARD_ICON_SVG = ``; const CLIPSPACE_ICON_SVG = ` `; -const CROP_ICON_SVG = ``; -const TRANSFORM_ICON_SVG = ``; +const CROP_ICON_SVG = ''; +const TRANSFORM_ICON_SVG = ''; + const LAYERFORGE_TOOL_ICONS = { [LAYERFORGE_TOOLS.SYSTEM_CLIPBOARD]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(SYSTEM_CLIPBOARD_ICON_SVG)}`,