diff --git a/js/CanvasView.js b/js/CanvasView.js
index 0320c95..c328d44 100644
--- a/js/CanvasView.js
+++ b/js/CanvasView.js
@@ -293,43 +293,37 @@ async function createCanvasWidget(node, widget, app) {
]),
$el("div.painter-separator"),
$el("div.painter-button-group", {}, [
- $el("button.painter-button.requires-selection", {
- id: `crop-mode-btn-${node.id}`,
- textContent: "Crop Mode",
- title: "Toggle crop mode for selected layer(s)",
- onclick: () => {
- const cropBtn = controlPanel.querySelector(`#crop-mode-btn-${node.id}`);
- const selectedLayers = canvas.canvasSelection.selectedLayers;
- if (selectedLayers.length === 0)
- return;
- // Toggle crop mode for all selected layers
- const firstLayer = selectedLayers[0];
- const newCropMode = !firstLayer.cropMode;
- selectedLayers.forEach((layer) => {
- layer.cropMode = newCropMode;
- // Initialize crop bounds if entering crop mode
- if (newCropMode && !layer.cropBounds) {
- layer.cropBounds = {
- x: 0,
- y: 0,
- width: layer.originalWidth,
- height: layer.originalHeight
- };
- }
- });
- // Update button appearance
- if (newCropMode) {
- cropBtn.classList.add('primary');
- cropBtn.title = "Exit crop mode for selected layer(s)";
+ $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();
}
- else {
- cropBtn.classList.remove('primary');
- cropBtn.title = "Toggle crop mode for selected layer(s)";
- }
- 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}` })
+ ])
+ ]),
$el("button.painter-button.requires-selection", {
textContent: "Rotate +90°",
title: "Rotate selected layer(s) by +90 degrees",
@@ -666,19 +660,49 @@ async function createCanvasWidget(node, widget, app) {
const updateButtonStates = () => {
const selectionCount = canvas.canvasSelection.selectedLayers.length;
const hasSelection = selectionCount > 0;
- controlPanel.querySelectorAll('.requires-selection').forEach((btn) => {
- const button = btn;
- if (button.textContent === 'Fuse') {
- button.disabled = selectionCount < 2;
- }
- else {
- button.disabled = !hasSelection;
+ // --- Handle Standard Buttons ---
+ controlPanel.querySelectorAll('.requires-selection').forEach((el) => {
+ if (el.tagName === 'BUTTON') {
+ if (el.textContent === 'Fuse') {
+ el.disabled = selectionCount < 2;
+ }
+ else {
+ el.disabled = !hasSelection;
+ }
}
});
const mattingBtn = controlPanel.querySelector('.matting-button');
if (mattingBtn && !mattingBtn.classList.contains('loading')) {
mattingBtn.disabled = selectionCount !== 1;
}
+ // --- Handle Crop/Transform Switch ---
+ const switchEl = controlPanel.querySelector(`#crop-transform-switch-${node.id}`);
+ if (switchEl) {
+ const input = switchEl.querySelector('input');
+ const knobIcon = switchEl.querySelector('.switch-icon');
+ const isDisabled = !hasSelection;
+ switchEl.classList.toggle('disabled', isDisabled);
+ input.disabled = isDisabled;
+ if (!isDisabled) {
+ const isCropMode = canvas.canvasSelection.selectedLayers[0].cropMode || false;
+ if (input.checked !== isCropMode) {
+ 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 ? "✂️" : "✥";
+ }
+ }
+ }
};
canvas.canvasSelection.onSelectionChange = updateButtonStates;
const undoButton = controlPanel.querySelector(`#undo-button-${node.id}`);
diff --git a/js/css/canvas_view.css b/js/css/canvas_view.css
index f4ce039..ad082f3 100644
--- a/js/css/canvas_view.css
+++ b/js/css/canvas_view.css
@@ -332,6 +332,20 @@
opacity: 0;
}
+/* Disabled state for switch */
+.clipboard-switch.disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+ background: #3a3a3a !important; /* Override gradient */
+ border-color: #4a4a4a !important;
+ transform: none !important;
+ box-shadow: none !important;
+}
+
+.clipboard-switch.disabled .switch-knob {
+ background-color: #4a4a4a !important;
+}
+
.painter-separator {
width: 1px;
diff --git a/js/utils/IconLoader.js b/js/utils/IconLoader.js
index 3fde907..8145f09 100644
--- a/js/utils/IconLoader.js
+++ b/js/utils/IconLoader.js
@@ -19,13 +19,19 @@ export const LAYERFORGE_TOOLS = {
SETTINGS: 'settings',
SYSTEM_CLIPBOARD: 'system_clipboard',
CLIPSPACE: 'clipspace',
+ CROP: 'crop',
+ TRANSFORM: 'transform',
};
// SVG Icons for LayerForge tools
const SYSTEM_CLIPBOARD_ICON_SVG = ``;
const CLIPSPACE_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)}`,
+ [LAYERFORGE_TOOLS.CROP]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(CROP_ICON_SVG)}`,
+ [LAYERFORGE_TOOLS.TRANSFORM]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(TRANSFORM_ICON_SVG)}`,
[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('')}`,
@@ -54,7 +60,9 @@ const LAYERFORGE_TOOL_COLORS = {
[LAYERFORGE_TOOLS.BRUSH]: '#4285F4',
[LAYERFORGE_TOOLS.ERASER]: '#FBBC05',
[LAYERFORGE_TOOLS.SHAPE]: '#FF6D01',
- [LAYERFORGE_TOOLS.SETTINGS]: '#F06292'
+ [LAYERFORGE_TOOLS.SETTINGS]: '#F06292',
+ [LAYERFORGE_TOOLS.CROP]: '#EA4335',
+ [LAYERFORGE_TOOLS.TRANSFORM]: '#34A853',
};
export class IconLoader {
constructor() {
diff --git a/src/CanvasView.ts b/src/CanvasView.ts
index dfa2e7c..f4cbf0f 100644
--- a/src/CanvasView.ts
+++ b/src/CanvasView.ts
@@ -326,47 +326,38 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
$el("div.painter-separator"),
$el("div.painter-button-group", {}, [
- $el("button.painter-button.requires-selection", {
- id: `crop-mode-btn-${node.id}`,
- textContent: "Crop Mode",
- title: "Toggle crop mode for selected layer(s)",
- onclick: () => {
- const cropBtn = controlPanel.querySelector(`#crop-mode-btn-${node.id}`) as HTMLButtonElement;
- const selectedLayers = canvas.canvasSelection.selectedLayers;
-
- if (selectedLayers.length === 0) return;
-
- // Toggle crop mode for all selected layers
- const firstLayer = selectedLayers[0];
- const newCropMode = !firstLayer.cropMode;
-
- selectedLayers.forEach((layer: Layer) => {
- layer.cropMode = newCropMode;
+ $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;
- // Initialize crop bounds if entering crop mode
- if (newCropMode && !layer.cropBounds) {
- layer.cropBounds = {
- x: 0,
- y: 0,
- width: layer.originalWidth,
- height: layer.originalHeight
- };
- }
- });
-
- // Update button appearance
- if (newCropMode) {
- cropBtn.classList.add('primary');
- cropBtn.title = "Exit crop mode for selected layer(s)";
- } else {
- cropBtn.classList.remove('primary');
- cropBtn.title = "Toggle crop mode for selected layer(s)";
+ 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();
}
-
- 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}`})
+ ])
+ ]),
$el("button.painter-button.requires-selection", {
textContent: "Rotate +90°",
title: "Rotate selected layer(s) by +90 degrees",
@@ -713,18 +704,53 @@ $el("label.clipboard-switch.mask-switch", {
const updateButtonStates = () => {
const selectionCount = canvas.canvasSelection.selectedLayers.length;
const hasSelection = selectionCount > 0;
- controlPanel.querySelectorAll('.requires-selection').forEach((btn: any) => {
- const button = btn as HTMLButtonElement;
- if (button.textContent === 'Fuse') {
- button.disabled = selectionCount < 2;
- } else {
- button.disabled = !hasSelection;
+
+ // --- Handle Standard Buttons ---
+ controlPanel.querySelectorAll('.requires-selection').forEach((el: any) => {
+ if (el.tagName === 'BUTTON') {
+ if (el.textContent === 'Fuse') {
+ el.disabled = selectionCount < 2;
+ } else {
+ el.disabled = !hasSelection;
+ }
}
});
+
const mattingBtn = controlPanel.querySelector('.matting-button') as HTMLButtonElement;
if (mattingBtn && !mattingBtn.classList.contains('loading')) {
mattingBtn.disabled = selectionCount !== 1;
}
+
+ // --- Handle Crop/Transform Switch ---
+ const switchEl = controlPanel.querySelector(`#crop-transform-switch-${node.id}`) as HTMLLabelElement;
+ if (switchEl) {
+ const input = switchEl.querySelector('input') as HTMLInputElement;
+ const knobIcon = switchEl.querySelector('.switch-icon') as HTMLElement;
+
+ const isDisabled = !hasSelection;
+ switchEl.classList.toggle('disabled', isDisabled);
+ input.disabled = isDisabled;
+
+ if (!isDisabled) {
+ const isCropMode = canvas.canvasSelection.selectedLayers[0].cropMode || false;
+ if (input.checked !== isCropMode) {
+ 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() as HTMLImageElement;
+ clonedIcon.style.width = '20px';
+ clonedIcon.style.height = '20px';
+ knobIcon.appendChild(clonedIcon);
+ } else {
+ knobIcon.textContent = isCropMode ? "✂️" : "✥";
+ }
+ }
+ }
};
canvas.canvasSelection.onSelectionChange = updateButtonStates;
diff --git a/src/css/canvas_view.css b/src/css/canvas_view.css
index f4ce039..ad082f3 100644
--- a/src/css/canvas_view.css
+++ b/src/css/canvas_view.css
@@ -332,6 +332,20 @@
opacity: 0;
}
+/* Disabled state for switch */
+.clipboard-switch.disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+ background: #3a3a3a !important; /* Override gradient */
+ border-color: #4a4a4a !important;
+ transform: none !important;
+ box-shadow: none !important;
+}
+
+.clipboard-switch.disabled .switch-knob {
+ background-color: #4a4a4a !important;
+}
+
.painter-separator {
width: 1px;
diff --git a/src/utils/IconLoader.ts b/src/utils/IconLoader.ts
index b7b78fa..d9c627e 100644
--- a/src/utils/IconLoader.ts
+++ b/src/utils/IconLoader.ts
@@ -13,7 +13,7 @@ export const LAYERFORGE_TOOLS = {
DELETE: 'delete',
DUPLICATE: 'duplicate',
BLEND_MODE: 'blend_mode',
- OPACITY: 'opacity',
+ OPACITY: 'opacity',
MASK: 'mask',
BRUSH: 'brush',
ERASER: 'eraser',
@@ -21,16 +21,21 @@ export const LAYERFORGE_TOOLS = {
SETTINGS: 'settings',
SYSTEM_CLIPBOARD: 'system_clipboard',
CLIPSPACE: 'clipspace',
+ CROP: 'crop',
+ TRANSFORM: 'transform',
} as const;
// SVG Icons for LayerForge tools
const SYSTEM_CLIPBOARD_ICON_SVG = ``;
const CLIPSPACE_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)}`,
+ [LAYERFORGE_TOOLS.CROP]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(CROP_ICON_SVG)}`,
+ [LAYERFORGE_TOOLS.TRANSFORM]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(TRANSFORM_ICON_SVG)}`,
[LAYERFORGE_TOOLS.VISIBILITY]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
[LAYERFORGE_TOOLS.MOVE]: `data:image/svg+xml;charset=utf-8,${encodeURIComponent('')}`,
@@ -72,7 +77,9 @@ const LAYERFORGE_TOOL_COLORS = {
[LAYERFORGE_TOOLS.BRUSH]: '#4285F4',
[LAYERFORGE_TOOLS.ERASER]: '#FBBC05',
[LAYERFORGE_TOOLS.SHAPE]: '#FF6D01',
- [LAYERFORGE_TOOLS.SETTINGS]: '#F06292'
+ [LAYERFORGE_TOOLS.SETTINGS]: '#F06292',
+ [LAYERFORGE_TOOLS.CROP]: '#EA4335',
+ [LAYERFORGE_TOOLS.TRANSFORM]: '#34A853',
};
export interface IconCache {