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)}`,