From 79424eafaa4650a2d27b713a10721bd3f813f669 Mon Sep 17 00:00:00 2001 From: mm Date: Mon, 30 Mar 2026 21:10:21 +0500 Subject: [PATCH 1/3] feat: add Ctrl+Up/Down shortcuts for brush size adjustment in masks panel --- src/App.tsx | 2 ++ src/hooks/useKeyboardShortcuts.tsx | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d06d2f9fa..655b85afd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3056,6 +3056,8 @@ function App() { displaySize, baseRenderSize, originalSize, + brushSettings: brushSettings, + setBrushSettings: setBrushSettings, }); useEffect(() => { diff --git a/src/hooks/useKeyboardShortcuts.tsx b/src/hooks/useKeyboardShortcuts.tsx index 0733ce7d3..9a5f935e5 100644 --- a/src/hooks/useKeyboardShortcuts.tsx +++ b/src/hooks/useKeyboardShortcuts.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react'; import { ImageFile, Panel, SelectedImage } from '../components/ui/AppProperties'; +import { BrushSettings } from '../components/ui/AppProperties'; interface KeyboardShortcutsProps { activeAiPatchContainerId?: string | null; @@ -50,6 +51,8 @@ interface KeyboardShortcutsProps { displaySize?: { width: number; height: number }; baseRenderSize?: { width: number; height: number }; originalSize?: { width: number; height: number }; + brushSettings: BrushSettings | null; + setBrushSettings: (settings: BrushSettings) => void; } export const useKeyboardShortcuts = ({ @@ -101,6 +104,8 @@ export const useKeyboardShortcuts = ({ displaySize, baseRenderSize, originalSize, + brushSettings, + setBrushSettings, }: KeyboardShortcutsProps) => { useEffect(() => { const handleKeyDown = (event: any) => { @@ -246,8 +251,9 @@ export const useKeyboardShortcuts = ({ } if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(key)) { - event.preventDefault(); - + event.preventDefault(); + + if (!isCtrl) { if (selectedImage) { if (key === 'arrowup' || key === 'arrowdown') { // Calculate current zoom percentage relative to original @@ -304,6 +310,7 @@ export const useKeyboardShortcuts = ({ } } } + } if (code.startsWith('Digit') && !isCtrl) { event.preventDefault(); @@ -421,6 +428,20 @@ export const useKeyboardShortcuts = ({ event.preventDefault(); handleZoomChange(Math.max(currentPercent / 1.2, 0.1)); break; + case 'arrowup': + event.preventDefault(); + if (brushSettings && activeRightPanel === Panel.Masks) { + const newSize = Math.min((brushSettings.size || 50) + 10, 200); + setBrushSettings({ ...brushSettings, size: newSize }); + } + break; + case 'arrowdown': + event.preventDefault(); + if (brushSettings && activeRightPanel === Panel.Masks) { + const newSize = Math.max((brushSettings.size || 50) - 10, 1); + setBrushSettings({ ...brushSettings, size: newSize }); + } + break; default: break; } @@ -478,5 +499,7 @@ export const useKeyboardShortcuts = ({ displaySize, baseRenderSize, originalSize, + brushSettings, + setBrushSettings, ]); }; From 1617b70b49d680db57a21e90b55b81adf9b7e10c Mon Sep 17 00:00:00 2001 From: mm Date: Mon, 30 Mar 2026 23:56:59 +0500 Subject: [PATCH 2/3] fix: prevent zoom conflict with brush resize shortcuts and improve zoom behavior --- src/App.tsx | 2 +- src/components/panel/BottomBar.tsx | 2 +- src/components/panel/Editor.tsx | 9 +-- src/hooks/useKeyboardShortcuts.tsx | 93 +++++++++++++++--------------- 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 655b85afd..16437f662 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2880,7 +2880,7 @@ function App() { targetZoomPercent = zoomValue / dpr; } - targetZoomPercent = Math.max(0.1 / dpr, Math.min(4.0, targetZoomPercent)); + targetZoomPercent = Math.max(0.1 / dpr, Math.min(2.0 / dpr, targetZoomPercent)); let transformZoom = 1.0; if ( diff --git a/src/components/panel/BottomBar.tsx b/src/components/panel/BottomBar.tsx index 750929d73..28c8aea89 100644 --- a/src/components/panel/BottomBar.tsx +++ b/src/components/panel/BottomBar.tsx @@ -214,7 +214,7 @@ export default function BottomBar({ const value = parseFloat(percentInputValue); if (!isNaN(value)) { const originalPercent = value / 100; - const clampedPercent = Math.max(0.05, Math.min(4.0, originalPercent)); + const clampedPercent = Math.max(0.1, Math.min(2.0, originalPercent)); onZoomChange(clampedPercent); } setIsEditingPercent(false); diff --git a/src/components/panel/Editor.tsx b/src/components/panel/Editor.tsx index daeda0b14..aaa763d0a 100644 --- a/src/components/panel/Editor.tsx +++ b/src/components/panel/Editor.tsx @@ -326,14 +326,15 @@ export default function Editor({ return { minScale: 0.1, maxScale: 20 }; } + const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1; const scaleFor100Percent = 1 / imageRenderSize.scale; - const minScale = 0.1 * scaleFor100Percent; - const maxScale = 2.0 * scaleFor100Percent; + const minScale = (0.1 / dpr) * scaleFor100Percent; + const maxScale = (2.0 / dpr) * scaleFor100Percent; return { - minScale: Math.max(0.1, minScale), - maxScale: Math.max(20, maxScale), + minScale, + maxScale, }; }, [selectedImage, imageRenderSize.scale, originalSize]); diff --git a/src/hooks/useKeyboardShortcuts.tsx b/src/hooks/useKeyboardShortcuts.tsx index 9a5f935e5..606c58204 100644 --- a/src/hooks/useKeyboardShortcuts.tsx +++ b/src/hooks/useKeyboardShortcuts.tsx @@ -151,9 +151,10 @@ export const useKeyboardShortcuts = ({ event.preventDefault(); // Calculate current zoom percentage relative to original + const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1; const currentPercent = originalSize && originalSize.width > 0 && displaySize && displaySize.width > 0 - ? Math.round((displaySize.width / originalSize.width) * 100) + ? Math.round(((displaySize.width * dpr) / originalSize.width) * 100) : 100; // Toggle between fit-to-window, 2x fit-to-window (if < 100%), and 100% @@ -171,10 +172,10 @@ export const useKeyboardShortcuts = ({ if (originalAspect > baseAspect) { // Width is limiting (landscape) - fitPercent = Math.round((baseRenderSize.width / originalSize.width) * 100); + fitPercent = Math.round(((baseRenderSize.width * dpr) / originalSize.width) * 100); } else { // Height is limiting (portrait) - fitPercent = Math.round((baseRenderSize.height / originalSize.height) * 100); + fitPercent = Math.round(((baseRenderSize.height * dpr) / originalSize.height) * 100); } } @@ -251,26 +252,49 @@ export const useKeyboardShortcuts = ({ } if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(key)) { - event.preventDefault(); - - if (!isCtrl) { - if (selectedImage) { - if (key === 'arrowup' || key === 'arrowdown') { - // Calculate current zoom percentage relative to original - const currentPercent = - originalSize && originalSize.width > 0 && displaySize && displaySize.width > 0 - ? displaySize.width / originalSize.width - : 1.0; + event.preventDefault(); + + if (!isCtrl) { + if (selectedImage) { + if (key === 'arrowup' || key === 'arrowdown') { + const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1; + // Calculate current zoom percentage relative to original + const currentPercent = + originalSize && originalSize.width > 0 && displaySize && displaySize.width > 0 + ? (displaySize.width * dpr) / originalSize.width + : 1.0; - const step = 0.1; // 10% steps - const newPercent = key === 'arrowup' ? currentPercent + step : currentPercent - step; + const step = 0.1; // 10% steps + const newPercent = key === 'arrowup' ? currentPercent + step : currentPercent - step; - // Clamp to 10%-200% of original size - const clampedPercent = Math.max(0.1, Math.min(newPercent, 2.0)); - handleZoomChange(clampedPercent); + // Clamp to 10%-200% of original size + const clampedPercent = Math.max(0.1, Math.min(newPercent, 2.0)); + handleZoomChange(clampedPercent); + } else { + const isNext = key === 'arrowright'; + const currentIndex = sortedImageList.findIndex((img: ImageFile) => img.path === selectedImage.path); + if (currentIndex === -1) { + return; + } + let nextIndex = isNext ? currentIndex + 1 : currentIndex - 1; + if (nextIndex >= sortedImageList.length) { + nextIndex = 0; + } + if (nextIndex < 0) { + nextIndex = sortedImageList.length - 1; + } + const nextImage = sortedImageList[nextIndex]; + if (nextImage) { + handleImageSelect(nextImage.path); + } + } } else { - const isNext = key === 'arrowright'; - const currentIndex = sortedImageList.findIndex((img: ImageFile) => img.path === selectedImage.path); + const isNext = key === 'arrowright' || key === 'arrowdown'; + const activePath = libraryActivePath; + if (!activePath || sortedImageList.length === 0) { + return; + } + const currentIndex = sortedImageList.findIndex((img: ImageFile) => img.path === activePath); if (currentIndex === -1) { return; } @@ -283,34 +307,12 @@ export const useKeyboardShortcuts = ({ } const nextImage = sortedImageList[nextIndex]; if (nextImage) { - handleImageSelect(nextImage.path); + setLibraryActivePath(nextImage.path); + setMultiSelectedPaths([nextImage.path]); } } - } else { - const isNext = key === 'arrowright' || key === 'arrowdown'; - const activePath = libraryActivePath; - if (!activePath || sortedImageList.length === 0) { - return; - } - const currentIndex = sortedImageList.findIndex((img: ImageFile) => img.path === activePath); - if (currentIndex === -1) { - return; - } - let nextIndex = isNext ? currentIndex + 1 : currentIndex - 1; - if (nextIndex >= sortedImageList.length) { - nextIndex = 0; - } - if (nextIndex < 0) { - nextIndex = sortedImageList.length - 1; - } - const nextImage = sortedImageList[nextIndex]; - if (nextImage) { - setLibraryActivePath(nextImage.path); - setMultiSelectedPaths([nextImage.path]); - } } } - } if (code.startsWith('Digit') && !isCtrl) { event.preventDefault(); @@ -363,9 +365,10 @@ export const useKeyboardShortcuts = ({ } if (isCtrl) { + const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1; const currentPercent = originalSize && originalSize.width > 0 && displaySize && displaySize.width > 0 - ? displaySize.width / originalSize.width + ? (displaySize.width * dpr) / originalSize.width : 1.0; switch (key) { From 3c23a8732937a461f7f270fa4aaeb6301b600fe2 Mon Sep 17 00:00:00 2001 From: mm Date: Tue, 31 Mar 2026 17:55:45 +0500 Subject: [PATCH 3/3] feat: add Alt key to temporarily toggle between Brush and Eraser modes --- src/components/panel/editor/ImageCanvas.tsx | 68 ++++++++++++++++++--- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/src/components/panel/editor/ImageCanvas.tsx b/src/components/panel/editor/ImageCanvas.tsx index 4ef492986..5528b6eea 100644 --- a/src/components/panel/editor/ImageCanvas.tsx +++ b/src/components/panel/editor/ImageCanvas.tsx @@ -759,6 +759,8 @@ const ImageCanvas = memo( const [isFadingIn, setIsFadingIn] = useState(false); const prevImageIdentityRef = useRef(selectedImage.thumbnailUrl); + const [baseTool, setBaseTool] = useState(brushSettings?.tool ?? ToolType.Brush); + useEffect(() => { const newSrc = finalPreviewUrl || selectedImage.thumbnailUrl; const isNewImage = prevImageIdentityRef.current !== selectedImage.thumbnailUrl; @@ -804,6 +806,32 @@ const ImageCanvas = memo( } }, [finalPreviewUrl, selectedImage.thumbnailUrl, isSliderDragging]); + useEffect(() => { + setBaseTool(brushSettings?.tool ?? ToolType.Brush); + }, [brushSettings?.tool]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Alt') { + e.preventDefault(); + (window as any).altKeyDown = true; + } + }; + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Alt') { + e.preventDefault(); + (window as any).altKeyDown = false; + } + }; + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + delete (window as any).altKeyDown; + }; + }, []); + const activeContainer = useMemo(() => { if (isMasking) { return adjustments.masks.find((c: MaskContainer) => c.id === activeMaskContainerId); @@ -1088,7 +1116,16 @@ const ImageCanvas = memo( return; } - const toolType = isAiSubjectActive ? ToolType.AiSeletor : ToolType.Brush; + const isAltPressed = e.evt.altKey; + let effectiveTool; + + if (isAiSubjectActive) { + effectiveTool = ToolType.AiSeletor; + } else if (isAltPressed) { + effectiveTool = baseTool === ToolType.Brush ? ToolType.Eraser : ToolType.Brush; + } else { + effectiveTool = baseTool; + } const isShiftClick = isBrushActive && e.evt.shiftKey && lastBrushPoint.current; if (isShiftClick) { @@ -1121,7 +1158,7 @@ const ImageCanvas = memo( brushSize: brushImageSpaceSize, feather: brushSettings?.feather ? brushSettings?.feather / 100 : 0, points: interpolatedPoints, - tool: brushSettings?.tool ?? ToolType.Brush, + tool: effectiveTool, }; const activeId = isMasking ? activeMaskId : activeAiSubMaskId; @@ -1146,7 +1183,7 @@ const ImageCanvas = memo( const newLine: DrawnLine = { brushSize: isBrushActive && brushSettings?.size ? brushStageSize : 2, points: [pos], - tool: toolType, + tool: effectiveTool, }; currentLine.current = newLine; } else { @@ -1182,6 +1219,7 @@ const ImageCanvas = memo( isToolActive, brushImageSpaceSize, brushStageSize, + baseTool, ], ); @@ -1302,6 +1340,16 @@ const ImageCanvas = memo( const cropX = crop ? (isPercent ? (crop.x / 100) * effectiveImageDimensions.width : crop.x) : 0; const cropY = crop ? (isPercent ? (crop.y / 100) * effectiveImageDimensions.height : crop.y) : 0; + const isAltPressedDuringMove = (window as any).altKeyDown || false; + let effectiveToolForPreview; + + if (isAltPressedDuringMove) { + // Alt toggles: Brush -> Eraser, Eraser -> Brush + effectiveToolForPreview = baseTool === ToolType.Brush ? ToolType.Eraser : ToolType.Brush; + } else { + effectiveToolForPreview = baseTool; + } + const imageSpaceLine: DrawnLine = { brushSize: brushImageSpaceSize, feather: brushSettings?.feather ? brushSettings?.feather / 100 : 0, @@ -1309,7 +1357,7 @@ const ImageCanvas = memo( x: p.x / scale + cropX, y: p.y / scale + cropY, })), - tool: brushSettings?.tool ?? ToolType.Brush, + tool: effectiveToolForPreview, }; const existingLines = activeSubMask.parameters?.lines || []; @@ -1351,6 +1399,7 @@ const ImageCanvas = memo( isMasking, localInitialDrawParams, brushImageSpaceSize, + baseTool, ], ); @@ -1458,6 +1507,9 @@ const ImageCanvas = memo( const activeId = isMasking ? activeMaskId : activeAiSubMaskId; if (isBrushActive) { + const wasAltPressed = (window as any).altKeyDown || false; + const effectiveToolForFinal = wasAltPressed ? (baseTool === ToolType.Brush ? ToolType.Eraser : ToolType.Brush) : baseTool; + const imageSpaceLine: DrawnLine = { brushSize: brushImageSpaceSize, feather: brushSettings?.feather ? brushSettings?.feather / 100 : 0, @@ -1465,7 +1517,7 @@ const ImageCanvas = memo( x: p.x / scale + cropX, y: p.y / scale + cropY, })), - tool: brushSettings?.tool ?? ToolType.Brush, + tool: effectiveToolForFinal, }; const existingLines = activeSubMask?.parameters.lines || []; @@ -1503,6 +1555,7 @@ const ImageCanvas = memo( localInitialDrawParams, brushImageSpaceSize, brushStageSize, + baseTool, ]); const handleMouseEnter = useCallback(() => { @@ -1903,7 +1956,6 @@ const ImageCanvas = memo( ); })} - {/* Visualizer for drawing new AI Bounding Box */} {previewBox && (