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 {