Skip to content
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -3187,6 +3187,8 @@ function App() {
displaySize,
baseRenderSize,
originalSize,
brushSettings: brushSettings,
setBrushSettings: setBrushSettings,
});

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/panel/BottomBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 5 additions & 4 deletions src/components/panel/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down
67 changes: 60 additions & 7 deletions src/components/panel/editor/ImageCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ const ImageCanvas = memo(
const [isFadingIn, setIsFadingIn] = useState(false);
const prevImageIdentityRef = useRef(selectedImage.thumbnailUrl);

const [baseTool, setBaseTool] = useState<ToolType>(brushSettings?.tool ?? ToolType.Brush);
const retainedPatchRef = useRef<typeof interactivePatch>(null);

useEffect(() => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -1192,6 +1228,7 @@ const ImageCanvas = memo(
isToolActive,
brushImageSpaceSize,
brushStageSize,
baseTool,
],
);

Expand Down Expand Up @@ -1312,14 +1349,24 @@ 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,
points: updatedLine.points.map((p: Coord) => ({
x: p.x / scale + cropX,
y: p.y / scale + cropY,
})),
tool: brushSettings?.tool ?? ToolType.Brush,
tool: effectiveToolForPreview,
};

const existingLines = activeSubMask.parameters?.lines || [];
Expand Down Expand Up @@ -1361,6 +1408,7 @@ const ImageCanvas = memo(
isMasking,
localInitialDrawParams,
brushImageSpaceSize,
baseTool,
],
);

Expand Down Expand Up @@ -1468,14 +1516,17 @@ 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,
points: line.points.map((p: Coord) => ({
x: p.x / scale + cropX,
y: p.y / scale + cropY,
})),
tool: brushSettings?.tool ?? ToolType.Brush,
tool: effectiveToolForFinal,
};

const existingLines = activeSubMask?.parameters.lines || [];
Expand Down Expand Up @@ -1513,6 +1564,7 @@ const ImageCanvas = memo(
localInitialDrawParams,
brushImageSpaceSize,
brushStageSize,
baseTool,
]);

const handleMouseEnter = useCallback(() => {
Expand Down Expand Up @@ -1933,7 +1985,6 @@ const ImageCanvas = memo(
);
})}

{/* Visualizer for drawing new AI Bounding Box */}
{previewBox && (
<Rect
x={Math.min(previewBox.start.x, previewBox.end.x)}
Expand All @@ -1950,7 +2001,9 @@ const ImageCanvas = memo(
<Circle
listening={false}
perfectDrawEnabled={false}
stroke={brushSettings?.tool === ToolType.Eraser ? '#f43f5e' : '#0ea5e9'}
stroke={(window as any).altKeyDown ?
(baseTool === ToolType.Brush ? '#f43f5e' : '#0ea5e9') :
(baseTool === ToolType.Eraser ? '#f43f5e' : '#0ea5e9')}
radius={brushStageSize / 2}
strokeWidth={1}
x={cursorPreview.x}
Expand Down
108 changes: 67 additions & 41 deletions src/hooks/useKeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 = ({
Expand Down Expand Up @@ -101,6 +104,8 @@ export const useKeyboardShortcuts = ({
displaySize,
baseRenderSize,
originalSize,
brushSettings,
setBrushSettings,
}: KeyboardShortcutsProps) => {
useEffect(() => {
const handleKeyDown = (event: any) => {
Expand Down Expand Up @@ -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%
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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]);
}
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -478,5 +502,7 @@ export const useKeyboardShortcuts = ({
displaySize,
baseRenderSize,
originalSize,
brushSettings,
setBrushSettings,
]);
};
Loading