diff --git a/src/App.tsx b/src/App.tsx index 798005c21..6c5171b5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3011,7 +3011,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 ( @@ -3187,6 +3187,8 @@ function App() { displaySize, baseRenderSize, originalSize, + brushSettings: brushSettings, + setBrushSettings: setBrushSettings, }); useEffect(() => { 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 621149abe..0d433c56e 100644 --- a/src/components/panel/Editor.tsx +++ b/src/components/panel/Editor.tsx @@ -328,14 +328,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/components/panel/editor/ImageCanvas.tsx b/src/components/panel/editor/ImageCanvas.tsx index 6965eee09..c281bf974 100644 --- a/src/components/panel/editor/ImageCanvas.tsx +++ b/src/components/panel/editor/ImageCanvas.tsx @@ -761,6 +761,7 @@ const ImageCanvas = memo( const [isFadingIn, setIsFadingIn] = useState(false); const prevImageIdentityRef = useRef(selectedImage.thumbnailUrl); + const [baseTool, setBaseTool] = useState(brushSettings?.tool ?? ToolType.Brush); const retainedPatchRef = useRef(null); useEffect(() => { @@ -814,6 +815,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); @@ -1098,7 +1125,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) { @@ -1131,7 +1167,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; @@ -1156,7 +1192,7 @@ const ImageCanvas = memo( const newLine: DrawnLine = { brushSize: isBrushActive && brushSettings?.size ? brushStageSize : 2, points: [pos], - tool: toolType, + tool: effectiveTool, }; currentLine.current = newLine; } else { @@ -1192,6 +1228,7 @@ const ImageCanvas = memo( isToolActive, brushImageSpaceSize, brushStageSize, + baseTool, ], ); @@ -1312,6 +1349,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, @@ -1319,7 +1366,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 || []; @@ -1361,6 +1408,7 @@ const ImageCanvas = memo( isMasking, localInitialDrawParams, brushImageSpaceSize, + baseTool, ], ); @@ -1468,6 +1516,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, @@ -1475,7 +1526,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 || []; @@ -1513,6 +1564,7 @@ const ImageCanvas = memo( localInitialDrawParams, brushImageSpaceSize, brushStageSize, + baseTool, ]); const handleMouseEnter = useCallback(() => { @@ -1933,7 +1985,6 @@ const ImageCanvas = memo( ); })} - {/* Visualizer for drawing new AI Bounding Box */} {previewBox && ( void; } export const useKeyboardShortcuts = ({ @@ -101,6 +104,8 @@ export const useKeyboardShortcuts = ({ displaySize, baseRenderSize, originalSize, + brushSettings, + setBrushSettings, }: KeyboardShortcutsProps) => { useEffect(() => { const handleKeyDown = (event: any) => { @@ -146,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% @@ -166,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); } } @@ -248,23 +254,47 @@ export const useKeyboardShortcuts = ({ if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(key)) { event.preventDefault(); - 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; + 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; } @@ -277,31 +307,10 @@ 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]); - } } } @@ -356,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) { @@ -421,6 +431,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 +502,7 @@ export const useKeyboardShortcuts = ({ displaySize, baseRenderSize, originalSize, + brushSettings, + setBrushSettings, ]); };