diff --git a/web_app/package-lock.json b/web_app/package-lock.json index 95d2ec5..c553fb7 100644 --- a/web_app/package-lock.json +++ b/web_app/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", @@ -1913,6 +1914,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", + "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", diff --git a/web_app/package.json b/web_app/package.json index 3a5e2e7..0ba3ac3 100644 --- a/web_app/package.json +++ b/web_app/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", diff --git a/web_app/src/components/Editor.tsx b/web_app/src/components/Editor.tsx index 3c26d04..0de861a 100644 --- a/web_app/src/components/Editor.tsx +++ b/web_app/src/components/Editor.tsx @@ -28,7 +28,7 @@ import { useStore } from "@/lib/states" import Cropper from "./Cropper" import { InteractiveSegPoints } from "./InteractiveSeg" import useHotKey from "@/hooks/useHotkey" -import Extender from "./Expender" +import Extender from "./Extender" const TOOLBAR_HEIGHT = 200 const MIN_BRUSH_SIZE = 10 @@ -221,7 +221,9 @@ export default function Editor(props: EditorProps) { } const [width, height] = getCurrentWidthHeight() - setImageSize(width, height) + if (width !== imageWidth || height !== imageHeight) { + setImageSize(width, height) + } const rW = windowSize.width / width const rH = (windowSize.height - TOOLBAR_HEIGHT) / height @@ -255,6 +257,8 @@ export default function Editor(props: EditorProps) { } }, [ viewportRef, + imageHeight, + imageWidth, original, isOriginalLoaded, windowSize, @@ -619,19 +623,6 @@ export default function Editor(props: EditorProps) { } ) - useKeyPressEvent( - "Alt", - (ev) => { - ev?.preventDefault() - ev?.stopPropagation() - // TODO: mouse scroll increase/decrease brush size - }, - (ev) => { - ev?.preventDefault() - ev?.stopPropagation() - } - ) - const getCurScale = (): number => { let s = minScale if (viewportRef.current?.instance?.transformState.scale !== undefined) { @@ -782,19 +773,17 @@ export default function Editor(props: EditorProps) { {interactiveSegState.isInteractiveSeg ? ( diff --git a/web_app/src/components/Expender.tsx b/web_app/src/components/Extender.tsx similarity index 66% rename from web_app/src/components/Expender.tsx rename to web_app/src/components/Extender.tsx index ea58c93..14f71fd 100644 --- a/web_app/src/components/Expender.tsx +++ b/web_app/src/components/Extender.tsx @@ -1,3 +1,4 @@ +import { EXTENDER_ALL, EXTENDER_X, EXTENDER_Y } from "@/lib/const" import { useStore } from "@/lib/states" import { cn } from "@/lib/utils" import React, { useEffect, useState } from "react" @@ -18,8 +19,6 @@ interface EVData { } interface Props { - maxHeight: number - maxWidth: number scale: number minHeight: number minWidth: number @@ -30,66 +29,44 @@ const clamp = ( newPos: number, newLength: number, oldPos: number, - oldLength: number, - minLength: number, - maxLength: number + minLength: number ) => { - return [newPos, newLength] - if (newPos !== oldPos && newLength === oldLength) { - if (newPos < 0) { - return [0, oldLength] - } - if (newPos + newLength > maxLength) { - return [maxLength - oldLength, oldLength] - } - } else { - if (newLength < minLength) { - if (newPos === oldPos) { - return [newPos, minLength] - } - return [newPos + newLength - minLength, minLength] - } - if (newPos < 0) { - return [0, newPos + newLength] - } - if (newPos + newLength > maxLength) { - return [newPos, maxLength - newPos] + if (newLength < minLength) { + if (newPos === oldPos) { + return [newPos, minLength] } + return [newPos + newLength - minLength, minLength] } return [newPos, newLength] } const Extender = (props: Props) => { - const { minHeight, minWidth, maxHeight, maxWidth, scale, show } = props + const { minHeight, minWidth, scale, show } = props const [ - imageWidth, - imageHeight, isInpainting, + imageHeight, + imageWdith, { x, y, width, height }, setX, setY, setWidth, setHeight, + extenderDirection, ] = useStore((state) => [ - state.imageWidth, - state.imageHeight, state.isInpainting, + state.imageHeight, + state.imageWidth, state.extenderState, state.setExtenderX, state.setExtenderY, state.setExtenderWidth, state.setExtenderHeight, + state.settings.extenderDirection, ]) const [isResizing, setIsResizing] = useState(false) - const [isMoving, setIsMoving] = useState(false) - - useEffect(() => { - setX(Math.round((maxWidth - 512) / 2)) - setY(Math.round((maxHeight - 512) / 2)) - }, [maxHeight, maxWidth, imageWidth, imageHeight]) const [evData, setEVData] = useState({ initX: 0, @@ -106,11 +83,11 @@ const Extender = (props: Props) => { } const clampLeftRight = (newX: number, newWidth: number) => { - return clamp(newX, newWidth, x, width, minWidth, maxWidth) + return clamp(newX, newWidth, x, minWidth) } const clampTopBottom = (newY: number, newHeight: number) => { - return clamp(newY, newHeight, y, height, minHeight, maxHeight) + return clamp(newY, newHeight, y, minHeight) } const onPointerMove = (e: PointerEvent) => { @@ -126,14 +103,31 @@ const Extender = (props: Props) => { const moveTop = () => { const newHeight = evData.initHeight - offsetY const newY = evData.initY + offsetY - const [clampedY, clampedHeight] = clampTopBottom(newY, newHeight) + let clampedY = newY + let clampedHeight = newHeight + if (extenderDirection === EXTENDER_ALL) { + if (clampedY > 0) { + clampedY = 0 + clampedHeight = evData.initHeight - Math.abs(evData.initY) + } + } else { + const clamped = clampTopBottom(newY, newHeight) + clampedY = clamped[0] + clampedHeight = clamped[1] + } setHeight(clampedHeight) setY(clampedY) } const moveBottom = () => { const newHeight = evData.initHeight + offsetY - const [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight) + let [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight) + + if (extenderDirection === EXTENDER_ALL) { + if (clampedY + clampedHeight < imageHeight) { + clampedHeight = imageHeight + } + } setHeight(clampedHeight) setY(clampedY) } @@ -141,14 +135,30 @@ const Extender = (props: Props) => { const moveLeft = () => { const newWidth = evData.initWidth - offsetX const newX = evData.initX + offsetX - const [clampedX, clampedWidth] = clampLeftRight(newX, newWidth) + let clampedX = newX + let clampedWidth = newWidth + if (extenderDirection === EXTENDER_ALL) { + if (clampedX > 0) { + clampedX = 0 + clampedWidth = evData.initWidth - Math.abs(evData.initX) + } + } else { + const clamped = clampLeftRight(newX, newWidth) + clampedX = clamped[0] + clampedWidth = clamped[1] + } setWidth(clampedWidth) setX(clampedX) } const moveRight = () => { const newWidth = evData.initWidth + offsetX - const [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth) + let [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth) + if (extenderDirection === EXTENDER_ALL) { + if (clampedX + clampedWidth < imageWdith) { + clampedWidth = imageWdith + } + } setWidth(clampedWidth) setX(clampedX) } @@ -196,31 +206,16 @@ const Extender = (props: Props) => { break } } - - if (isMoving) { - const newX = evData.initX + offsetX - const newY = evData.initY + offsetY - const [clampedX, clampedWidth] = clampLeftRight(newX, evData.initWidth) - const [clampedY, clampedHeight] = clampTopBottom(newY, evData.initHeight) - setWidth(clampedWidth) - setHeight(clampedHeight) - setX(clampedX) - setY(clampedY) - } } const onPointerDone = (e: PointerEvent) => { if (isResizing) { setIsResizing(false) } - - if (isMoving) { - setIsMoving(false) - } } useEffect(() => { - if (isResizing || isMoving) { + if (isResizing) { document.addEventListener("pointermove", onPointerMove, DOC_MOVE_OPTS) document.addEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS) document.addEventListener("pointercancel", onPointerDone, DOC_MOVE_OPTS) @@ -238,7 +233,7 @@ const Extender = (props: Props) => { ) } } - }, [isResizing, isMoving, width, height, evData]) + }, [isResizing, width, height, evData]) const onCropPointerDown = (e: React.PointerEvent) => { const { ord } = (e.target as HTMLElement).dataset @@ -300,36 +295,55 @@ const Extender = (props: Props) => { onPointerDown={onCropPointerDown} className="absolute top-0 h-full w-full" > -
-
-
-
- {createDragHandle("cursor-nw-resize", "top", "left")} - {createDragHandle("cursor-ne-resize", "top", "right")} - {createDragHandle("cursor-sw-resize", "bottom", "left")} - {createDragHandle("cursor-se-resize", "bottom", "right")} - {createDragHandle("cursor-ns-resize", "top", "")} - {createDragHandle("cursor-ns-resize", "bottom", "")} - {createDragHandle("cursor-ew-resize", "left", "")} - {createDragHandle("cursor-ew-resize", "right", "")} + {[EXTENDER_Y, EXTENDER_ALL].includes(extenderDirection) ? ( + <> +
+
+ {createDragHandle("cursor-ns-resize", "top", "")} + {createDragHandle("cursor-ns-resize", "bottom", "")} + + ) : ( + <> + )} + + {[EXTENDER_X, EXTENDER_ALL].includes(extenderDirection) ? ( + <> +
+
+ {createDragHandle("cursor-ew-resize", "left", "")} + {createDragHandle("cursor-ew-resize", "right", "")} + + ) : ( + <> + )} + + {extenderDirection === EXTENDER_ALL ? ( + <> + {createDragHandle("cursor-nw-resize", "top", "left")} + {createDragHandle("cursor-ne-resize", "top", "right")} + {createDragHandle("cursor-sw-resize", "bottom", "left")} + {createDragHandle("cursor-se-resize", "bottom", "right")} + + ) : ( + <> + )}
) } const onInfoBarPointerDown = (e: React.PointerEvent) => { - setIsMoving(true) setEVData({ initX: x, initY: y, @@ -345,7 +359,7 @@ const Extender = (props: Props) => { return (
{ const createBorder = () => { return (
{ - toggleOpen() - }) - const { toast } = useToast() const [scrollTop, setScrollTop] = useState(0) const [closeScrollTop, setCloseScrollTop] = useState(0) @@ -91,6 +87,37 @@ export default function FileManager(props: Props) { const debouncedSearchText = useDebounce(fileManagerState.searchText, 300) const [tab, setTab] = useState(IMAGE_TAB) const [photos, setPhotos] = useState([]) + const [photoIndex, setPhotoIndex] = useState(0) + + useHotKey("f", () => { + toggleOpen() + }) + + useHotKey( + "left", + () => { + let newIndex = photoIndex + if (photoIndex > 0) { + newIndex = photoIndex - 1 + } + setPhotoIndex(newIndex) + onPhotoClick(tab, photos[newIndex].name) + }, + [photoIndex, photos] + ) + + useHotKey( + "right", + () => { + let newIndex = photoIndex + if (photoIndex < photos.length - 1) { + newIndex = photoIndex + 1 + } + setPhotoIndex(newIndex) + onPhotoClick(tab, photos[newIndex].name) + }, + [photoIndex, photos] + ) useEffect(() => { if (!open) { @@ -165,6 +192,7 @@ export default function FileManager(props: Props) { const onClick = ({ index }: { index: number }) => { toggleOpen() + setPhotoIndex(index) onPhotoClick(tab, photos[index].name) } diff --git a/web_app/src/components/Header.tsx b/web_app/src/components/Header.tsx index 848886d..ecceac9 100644 --- a/web_app/src/components/Header.tsx +++ b/web_app/src/components/Header.tsx @@ -2,12 +2,6 @@ import { PlayIcon } from "@radix-ui/react-icons" import { useCallback, useState } from "react" import { IconButton, ImageUploadButton } from "@/components/ui/button" import Shortcuts from "@/components/Shortcuts" -import emitter, { - DREAM_BUTTON_MOUSE_ENTER, - DREAM_BUTTON_MOUSE_LEAVE, - EVENT_CUSTOM_MASK, - RERUN_LAST_MASK, -} from "@/lib/event" import { useImage } from "@/hooks/useImage" import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover" @@ -17,9 +11,9 @@ import FileManager from "./FileManager" import { getMediaFile } from "@/lib/api" import { useStore } from "@/lib/states" import SettingsDialog from "./Settings" -import { cn } from "@/lib/utils" -import useHotKey from "@/hooks/useHotkey" +import { cn, fileToImage } from "@/lib/utils" import Coffee from "./Coffee" +import { useToast } from "./ui/use-toast" const Header = () => { const [ @@ -27,48 +21,49 @@ const Header = () => { customMask, isInpainting, enableFileManager, - enableManualInpainting, + runMannually, enableUploadMask, model, setFile, setCustomFile, + runInpainting, + showPrevMask, + hidePrevMask, + imageHeight, + imageWidth, ] = useStore((state) => [ state.file, state.customMask, state.isInpainting, state.serverConfig.enableFileManager, - state.settings.enableManualInpainting, + state.runMannually(), state.settings.enableUploadMask, state.settings.model, state.setFile, state.setCustomFile, + state.runInpainting, + state.showPrevMask, + state.hidePrevMask, + state.imageHeight, + state.imageWidth, ]) + + const { toast } = useToast() const [maskImage, maskImageLoaded] = useImage(customMask) const [openMaskPopover, setOpenMaskPopover] = useState(false) - const handleRerunLastMask = useCallback(() => { - emitter.emit(RERUN_LAST_MASK) - }, []) + const handleRerunLastMask = () => { + runInpainting() + } const onRerunMouseEnter = () => { - emitter.emit(DREAM_BUTTON_MOUSE_ENTER) + showPrevMask() } const onRerunMouseLeave = () => { - emitter.emit(DREAM_BUTTON_MOUSE_LEAVE) + hidePrevMask() } - useHotKey( - "r", - () => { - if (!isInpainting) { - handleRerunLastMask() - } - }, - {}, - [isInpainting, handleRerunLastMask] - ) - return (
@@ -103,10 +98,31 @@ const Header = () => { { + onFileUpload={async (file) => { + let newCustomMask: HTMLImageElement | null = null + try { + newCustomMask = await fileToImage(file) + } catch (e: any) { + toast({ + variant: "destructive", + description: e.message ? e.message : e.toString(), + }) + return + } + if ( + newCustomMask.naturalHeight !== imageHeight || + newCustomMask.naturalWidth !== imageWidth + ) { + toast({ + variant: "destructive", + description: `The size of the mask must same as image: ${imageWidth}x${imageHeight}`, + }) + return + } + setCustomFile(file) - if (!enableManualInpainting) { - emitter.emit(EVENT_CUSTOM_MASK, { mask: file }) + if (!runMannually) { + runInpainting() } }} > @@ -125,7 +141,6 @@ const Header = () => { }} onClick={() => { if (customMask) { - emitter.emit(EVENT_CUSTOM_MASK, { mask: customMask }) } }} > @@ -149,7 +164,7 @@ const Header = () => { {file && !model.need_prompt ? ( - ( @@ -334,7 +334,7 @@ export function SettingsDialog() { )} /> - + */}
) } diff --git a/web_app/src/components/Shortcuts.tsx b/web_app/src/components/Shortcuts.tsx index d88a1a3..85be539 100644 --- a/web_app/src/components/Shortcuts.tsx +++ b/web_app/src/components/Shortcuts.tsx @@ -74,7 +74,6 @@ export function Shortcuts() { /> - diff --git a/web_app/src/components/SidePanel.tsx b/web_app/src/components/SidePanel.tsx index 7d48989..e563540 100644 --- a/web_app/src/components/SidePanel.tsx +++ b/web_app/src/components/SidePanel.tsx @@ -1,4 +1,4 @@ -import { FormEvent } from "react" +import { FormEvent, useState } from "react" import { useToggle } from "react-use" import { useStore } from "@/lib/states" import { Switch } from "./ui/switch" @@ -17,17 +17,110 @@ import { SDSampler } from "@/lib/types" import { Separator } from "./ui/separator" import { ScrollArea } from "./ui/scroll-area" import { Sheet, SheetContent, SheetHeader, SheetTrigger } from "./ui/sheet" -import { ChevronLeft, ChevronRight, Upload } from "lucide-react" +import { + ArrowDownFromLine, + ArrowLeftFromLine, + ArrowRightFromLine, + ArrowUpFromLine, + ChevronLeft, + ChevronRight, + HelpCircle, + LucideIcon, + Maximize, + Move, + MoveHorizontal, + MoveVertical, + Upload, +} from "lucide-react" import { Button, ImageUploadButton } from "./ui/button" import useHotKey from "@/hooks/useHotkey" import { Slider } from "./ui/slider" import { useImage } from "@/hooks/useImage" -import { INSTRUCT_PIX2PIX, PAINT_BY_EXAMPLE } from "@/lib/const" +import { + EXTENDER_ALL, + EXTENDER_BUILTIN_ALL, + EXTENDER_BUILTIN_X_LEFT, + EXTENDER_BUILTIN_X_RIGHT, + EXTENDER_BUILTIN_Y_BOTTOM, + EXTENDER_BUILTIN_Y_TOP, + EXTENDER_X, + EXTENDER_Y, + INSTRUCT_PIX2PIX, + PAINT_BY_EXAMPLE, +} from "@/lib/const" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs" +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip" const RowContainer = ({ children }: { children: React.ReactNode }) => (
{children}
) +const ExtenderButton = ({ + IconCls, + text, + onClick, +}: { + IconCls: LucideIcon + text: string + onClick: () => void +}) => { + const [showExtender] = useStore((state) => [state.settings.showExtender]) + return ( + + ) +} + +const LabelTitle = ({ + text, + toolTip, + url, + htmlFor, + disabled = false, +}: { + text: string + toolTip?: string + url?: string + htmlFor?: string + disabled?: boolean +}) => { + return ( + + + + + +

{toolTip}

+ {url ? ( + + ) : ( + <> + )} +
+
+ ) +} + const SidePanel = () => { const [ settings, @@ -38,6 +131,8 @@ const SidePanel = () => { showSidePanel, runInpainting, updateAppState, + updateExtenderByBuiltIn, + updateExtenderDirection, ] = useStore((state) => [ state.settings, state.windowSize, @@ -47,6 +142,8 @@ const SidePanel = () => { state.showSidePanel(), state.runInpainting, state.updateAppState, + state.updateExtenderByBuiltIn, + state.updateExtenderDirection, ]) const [exampleImage, isExampleImageLoaded] = useImage(paintByExampleFile) const [open, toggleOpen] = useToggle(true) @@ -75,7 +172,7 @@ const SidePanel = () => {
- + { return ( <> - + { return (
- + {
- + { />
- + {
- + { />
- + { return (
- +