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 ( + + + + {text} + + + ) +} + +const LabelTitle = ({ + text, + toolTip, + url, + htmlFor, + disabled = false, +}: { + text: string + toolTip?: string + url?: string + htmlFor?: string + disabled?: boolean +}) => { + return ( + + + + {text} + + + + {toolTip} + {url ? ( + + + More info + + + ) : ( + <>> + )} + + + ) +} + 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 = () => { - Controlnet + { return ( <> - LCM Lora + { return ( - Freeu + { - - s1 - + { /> - - s2 - + { - - b1 - + { /> - - b2 - + { return ( - Negative prompt + { } return ( - Image guidance scale + { return ( - Strength + { ) } - const renderExpender = () => { + const renderExtender = () => { return ( <> - - Expender - { - updateSettings({ showExpender: value }) - if (value) { - updateSettings({ showCropper: false }) - } - }} - /> - + + + + { + updateSettings({ showExtender: value }) + if (value) { + updateSettings({ showCropper: false }) + } + }} + /> + + + updateExtenderDirection(value)} + className="flex flex-col justify-center items-center" + > + + + + + + + + + + + + + + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_LEFT, 1.5) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_LEFT, 2.0) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_RIGHT, 1.5) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_RIGHT, 2.0) + } + /> + + + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_TOP, 1.5) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_TOP, 2.0) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_BOTTOM, 1.5) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_BOTTOM, 2.0) + } + /> + + + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.25) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.5) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.75) + } + /> + + updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 2.0) + } + /> + + + > ) @@ -466,23 +709,23 @@ const SidePanel = () => { > - Cropper + { updateSettings({ showCropper: value }) if (value) { - updateSettings({ showExpender: false }) + updateSettings({ showExtender: false }) } }} /> - {renderExpender()} + {renderExtender()} - Steps + { - Guidance scale + { {renderStrength()} - Sampler + { @@ -563,7 +810,11 @@ const SidePanel = () => { {/* 每次会从服务器返回更新该值 */} - Seed + + {/* */} { {renderLCMLora()} - Mask blur + { - Match histograms + ( ({ tooltip, children, ...rest }, ref) => { return ( - <> - - - - - {children} - - - - {tooltip} - - - - > + + + + {children} + + + + {tooltip} + + ) } ) diff --git a/web_app/src/components/ui/label.tsx b/web_app/src/components/ui/label.tsx index bf021cd..9dd1072 100644 --- a/web_app/src/components/ui/label.tsx +++ b/web_app/src/components/ui/label.tsx @@ -20,7 +20,12 @@ const Label = React.forwardRef< >(({ className, disabled, ...props }, ref) => ( )) diff --git a/web_app/src/components/ui/radio-group.tsx b/web_app/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..c939b06 --- /dev/null +++ b/web_app/src/components/ui/radio-group.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import { CheckIcon } from "@radix-ui/react-icons" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/web_app/src/components/ui/tabs.tsx b/web_app/src/components/ui/tabs.tsx index 85d83be..ddb6e43 100644 --- a/web_app/src/components/ui/tabs.tsx +++ b/web_app/src/components/ui/tabs.tsx @@ -15,6 +15,7 @@ const TabsList = React.forwardRef< "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", className )} + tabIndex={-1} {...props} /> )) @@ -30,6 +31,7 @@ const TabsTrigger = React.forwardRef< "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", className )} + tabIndex={-1} {...props} /> )) @@ -45,6 +47,7 @@ const TabsContent = React.forwardRef< "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", className )} + tabIndex={-1} {...props} /> )) diff --git a/web_app/src/lib/api.ts b/web_app/src/lib/api.ts index 56e324d..3b09f41 100644 --- a/web_app/src/lib/api.ts +++ b/web_app/src/lib/api.ts @@ -1,6 +1,6 @@ import { ModelInfo, Rect } from "@/lib/types" import { Settings } from "@/lib/states" -import { dataURItoBlob, srcToFile } from "@/lib/utils" +import { srcToFile } from "@/lib/utils" import axios from "axios" export const API_ENDPOINT = import.meta.env.VITE_BACKEND @@ -15,6 +15,7 @@ export default async function inpaint( imageFile: File, settings: Settings, croperRect: Rect, + extenderState: Rect, mask: File | Blob, paintByExampleImage: File | null = null ) { @@ -32,11 +33,18 @@ export default async function inpaint( fd.append("prompt", settings.prompt) fd.append("negativePrompt", settings.negativePrompt) + + fd.append("useCroper", settings.showCropper ? "true" : "false") fd.append("croperX", croperRect.x.toString()) fd.append("croperY", croperRect.y.toString()) fd.append("croperHeight", croperRect.height.toString()) fd.append("croperWidth", croperRect.width.toString()) - fd.append("useCroper", settings.showCropper ? "true" : "false") + + fd.append("useExtender", settings.showExtender ? "true" : "false") + fd.append("extenderX", extenderState.x.toString()) + fd.append("extenderY", extenderState.y.toString()) + fd.append("extenderHeight", extenderState.height.toString()) + fd.append("extenderWidth", extenderState.width.toString()) fd.append("sdMaskBlur", settings.sdMaskBlur.toString()) fd.append("sdStrength", settings.sdStrength.toString()) @@ -82,7 +90,7 @@ export default async function inpaint( }) if (res.ok) { const blob = await res.blob() - const newSeed = res.headers.get("x-seed") + const newSeed = res.headers.get("X-seed") return { blob: URL.createObjectURL(blob), seed: newSeed } } const errMsg = await res.text() diff --git a/web_app/src/lib/const.ts b/web_app/src/lib/const.ts index f109975..1a782ad 100644 --- a/web_app/src/lib/const.ts +++ b/web_app/src/lib/const.ts @@ -8,6 +8,15 @@ export const MODEL_TYPE_DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint" export const MODEL_TYPE_OTHER = "diffusers_other" export const BRUSH_COLOR = "#ffcc00bb" +export const EXTENDER_X = "extender_x" +export const EXTENDER_Y = "extender_y" +export const EXTENDER_ALL = "extender_all" +export const EXTENDER_BUILTIN_X_LEFT = "extender_builtin_x_left" +export const EXTENDER_BUILTIN_X_RIGHT = "extender_builtin_x_right" +export const EXTENDER_BUILTIN_Y_TOP = "extender_builtin_y_top" +export const EXTENDER_BUILTIN_Y_BOTTOM = "extender_builtin_y_bottom" +export const EXTENDER_BUILTIN_ALL = "extender_builtin_all" + export const PAINT_BY_EXAMPLE = "Fantasy-Studio/Paint-by-Example" export const INSTRUCT_PIX2PIX = "timbrooks/instruct-pix2pix" export const KANDINSKY_2_2 = "kandinsky-community/kandinsky-2-2-decoder-inpaint" diff --git a/web_app/src/lib/event.ts b/web_app/src/lib/event.ts deleted file mode 100644 index b1200f6..0000000 --- a/web_app/src/lib/event.ts +++ /dev/null @@ -1,22 +0,0 @@ -import mitt from "mitt" - -export const EVENT_PROMPT = "prompt" - -export const EVENT_CUSTOM_MASK = "custom_mask" -export interface CustomMaskEventData { - mask: File -} - -export const EVENT_PAINT_BY_EXAMPLE = "paint_by_example" -export interface PaintByExampleEventData { - image: File -} - -export const RERUN_LAST_MASK = "rerun_last_mask" - -export const DREAM_BUTTON_MOUSE_ENTER = "dream_button_mouse_enter" -export const DREAM_BUTTON_MOUSE_LEAVE = "dream_btoon_mouse_leave" - -const emitter = mitt() - -export default emitter diff --git a/web_app/src/lib/states.ts b/web_app/src/lib/states.ts index a1c09c1..53c3203 100644 --- a/web_app/src/lib/states.ts +++ b/web_app/src/lib/states.ts @@ -21,6 +21,14 @@ import { BRUSH_COLOR, DEFAULT_BRUSH_SIZE, DEFAULT_NEGATIVE_PROMPT, + 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, MODEL_TYPE_INPAINT, PAINT_BY_EXAMPLE, } from "./const" @@ -56,7 +64,8 @@ export type Settings = { enableManualInpainting: boolean enableUploadMask: boolean showCropper: boolean - showExpender: boolean + showExtender: boolean + extenderDirection: string // For LDM ldmSteps: number @@ -168,6 +177,9 @@ type AppAction = { setExtenderY: (newValue: number) => void setExtenderWidth: (newValue: number) => void setExtenderHeight: (newValue: number) => void + updateExtenderDirection: (newValue: string) => void + resetExtender: (width: number, height: number) => void + updateExtenderByBuiltIn: (direction: string, scale: number) => void setServerConfig: (newValue: ServerConfig) => void setSeed: (newValue: number) => void @@ -281,7 +293,8 @@ const defaultValues: AppState = { }, enableControlnet: false, showCropper: false, - showExpender: false, + showExtender: false, + extenderDirection: EXTENDER_ALL, enableDownloadMask: false, enableManualInpainting: false, enableUploadMask: false, @@ -362,7 +375,7 @@ export const useStore = createWithEqualityFn()( } return targetFile }, - + // todo: 传入 custom mask,单独逻辑 runInpainting: async () => { const { isInpainting, @@ -372,6 +385,7 @@ export const useStore = createWithEqualityFn()( imageHeight, settings, cropperState, + extenderState, } = get() if (isInpainting) { return @@ -398,7 +412,11 @@ export const useStore = createWithEqualityFn()( // 2. 结果替换当前 render let maskLineGroup: LineGroup = [] if (useLastLineGroup === true) { - if (lastLineGroup.length === 0 && maskImage === null) { + if ( + lastLineGroup.length === 0 && + maskImage === null && + !settings.showExtender + ) { toast({ variant: "destructive", description: "Please draw mask on picture", @@ -407,7 +425,11 @@ export const useStore = createWithEqualityFn()( } maskLineGroup = lastLineGroup } else { - if (curLineGroup.length === 0 && maskImage === null) { + if ( + curLineGroup.length === 0 && + maskImage === null && + !settings.showExtender + ) { toast({ variant: "destructive", description: "Please draw mask on picture", @@ -455,6 +477,7 @@ export const useStore = createWithEqualityFn()( targetFile, settings, cropperState, + extenderState, dataURItoBlob(maskCanvas.toDataURL()), paintByExampleFile ) @@ -465,7 +488,7 @@ export const useStore = createWithEqualityFn()( const { blob, seed } = res if (seed) { - set((state) => (state.settings.seed = parseInt(seed, 10))) + get().setSeed(parseInt(seed, 10)) } const newRender = new Image() await loadImage(newRender, blob) @@ -794,7 +817,7 @@ export const useStore = createWithEqualityFn()( state.isPluginRunning = newValue }), - setFile: (file: File) => + setFile: (file: File) => { set((state) => { state.file = file state.interactiveSegState = castDraft( @@ -802,7 +825,8 @@ export const useStore = createWithEqualityFn()( ) state.editorState = castDraft(defaultValues.editorState) state.cropperState = defaultValues.cropperState - }), + }) + }, setCustomFile: (file: File) => set((state) => { @@ -822,6 +846,7 @@ export const useStore = createWithEqualityFn()( state.editorState.brushSizeScale = Math.max(Math.min(width, height), 512) / 512 }) + get().resetExtender(width, height) }, setCropperX: (newValue: number) => @@ -864,6 +889,68 @@ export const useStore = createWithEqualityFn()( state.extenderState.height = newValue }), + updateExtenderDirection: (newValue: string) => { + console.log( + `updateExtenderDirection: ${JSON.stringify(get().extenderState)}` + ) + set((state) => { + state.settings.extenderDirection = newValue + state.extenderState.x = 0 + state.extenderState.y = 0 + state.extenderState.width = state.imageWidth + state.extenderState.height = state.imageHeight + }) + }, + + updateExtenderByBuiltIn: (direction: string, scale: number) => { + const newExtenderState = { ...defaultValues.extenderState } + let { x, y, width, height } = newExtenderState + const { imageWidth, imageHeight } = get() + width = imageWidth + height = imageHeight + + switch (direction) { + case EXTENDER_BUILTIN_X_LEFT: + x = -Math.ceil(imageWidth * (scale - 1)) + width = Math.ceil(imageWidth * scale) + break + case EXTENDER_BUILTIN_X_RIGHT: + width = Math.ceil(imageWidth * scale) + break + case EXTENDER_BUILTIN_Y_TOP: + y = -Math.ceil(imageHeight * (scale - 1)) + height = Math.ceil(imageHeight * scale) + break + case EXTENDER_BUILTIN_Y_BOTTOM: + height = Math.ceil(imageHeight * scale) + break + case EXTENDER_BUILTIN_ALL: + x = -Math.ceil((imageWidth * (scale - 1)) / 2) + y = -Math.ceil((imageHeight * (scale - 1)) / 2) + width = Math.ceil(imageWidth * scale) + height = Math.ceil(imageHeight * scale) + break + default: + break + } + + set((state) => { + state.extenderState.x = x + state.extenderState.y = y + state.extenderState.width = width + state.extenderState.height = height + }) + }, + + resetExtender: (width: number, height: number) => { + set((state) => { + state.extenderState.x = 0 + state.extenderState.y = 0 + state.extenderState.width = width + state.extenderState.height = height + }) + }, + setSeed: (newValue: number) => set((state) => { state.settings.seed = newValue @@ -871,7 +958,7 @@ export const useStore = createWithEqualityFn()( })), { name: "ZUSTAND_STATE", // name of the item in the storage (must be unique) - version: 1, + version: 0, partialize: (state) => Object.fromEntries( Object.entries(state).filter(([key]) => diff --git a/web_app/src/lib/types.ts b/web_app/src/lib/types.ts index ad3370d..e043676 100644 --- a/web_app/src/lib/types.ts +++ b/web_app/src/lib/types.ts @@ -67,7 +67,7 @@ export enum SDSampler { kEulerA = "k_euler_a", dpmPlusPlus = "dpm++", uni_pc = "uni_pc", - lcm = "lcm", + // lcm = "lcm", } export interface FreeuConfig { diff --git a/web_app/src/lib/utils.ts b/web_app/src/lib/utils.ts index ffc52df..4c3e1fb 100644 --- a/web_app/src/lib/utils.ts +++ b/web_app/src/lib/utils.ts @@ -71,6 +71,26 @@ export function canvasToImage( }) } +export function fileToImage(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + const image = new Image() + image.onload = () => { + resolve(image) + } + image.onerror = () => { + reject("无法加载图像。") + } + image.src = reader.result as string + } + reader.onerror = () => { + reject("无法读取文件。") + } + reader.readAsDataURL(file) + }) +} + export function srcToFile(src: string, fileName: string, mimeType: string) { return fetch(src) .then(function (res) { diff --git a/web_app/src/main.tsx b/web_app/src/main.tsx index 5617f22..fce2cfe 100644 --- a/web_app/src/main.tsx +++ b/web_app/src/main.tsx @@ -5,6 +5,7 @@ import "inter-ui/inter.css" import App from "./App.tsx" import "./globals.css" import { ThemeProvider } from "next-themes" +import { TooltipProvider } from "./components/ui/tooltip.tsx" const queryClient = new QueryClient() @@ -12,7 +13,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render( - + + +
{toolTip}
{tooltip}