This commit is contained in:
Qing 2023-12-22 14:00:11 +08:00
parent 141936a937
commit eb9764176c
20 changed files with 738 additions and 255 deletions

View File

@ -17,6 +17,7 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@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-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3", "@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": { "node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",

View File

@ -19,6 +19,7 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@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-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",

View File

@ -28,7 +28,7 @@ import { useStore } from "@/lib/states"
import Cropper from "./Cropper" import Cropper from "./Cropper"
import { InteractiveSegPoints } from "./InteractiveSeg" import { InteractiveSegPoints } from "./InteractiveSeg"
import useHotKey from "@/hooks/useHotkey" import useHotKey from "@/hooks/useHotkey"
import Extender from "./Expender" import Extender from "./Extender"
const TOOLBAR_HEIGHT = 200 const TOOLBAR_HEIGHT = 200
const MIN_BRUSH_SIZE = 10 const MIN_BRUSH_SIZE = 10
@ -221,7 +221,9 @@ export default function Editor(props: EditorProps) {
} }
const [width, height] = getCurrentWidthHeight() const [width, height] = getCurrentWidthHeight()
setImageSize(width, height) if (width !== imageWidth || height !== imageHeight) {
setImageSize(width, height)
}
const rW = windowSize.width / width const rW = windowSize.width / width
const rH = (windowSize.height - TOOLBAR_HEIGHT) / height const rH = (windowSize.height - TOOLBAR_HEIGHT) / height
@ -255,6 +257,8 @@ export default function Editor(props: EditorProps) {
} }
}, [ }, [
viewportRef, viewportRef,
imageHeight,
imageWidth,
original, original,
isOriginalLoaded, isOriginalLoaded,
windowSize, 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 => { const getCurScale = (): number => {
let s = minScale let s = minScale
if (viewportRef.current?.instance?.transformState.scale !== undefined) { if (viewportRef.current?.instance?.transformState.scale !== undefined) {
@ -782,19 +773,17 @@ export default function Editor(props: EditorProps) {
<Cropper <Cropper
maxHeight={imageHeight} maxHeight={imageHeight}
maxWidth={imageWidth} maxWidth={imageWidth}
minHeight={Math.min(256, imageHeight)} minHeight={Math.min(512, imageHeight)}
minWidth={Math.min(256, imageWidth)} minWidth={Math.min(512, imageWidth)}
scale={getCurScale()} scale={getCurScale()}
show={settings.showCropper} show={settings.showCropper}
/> />
<Extender <Extender
maxHeight={imageHeight} minHeight={Math.min(512, imageHeight)}
maxWidth={imageWidth} minWidth={Math.min(512, imageWidth)}
minHeight={Math.min(256, imageHeight)}
minWidth={Math.min(256, imageWidth)}
scale={getCurScale()} scale={getCurScale()}
show={settings.showExpender} show={settings.showExtender}
/> />
{interactiveSegState.isInteractiveSeg ? ( {interactiveSegState.isInteractiveSeg ? (

View File

@ -1,3 +1,4 @@
import { EXTENDER_ALL, EXTENDER_X, EXTENDER_Y } from "@/lib/const"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
@ -18,8 +19,6 @@ interface EVData {
} }
interface Props { interface Props {
maxHeight: number
maxWidth: number
scale: number scale: number
minHeight: number minHeight: number
minWidth: number minWidth: number
@ -30,66 +29,44 @@ const clamp = (
newPos: number, newPos: number,
newLength: number, newLength: number,
oldPos: number, oldPos: number,
oldLength: number, minLength: number
minLength: number,
maxLength: number
) => { ) => {
return [newPos, newLength] if (newLength < minLength) {
if (newPos !== oldPos && newLength === oldLength) { if (newPos === oldPos) {
if (newPos < 0) { return [newPos, minLength]
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]
} }
return [newPos + newLength - minLength, minLength]
} }
return [newPos, newLength] return [newPos, newLength]
} }
const Extender = (props: Props) => { const Extender = (props: Props) => {
const { minHeight, minWidth, maxHeight, maxWidth, scale, show } = props const { minHeight, minWidth, scale, show } = props
const [ const [
imageWidth,
imageHeight,
isInpainting, isInpainting,
imageHeight,
imageWdith,
{ x, y, width, height }, { x, y, width, height },
setX, setX,
setY, setY,
setWidth, setWidth,
setHeight, setHeight,
extenderDirection,
] = useStore((state) => [ ] = useStore((state) => [
state.imageWidth,
state.imageHeight,
state.isInpainting, state.isInpainting,
state.imageHeight,
state.imageWidth,
state.extenderState, state.extenderState,
state.setExtenderX, state.setExtenderX,
state.setExtenderY, state.setExtenderY,
state.setExtenderWidth, state.setExtenderWidth,
state.setExtenderHeight, state.setExtenderHeight,
state.settings.extenderDirection,
]) ])
const [isResizing, setIsResizing] = useState(false) 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<EVData>({ const [evData, setEVData] = useState<EVData>({
initX: 0, initX: 0,
@ -106,11 +83,11 @@ const Extender = (props: Props) => {
} }
const clampLeftRight = (newX: number, newWidth: number) => { 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) => { const clampTopBottom = (newY: number, newHeight: number) => {
return clamp(newY, newHeight, y, height, minHeight, maxHeight) return clamp(newY, newHeight, y, minHeight)
} }
const onPointerMove = (e: PointerEvent) => { const onPointerMove = (e: PointerEvent) => {
@ -126,14 +103,31 @@ const Extender = (props: Props) => {
const moveTop = () => { const moveTop = () => {
const newHeight = evData.initHeight - offsetY const newHeight = evData.initHeight - offsetY
const newY = evData.initY + 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) setHeight(clampedHeight)
setY(clampedY) setY(clampedY)
} }
const moveBottom = () => { const moveBottom = () => {
const newHeight = evData.initHeight + offsetY 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) setHeight(clampedHeight)
setY(clampedY) setY(clampedY)
} }
@ -141,14 +135,30 @@ const Extender = (props: Props) => {
const moveLeft = () => { const moveLeft = () => {
const newWidth = evData.initWidth - offsetX const newWidth = evData.initWidth - offsetX
const newX = evData.initX + 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) setWidth(clampedWidth)
setX(clampedX) setX(clampedX)
} }
const moveRight = () => { const moveRight = () => {
const newWidth = evData.initWidth + offsetX 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) setWidth(clampedWidth)
setX(clampedX) setX(clampedX)
} }
@ -196,31 +206,16 @@ const Extender = (props: Props) => {
break 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) => { const onPointerDone = (e: PointerEvent) => {
if (isResizing) { if (isResizing) {
setIsResizing(false) setIsResizing(false)
} }
if (isMoving) {
setIsMoving(false)
}
} }
useEffect(() => { useEffect(() => {
if (isResizing || isMoving) { if (isResizing) {
document.addEventListener("pointermove", onPointerMove, DOC_MOVE_OPTS) document.addEventListener("pointermove", onPointerMove, DOC_MOVE_OPTS)
document.addEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS) document.addEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
document.addEventListener("pointercancel", 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<HTMLDivElement>) => { const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
const { ord } = (e.target as HTMLElement).dataset const { ord } = (e.target as HTMLElement).dataset
@ -300,36 +295,55 @@ const Extender = (props: Props) => {
onPointerDown={onCropPointerDown} onPointerDown={onCropPointerDown}
className="absolute top-0 h-full w-full" className="absolute top-0 h-full w-full"
> >
<div {[EXTENDER_Y, EXTENDER_ALL].includes(extenderDirection) ? (
className="absolute pointer-events-auto top-0 left-0 w-full cursor-ns-resize h-[12px] mt-[-6px]" <>
data-ord="top" <div
/> className="absolute pointer-events-auto top-0 left-0 w-full cursor-ns-resize h-[12px] mt-[-6px]"
<div data-ord="top"
className="absolute pointer-events-auto top-0 right-0 h-full cursor-ew-resize w-[12px] mr-[-6px]" />
data-ord="right" <div
/> className="absolute pointer-events-auto bottom-0 left-0 w-full cursor-ns-resize h-[12px] mb-[-6px]"
<div data-ord="bottom"
className="absolute pointer-events-auto bottom-0 left-0 w-full cursor-ns-resize h-[12px] mb-[-6px]" />
data-ord="bottom" {createDragHandle("cursor-ns-resize", "top", "")}
/> {createDragHandle("cursor-ns-resize", "bottom", "")}
<div </>
className="absolute pointer-events-auto top-0 left-0 h-full cursor-ew-resize w-[12px] ml-[-6px]" ) : (
data-ord="left" <></>
/> )}
{createDragHandle("cursor-nw-resize", "top", "left")}
{createDragHandle("cursor-ne-resize", "top", "right")} {[EXTENDER_X, EXTENDER_ALL].includes(extenderDirection) ? (
{createDragHandle("cursor-sw-resize", "bottom", "left")} <>
{createDragHandle("cursor-se-resize", "bottom", "right")} <div
{createDragHandle("cursor-ns-resize", "top", "")} className="absolute pointer-events-auto top-0 right-0 h-full cursor-ew-resize w-[12px] mr-[-6px]"
{createDragHandle("cursor-ns-resize", "bottom", "")} data-ord="right"
{createDragHandle("cursor-ew-resize", "left", "")} />
{createDragHandle("cursor-ew-resize", "right", "")} <div
className="absolute pointer-events-auto top-0 left-0 h-full cursor-ew-resize w-[12px] ml-[-6px]"
data-ord="left"
/>
{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")}
</>
) : (
<></>
)}
</div> </div>
) )
} }
const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
setIsMoving(true)
setEVData({ setEVData({
initX: x, initX: x,
initY: y, initY: y,
@ -345,7 +359,7 @@ const Extender = (props: Props) => {
return ( return (
<div <div
className={twMerge( className={twMerge(
"border absolute pointer-events-auto px-2 py-1 rounded-full hover:cursor-move bg-background", "border absolute pointer-events-auto px-2 py-1 rounded-full bg-background",
"origin-top-left top-0 left-0" "origin-top-left top-0 left-0"
)} )}
style={{ style={{
@ -362,7 +376,7 @@ const Extender = (props: Props) => {
const createBorder = () => { const createBorder = () => {
return ( return (
<div <div
className="outline-dashed outline-primary" className={cn("outline-dashed outline-primary")}
style={{ style={{
height, height,
width, width,

View File

@ -79,10 +79,6 @@ export default function FileManager(props: Props) {
state.updateFileManagerState, state.updateFileManagerState,
]) ])
useHotKey("f", () => {
toggleOpen()
})
const { toast } = useToast() const { toast } = useToast()
const [scrollTop, setScrollTop] = useState(0) const [scrollTop, setScrollTop] = useState(0)
const [closeScrollTop, setCloseScrollTop] = useState(0) const [closeScrollTop, setCloseScrollTop] = useState(0)
@ -91,6 +87,37 @@ export default function FileManager(props: Props) {
const debouncedSearchText = useDebounce(fileManagerState.searchText, 300) const debouncedSearchText = useDebounce(fileManagerState.searchText, 300)
const [tab, setTab] = useState(IMAGE_TAB) const [tab, setTab] = useState(IMAGE_TAB)
const [photos, setPhotos] = useState<Photo[]>([]) const [photos, setPhotos] = useState<Photo[]>([])
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(() => { useEffect(() => {
if (!open) { if (!open) {
@ -165,6 +192,7 @@ export default function FileManager(props: Props) {
const onClick = ({ index }: { index: number }) => { const onClick = ({ index }: { index: number }) => {
toggleOpen() toggleOpen()
setPhotoIndex(index)
onPhotoClick(tab, photos[index].name) onPhotoClick(tab, photos[index].name)
} }

View File

@ -2,12 +2,6 @@ import { PlayIcon } from "@radix-ui/react-icons"
import { useCallback, useState } from "react" import { useCallback, useState } from "react"
import { IconButton, ImageUploadButton } from "@/components/ui/button" import { IconButton, ImageUploadButton } from "@/components/ui/button"
import Shortcuts from "@/components/Shortcuts" 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 { useImage } from "@/hooks/useImage"
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover" import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
@ -17,9 +11,9 @@ import FileManager from "./FileManager"
import { getMediaFile } from "@/lib/api" import { getMediaFile } from "@/lib/api"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import SettingsDialog from "./Settings" import SettingsDialog from "./Settings"
import { cn } from "@/lib/utils" import { cn, fileToImage } from "@/lib/utils"
import useHotKey from "@/hooks/useHotkey"
import Coffee from "./Coffee" import Coffee from "./Coffee"
import { useToast } from "./ui/use-toast"
const Header = () => { const Header = () => {
const [ const [
@ -27,48 +21,49 @@ const Header = () => {
customMask, customMask,
isInpainting, isInpainting,
enableFileManager, enableFileManager,
enableManualInpainting, runMannually,
enableUploadMask, enableUploadMask,
model, model,
setFile, setFile,
setCustomFile, setCustomFile,
runInpainting,
showPrevMask,
hidePrevMask,
imageHeight,
imageWidth,
] = useStore((state) => [ ] = useStore((state) => [
state.file, state.file,
state.customMask, state.customMask,
state.isInpainting, state.isInpainting,
state.serverConfig.enableFileManager, state.serverConfig.enableFileManager,
state.settings.enableManualInpainting, state.runMannually(),
state.settings.enableUploadMask, state.settings.enableUploadMask,
state.settings.model, state.settings.model,
state.setFile, state.setFile,
state.setCustomFile, state.setCustomFile,
state.runInpainting,
state.showPrevMask,
state.hidePrevMask,
state.imageHeight,
state.imageWidth,
]) ])
const { toast } = useToast()
const [maskImage, maskImageLoaded] = useImage(customMask) const [maskImage, maskImageLoaded] = useImage(customMask)
const [openMaskPopover, setOpenMaskPopover] = useState(false) const [openMaskPopover, setOpenMaskPopover] = useState(false)
const handleRerunLastMask = useCallback(() => { const handleRerunLastMask = () => {
emitter.emit(RERUN_LAST_MASK) runInpainting()
}, []) }
const onRerunMouseEnter = () => { const onRerunMouseEnter = () => {
emitter.emit(DREAM_BUTTON_MOUSE_ENTER) showPrevMask()
} }
const onRerunMouseLeave = () => { const onRerunMouseLeave = () => {
emitter.emit(DREAM_BUTTON_MOUSE_LEAVE) hidePrevMask()
} }
useHotKey(
"r",
() => {
if (!isInpainting) {
handleRerunLastMask()
}
},
{},
[isInpainting, handleRerunLastMask]
)
return ( return (
<header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 border-b backdrop-filter backdrop-blur-md bg-background/70"> <header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 border-b backdrop-filter backdrop-blur-md bg-background/70">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@ -103,10 +98,31 @@ const Header = () => {
<ImageUploadButton <ImageUploadButton
disabled={isInpainting} disabled={isInpainting}
tooltip="Upload custom mask" tooltip="Upload custom mask"
onFileUpload={(file) => { 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) setCustomFile(file)
if (!enableManualInpainting) { if (!runMannually) {
emitter.emit(EVENT_CUSTOM_MASK, { mask: file }) runInpainting()
} }
}} }}
> >
@ -125,7 +141,6 @@ const Header = () => {
}} }}
onClick={() => { onClick={() => {
if (customMask) { if (customMask) {
emitter.emit(EVENT_CUSTOM_MASK, { mask: customMask })
} }
}} }}
> >
@ -149,7 +164,7 @@ const Header = () => {
{file && !model.need_prompt ? ( {file && !model.need_prompt ? (
<IconButton <IconButton
disabled={isInpainting} disabled={isInpainting}
tooltip="Rerun last mask" tooltip="Rerun previous mask"
onClick={handleRerunLastMask} onClick={handleRerunLastMask}
onMouseEnter={onRerunMouseEnter} onMouseEnter={onRerunMouseEnter}
onMouseLeave={onRerunMouseLeave} onMouseLeave={onRerunMouseLeave}

View File

@ -314,7 +314,7 @@ export function SettingsDialog() {
<Separator /> <Separator />
<FormField {/* <FormField
control={form.control} control={form.control}
name="enableUploadMask" name="enableUploadMask"
render={({ field }) => ( render={({ field }) => (
@ -334,7 +334,7 @@ export function SettingsDialog() {
</FormItem> </FormItem>
)} )}
/> />
<Separator /> <Separator /> */}
</div> </div>
) )
} }

View File

@ -74,7 +74,6 @@ export function Shortcuts() {
/> />
<ShortCut content="Cancel Drawing" keys={["Esc"]} /> <ShortCut content="Cancel Drawing" keys={["Esc"]} />
<ShortCut content="Rerun last mask" keys={["R"]} />
<ShortCut content="Undo" keys={[CmdOrCtrl(), "Z"]} /> <ShortCut content="Undo" keys={[CmdOrCtrl(), "Z"]} />
<ShortCut content="Redo" keys={[CmdOrCtrl(), "Shift", "Z"]} /> <ShortCut content="Redo" keys={[CmdOrCtrl(), "Shift", "Z"]} />
<ShortCut content="Copy Result" keys={[CmdOrCtrl(), "C"]} /> <ShortCut content="Copy Result" keys={[CmdOrCtrl(), "C"]} />

View File

@ -1,4 +1,4 @@
import { FormEvent } from "react" import { FormEvent, useState } from "react"
import { useToggle } from "react-use" import { useToggle } from "react-use"
import { useStore } from "@/lib/states" import { useStore } from "@/lib/states"
import { Switch } from "./ui/switch" import { Switch } from "./ui/switch"
@ -17,17 +17,110 @@ import { SDSampler } from "@/lib/types"
import { Separator } from "./ui/separator" import { Separator } from "./ui/separator"
import { ScrollArea } from "./ui/scroll-area" import { ScrollArea } from "./ui/scroll-area"
import { Sheet, SheetContent, SheetHeader, SheetTrigger } from "./ui/sheet" 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 { Button, ImageUploadButton } from "./ui/button"
import useHotKey from "@/hooks/useHotkey" import useHotKey from "@/hooks/useHotkey"
import { Slider } from "./ui/slider" import { Slider } from "./ui/slider"
import { useImage } from "@/hooks/useImage" 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 }) => ( const RowContainer = ({ children }: { children: React.ReactNode }) => (
<div className="flex justify-between items-center pr-2">{children}</div> <div className="flex justify-between items-center pr-2">{children}</div>
) )
const ExtenderButton = ({
IconCls,
text,
onClick,
}: {
IconCls: LucideIcon
text: string
onClick: () => void
}) => {
const [showExtender] = useStore((state) => [state.settings.showExtender])
return (
<Button
variant="outline"
size="sm"
className="p-1"
disabled={!showExtender}
onClick={onClick}
>
<div className="flex items-center gap-1">
<IconCls size={15} strokeWidth={1} />
{text}
</div>
</Button>
)
}
const LabelTitle = ({
text,
toolTip,
url,
htmlFor,
disabled = false,
}: {
text: string
toolTip?: string
url?: string
htmlFor?: string
disabled?: boolean
}) => {
return (
<Tooltip>
<TooltipTrigger asChild>
<Label
htmlFor={htmlFor ? htmlFor : text.toLowerCase().replace(" ", "-")}
className="font-medium"
disabled={disabled}
>
{text}
</Label>
</TooltipTrigger>
<TooltipContent className="flex flex-col max-w-xs text-sm" side="left">
<p>{toolTip}</p>
{url ? (
<Button variant="link" className="justify-end">
<a href={url} target="_blank">
More info
</a>
</Button>
) : (
<></>
)}
</TooltipContent>
</Tooltip>
)
}
const SidePanel = () => { const SidePanel = () => {
const [ const [
settings, settings,
@ -38,6 +131,8 @@ const SidePanel = () => {
showSidePanel, showSidePanel,
runInpainting, runInpainting,
updateAppState, updateAppState,
updateExtenderByBuiltIn,
updateExtenderDirection,
] = useStore((state) => [ ] = useStore((state) => [
state.settings, state.settings,
state.windowSize, state.windowSize,
@ -47,6 +142,8 @@ const SidePanel = () => {
state.showSidePanel(), state.showSidePanel(),
state.runInpainting, state.runInpainting,
state.updateAppState, state.updateAppState,
state.updateExtenderByBuiltIn,
state.updateExtenderDirection,
]) ])
const [exampleImage, isExampleImageLoaded] = useImage(paintByExampleFile) const [exampleImage, isExampleImageLoaded] = useImage(paintByExampleFile)
const [open, toggleOpen] = useToggle(true) const [open, toggleOpen] = useToggle(true)
@ -75,7 +172,7 @@ const SidePanel = () => {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex justify-between items-center pr-2"> <div className="flex justify-between items-center pr-2">
<Label htmlFor="controlnet">Controlnet</Label> <LabelTitle text="Controlnet" />
<Switch <Switch
id="controlnet" id="controlnet"
checked={settings.enableControlnet} checked={settings.enableControlnet}
@ -148,7 +245,11 @@ const SidePanel = () => {
return ( return (
<> <>
<RowContainer> <RowContainer>
<Label htmlFor="lcm-lora">LCM Lora</Label> <LabelTitle
text="LCM Lora"
url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inference_with_lcm_lora"
toolTip="Enable quality image generation in typically 2-4 steps. Suggest disabling guidance_scale by setting it to 0. You can also try values between 1.0 and 2.0. When LCM Lora is enabled, LCMSampler will be used automatically."
/>
<Switch <Switch
id="lcm-lora" id="lcm-lora"
checked={settings.enableLCMLora} checked={settings.enableLCMLora}
@ -170,7 +271,7 @@ const SidePanel = () => {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex justify-between items-center pr-2"> <div className="flex justify-between items-center pr-2">
<Label htmlFor="freeu">Freeu</Label> <LabelTitle text="Freeu" />
<Switch <Switch
id="freeu" id="freeu"
checked={settings.enableFreeu} checked={settings.enableFreeu}
@ -182,9 +283,11 @@ const SidePanel = () => {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex justify-center gap-6"> <div className="flex justify-center gap-6">
<div className="flex gap-2 items-center justify-center"> <div className="flex gap-2 items-center justify-center">
<Label htmlFor="freeu-s1" disabled={!settings.enableFreeu}> <LabelTitle
s1 htmlFor="freeu-s1"
</Label> text="s1"
disabled={!settings.enableFreeu}
/>
<NumberInput <NumberInput
id="freeu-s1" id="freeu-s1"
className="w-14" className="w-14"
@ -199,9 +302,11 @@ const SidePanel = () => {
/> />
</div> </div>
<div className="flex gap-2 items-center justify-center"> <div className="flex gap-2 items-center justify-center">
<Label htmlFor="freeu-s2" disabled={!settings.enableFreeu}> <LabelTitle
s2 htmlFor="freeu-s2"
</Label> text="s2"
disabled={!settings.enableFreeu}
/>
<NumberInput <NumberInput
id="freeu-s2" id="freeu-s2"
className="w-14" className="w-14"
@ -219,9 +324,11 @@ const SidePanel = () => {
<div className="flex justify-center gap-6"> <div className="flex justify-center gap-6">
<div className="flex gap-2 items-center justify-center"> <div className="flex gap-2 items-center justify-center">
<Label htmlFor="freeu-b1" disabled={!settings.enableFreeu}> <LabelTitle
b1 htmlFor="freeu-b1"
</Label> text="b1"
disabled={!settings.enableFreeu}
/>
<NumberInput <NumberInput
id="freeu-b1" id="freeu-b1"
className="w-14" className="w-14"
@ -236,9 +343,11 @@ const SidePanel = () => {
/> />
</div> </div>
<div className="flex gap-2 items-center justify-center"> <div className="flex gap-2 items-center justify-center">
<Label htmlFor="freeu-b2" disabled={!settings.enableFreeu}> <LabelTitle
b2 htmlFor="freeu-b2"
</Label> text="b2"
disabled={!settings.enableFreeu}
/>
<NumberInput <NumberInput
id="freeu-b2" id="freeu-b2"
className="w-14" className="w-14"
@ -266,7 +375,11 @@ const SidePanel = () => {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Label htmlFor="negative-prompt">Negative prompt</Label> <LabelTitle
text="Negative prompt"
url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#negative-prompt"
toolTip="Negative prompt guides the model away from generating certain things in an image"
/>
<div className="pl-2 pr-4"> <div className="pl-2 pr-4">
<Textarea <Textarea
rows={4} rows={4}
@ -336,7 +449,10 @@ const SidePanel = () => {
} }
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Label htmlFor="image-guidance-scale">Image guidance scale</Label> <LabelTitle
htmlFor="image-guidance-scale"
text="Image guidance scale"
/>
<RowContainer> <RowContainer>
<Slider <Slider
className="w-[180px]" className="w-[180px]"
@ -370,7 +486,11 @@ const SidePanel = () => {
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Label htmlFor="strength">Strength</Label> <LabelTitle
text="Strength"
url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#strength"
toolTip="Strength is a measure of how much noise is added to the base image, which influences how similar the output is to the base image. Higher value means more noise and more different from the base image"
/>
<RowContainer> <RowContainer>
<Slider <Slider
className="w-[180px]" className="w-[180px]"
@ -397,22 +517,145 @@ const SidePanel = () => {
) )
} }
const renderExpender = () => { const renderExtender = () => {
return ( return (
<> <>
<RowContainer> <div className="flex flex-col gap-4">
<Label htmlFor="Expender">Expender</Label> <RowContainer>
<Switch <LabelTitle text="Extender" />
id="expender" <Switch
checked={settings.showExpender} id="extender"
onCheckedChange={(value) => { checked={settings.showExtender}
updateSettings({ showExpender: value }) onCheckedChange={(value) => {
if (value) { updateSettings({ showExtender: value })
updateSettings({ showCropper: false }) if (value) {
} updateSettings({ showCropper: false })
}} }
/> }}
</RowContainer> />
</RowContainer>
<Tabs
defaultValue={settings.extenderDirection}
onValueChange={(value) => updateExtenderDirection(value)}
className="flex flex-col justify-center items-center"
>
<TabsList className="w-[140px] mb-2">
<TabsTrigger value={EXTENDER_X} disabled={!settings.showExtender}>
<MoveHorizontal size={20} strokeWidth={1} />
</TabsTrigger>
<TabsTrigger value={EXTENDER_Y} disabled={!settings.showExtender}>
<MoveVertical size={20} strokeWidth={1} />
</TabsTrigger>
<TabsTrigger
value={EXTENDER_ALL}
disabled={!settings.showExtender}
>
<Move size={20} strokeWidth={1} />
</TabsTrigger>
</TabsList>
<TabsContent
value={EXTENDER_X}
className="flex gap-2 justify-center mt-0"
>
<ExtenderButton
IconCls={ArrowLeftFromLine}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_LEFT, 1.5)
}
/>
<ExtenderButton
IconCls={ArrowLeftFromLine}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_LEFT, 2.0)
}
/>
<ExtenderButton
IconCls={ArrowRightFromLine}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_RIGHT, 1.5)
}
/>
<ExtenderButton
IconCls={ArrowRightFromLine}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_RIGHT, 2.0)
}
/>
</TabsContent>
<TabsContent
value={EXTENDER_Y}
className="flex gap-2 justify-center mt-0"
>
<ExtenderButton
IconCls={ArrowUpFromLine}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_TOP, 1.5)
}
/>
<ExtenderButton
IconCls={ArrowUpFromLine}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_TOP, 2.0)
}
/>
<ExtenderButton
IconCls={ArrowDownFromLine}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_BOTTOM, 1.5)
}
/>
<ExtenderButton
IconCls={ArrowDownFromLine}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_BOTTOM, 2.0)
}
/>
</TabsContent>
<TabsContent
value={EXTENDER_ALL}
className="flex gap-2 justify-center mt-0"
>
<ExtenderButton
IconCls={Maximize}
text="1.25x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.25)
}
/>
<ExtenderButton
IconCls={Maximize}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.5)
}
/>
<ExtenderButton
IconCls={Maximize}
text="1.75x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.75)
}
/>
<ExtenderButton
IconCls={Maximize}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 2.0)
}
/>
</TabsContent>
</Tabs>
</div>
<Separator /> <Separator />
</> </>
) )
@ -466,23 +709,23 @@ const SidePanel = () => {
> >
<div className="flex flex-col gap-4 mt-4"> <div className="flex flex-col gap-4 mt-4">
<RowContainer> <RowContainer>
<Label htmlFor="cropper">Cropper</Label> <LabelTitle text="Cropper" />
<Switch <Switch
id="cropper" id="cropper"
checked={settings.showCropper} checked={settings.showCropper}
onCheckedChange={(value) => { onCheckedChange={(value) => {
updateSettings({ showCropper: value }) updateSettings({ showCropper: value })
if (value) { if (value) {
updateSettings({ showExpender: false }) updateSettings({ showExtender: false })
} }
}} }}
/> />
</RowContainer> </RowContainer>
{renderExpender()} {renderExtender()}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Label htmlFor="steps">Steps</Label> <LabelTitle htmlFor="steps" text="Steps" />
<RowContainer> <RowContainer>
<Slider <Slider
className="w-[180px]" className="w-[180px]"
@ -506,12 +749,16 @@ const SidePanel = () => {
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Label htmlFor="guidance-scale">Guidance scale</Label> <LabelTitle
text="Guidance scale"
url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#guidance-scale"
toolTip="Guidance scale affects how aligned the text prompt and generated image are. Higher value means the prompt and generated image are closely aligned, so the output is a stricter interpretation of the prompt"
/>
<RowContainer> <RowContainer>
<Slider <Slider
className="w-[180px]" className="w-[180px]"
defaultValue={[750]} defaultValue={[750]}
min={100} min={0}
max={1500} max={1500}
step={1} step={1}
value={[Math.floor(settings.sdGuidanceScale * 100)]} value={[Math.floor(settings.sdGuidanceScale * 100)]}
@ -535,7 +782,7 @@ const SidePanel = () => {
{renderStrength()} {renderStrength()}
<RowContainer> <RowContainer>
<Label htmlFor="sampler">Sampler</Label> <LabelTitle text="Sampler" />
<Select <Select
value={settings.sdSampler as string} value={settings.sdSampler as string}
onValueChange={(value) => { onValueChange={(value) => {
@ -563,7 +810,11 @@ const SidePanel = () => {
<RowContainer> <RowContainer>
{/* 每次会从服务器返回更新该值 */} {/* 每次会从服务器返回更新该值 */}
<Label htmlFor="seed">Seed</Label> <LabelTitle
text="Seed"
toolTip="Using same parameters and a fixed seed can generate same result image."
/>
{/* <Pin /> */}
<div className="flex gap-2 justify-center items-center"> <div className="flex gap-2 justify-center items-center">
<Switch <Switch
id="seed" id="seed"
@ -594,7 +845,10 @@ const SidePanel = () => {
{renderLCMLora()} {renderLCMLora()}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Label htmlFor="mask-blur">Mask blur</Label> <LabelTitle
text="Mask blur"
toolTip="How much to blur the mask before processing, in pixels."
/>
<RowContainer> <RowContainer>
<Slider <Slider
className="w-[180px]" className="w-[180px]"
@ -620,7 +874,11 @@ const SidePanel = () => {
</div> </div>
<RowContainer> <RowContainer>
<Label htmlFor="match-histograms">Match histograms</Label> <LabelTitle
text="Match histograms"
toolTip="Match the inpainting result histogram to the source image histogram"
url="https://github.com/Sanster/lama-cleaner/pull/143#issuecomment-1325859307"
/>
<Switch <Switch
id="match-histograms" id="match-histograms"
checked={settings.sdMatchHistograms} checked={settings.sdMatchHistograms}

View File

@ -4,12 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Input } from "./input" import { Input } from "./input"
import { import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./tooltip"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
@ -72,27 +67,23 @@ export interface IconButtonProps extends ButtonProps {
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>( const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
({ tooltip, children, ...rest }, ref) => { ({ tooltip, children, ...rest }, ref) => {
return ( return (
<> <Tooltip>
<TooltipProvider> <TooltipTrigger asChild>
<Tooltip> <Button
<TooltipTrigger asChild> variant="ghost"
<Button size="icon"
variant="ghost" {...rest}
size="icon" ref={ref}
{...rest} tabIndex={-1}
ref={ref} className="cursor-default bg-background"
tabIndex={-1} >
className="cursor-default bg-background" <div className="icon-button-icon-wrapper">{children}</div>
> </Button>
<div className="icon-button-icon-wrapper">{children}</div> </TooltipTrigger>
</Button> <TooltipContent>
</TooltipTrigger> <p>{tooltip}</p>
<TooltipContent> </TooltipContent>
<p>{tooltip}</p> </Tooltip>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
) )
} }
) )

View File

@ -20,7 +20,12 @@ const Label = React.forwardRef<
>(({ className, disabled, ...props }, ref) => ( >(({ className, disabled, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root
ref={ref} ref={ref}
className={cn(labelVariants(), className, disabled ? "opacity-50" : "")} className={cn(
labelVariants(),
className,
disabled ? "opacity-50" : "",
"select-none"
)}
{...props} {...props}
/> />
)) ))

View File

@ -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<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -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", "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className className
)} )}
tabIndex={-1}
{...props} {...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", "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 className
)} )}
tabIndex={-1}
{...props} {...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", "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className className
)} )}
tabIndex={-1}
{...props} {...props}
/> />
)) ))

View File

@ -1,6 +1,6 @@
import { ModelInfo, Rect } from "@/lib/types" import { ModelInfo, Rect } from "@/lib/types"
import { Settings } from "@/lib/states" import { Settings } from "@/lib/states"
import { dataURItoBlob, srcToFile } from "@/lib/utils" import { srcToFile } from "@/lib/utils"
import axios from "axios" import axios from "axios"
export const API_ENDPOINT = import.meta.env.VITE_BACKEND export const API_ENDPOINT = import.meta.env.VITE_BACKEND
@ -15,6 +15,7 @@ export default async function inpaint(
imageFile: File, imageFile: File,
settings: Settings, settings: Settings,
croperRect: Rect, croperRect: Rect,
extenderState: Rect,
mask: File | Blob, mask: File | Blob,
paintByExampleImage: File | null = null paintByExampleImage: File | null = null
) { ) {
@ -32,11 +33,18 @@ export default async function inpaint(
fd.append("prompt", settings.prompt) fd.append("prompt", settings.prompt)
fd.append("negativePrompt", settings.negativePrompt) fd.append("negativePrompt", settings.negativePrompt)
fd.append("useCroper", settings.showCropper ? "true" : "false")
fd.append("croperX", croperRect.x.toString()) fd.append("croperX", croperRect.x.toString())
fd.append("croperY", croperRect.y.toString()) fd.append("croperY", croperRect.y.toString())
fd.append("croperHeight", croperRect.height.toString()) fd.append("croperHeight", croperRect.height.toString())
fd.append("croperWidth", croperRect.width.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("sdMaskBlur", settings.sdMaskBlur.toString())
fd.append("sdStrength", settings.sdStrength.toString()) fd.append("sdStrength", settings.sdStrength.toString())
@ -82,7 +90,7 @@ export default async function inpaint(
}) })
if (res.ok) { if (res.ok) {
const blob = await res.blob() 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 } return { blob: URL.createObjectURL(blob), seed: newSeed }
} }
const errMsg = await res.text() const errMsg = await res.text()

View File

@ -8,6 +8,15 @@ export const MODEL_TYPE_DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint"
export const MODEL_TYPE_OTHER = "diffusers_other" export const MODEL_TYPE_OTHER = "diffusers_other"
export const BRUSH_COLOR = "#ffcc00bb" 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 PAINT_BY_EXAMPLE = "Fantasy-Studio/Paint-by-Example"
export const INSTRUCT_PIX2PIX = "timbrooks/instruct-pix2pix" export const INSTRUCT_PIX2PIX = "timbrooks/instruct-pix2pix"
export const KANDINSKY_2_2 = "kandinsky-community/kandinsky-2-2-decoder-inpaint" export const KANDINSKY_2_2 = "kandinsky-community/kandinsky-2-2-decoder-inpaint"

View File

@ -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

View File

@ -21,6 +21,14 @@ import {
BRUSH_COLOR, BRUSH_COLOR,
DEFAULT_BRUSH_SIZE, DEFAULT_BRUSH_SIZE,
DEFAULT_NEGATIVE_PROMPT, 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, MODEL_TYPE_INPAINT,
PAINT_BY_EXAMPLE, PAINT_BY_EXAMPLE,
} from "./const" } from "./const"
@ -56,7 +64,8 @@ export type Settings = {
enableManualInpainting: boolean enableManualInpainting: boolean
enableUploadMask: boolean enableUploadMask: boolean
showCropper: boolean showCropper: boolean
showExpender: boolean showExtender: boolean
extenderDirection: string
// For LDM // For LDM
ldmSteps: number ldmSteps: number
@ -168,6 +177,9 @@ type AppAction = {
setExtenderY: (newValue: number) => void setExtenderY: (newValue: number) => void
setExtenderWidth: (newValue: number) => void setExtenderWidth: (newValue: number) => void
setExtenderHeight: (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 setServerConfig: (newValue: ServerConfig) => void
setSeed: (newValue: number) => void setSeed: (newValue: number) => void
@ -281,7 +293,8 @@ const defaultValues: AppState = {
}, },
enableControlnet: false, enableControlnet: false,
showCropper: false, showCropper: false,
showExpender: false, showExtender: false,
extenderDirection: EXTENDER_ALL,
enableDownloadMask: false, enableDownloadMask: false,
enableManualInpainting: false, enableManualInpainting: false,
enableUploadMask: false, enableUploadMask: false,
@ -362,7 +375,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
} }
return targetFile return targetFile
}, },
// todo: 传入 custom mask单独逻辑
runInpainting: async () => { runInpainting: async () => {
const { const {
isInpainting, isInpainting,
@ -372,6 +385,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
imageHeight, imageHeight,
settings, settings,
cropperState, cropperState,
extenderState,
} = get() } = get()
if (isInpainting) { if (isInpainting) {
return return
@ -398,7 +412,11 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
// 2. 结果替换当前 render // 2. 结果替换当前 render
let maskLineGroup: LineGroup = [] let maskLineGroup: LineGroup = []
if (useLastLineGroup === true) { if (useLastLineGroup === true) {
if (lastLineGroup.length === 0 && maskImage === null) { if (
lastLineGroup.length === 0 &&
maskImage === null &&
!settings.showExtender
) {
toast({ toast({
variant: "destructive", variant: "destructive",
description: "Please draw mask on picture", description: "Please draw mask on picture",
@ -407,7 +425,11 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
} }
maskLineGroup = lastLineGroup maskLineGroup = lastLineGroup
} else { } else {
if (curLineGroup.length === 0 && maskImage === null) { if (
curLineGroup.length === 0 &&
maskImage === null &&
!settings.showExtender
) {
toast({ toast({
variant: "destructive", variant: "destructive",
description: "Please draw mask on picture", description: "Please draw mask on picture",
@ -455,6 +477,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
targetFile, targetFile,
settings, settings,
cropperState, cropperState,
extenderState,
dataURItoBlob(maskCanvas.toDataURL()), dataURItoBlob(maskCanvas.toDataURL()),
paintByExampleFile paintByExampleFile
) )
@ -465,7 +488,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
const { blob, seed } = res const { blob, seed } = res
if (seed) { if (seed) {
set((state) => (state.settings.seed = parseInt(seed, 10))) get().setSeed(parseInt(seed, 10))
} }
const newRender = new Image() const newRender = new Image()
await loadImage(newRender, blob) await loadImage(newRender, blob)
@ -794,7 +817,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
state.isPluginRunning = newValue state.isPluginRunning = newValue
}), }),
setFile: (file: File) => setFile: (file: File) => {
set((state) => { set((state) => {
state.file = file state.file = file
state.interactiveSegState = castDraft( state.interactiveSegState = castDraft(
@ -802,7 +825,8 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
) )
state.editorState = castDraft(defaultValues.editorState) state.editorState = castDraft(defaultValues.editorState)
state.cropperState = defaultValues.cropperState state.cropperState = defaultValues.cropperState
}), })
},
setCustomFile: (file: File) => setCustomFile: (file: File) =>
set((state) => { set((state) => {
@ -822,6 +846,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
state.editorState.brushSizeScale = state.editorState.brushSizeScale =
Math.max(Math.min(width, height), 512) / 512 Math.max(Math.min(width, height), 512) / 512
}) })
get().resetExtender(width, height)
}, },
setCropperX: (newValue: number) => setCropperX: (newValue: number) =>
@ -864,6 +889,68 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
state.extenderState.height = newValue 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) => setSeed: (newValue: number) =>
set((state) => { set((state) => {
state.settings.seed = newValue state.settings.seed = newValue
@ -871,7 +958,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
})), })),
{ {
name: "ZUSTAND_STATE", // name of the item in the storage (must be unique) name: "ZUSTAND_STATE", // name of the item in the storage (must be unique)
version: 1, version: 0,
partialize: (state) => partialize: (state) =>
Object.fromEntries( Object.fromEntries(
Object.entries(state).filter(([key]) => Object.entries(state).filter(([key]) =>

View File

@ -67,7 +67,7 @@ export enum SDSampler {
kEulerA = "k_euler_a", kEulerA = "k_euler_a",
dpmPlusPlus = "dpm++", dpmPlusPlus = "dpm++",
uni_pc = "uni_pc", uni_pc = "uni_pc",
lcm = "lcm", // lcm = "lcm",
} }
export interface FreeuConfig { export interface FreeuConfig {

View File

@ -71,6 +71,26 @@ export function canvasToImage(
}) })
} }
export function fileToImage(file: File): Promise<HTMLImageElement> {
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) { export function srcToFile(src: string, fileName: string, mimeType: string) {
return fetch(src) return fetch(src)
.then(function (res) { .then(function (res) {

View File

@ -5,6 +5,7 @@ import "inter-ui/inter.css"
import App from "./App.tsx" import App from "./App.tsx"
import "./globals.css" import "./globals.css"
import { ThemeProvider } from "next-themes" import { ThemeProvider } from "next-themes"
import { TooltipProvider } from "./components/ui/tooltip.tsx"
const queryClient = new QueryClient() const queryClient = new QueryClient()
@ -12,7 +13,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="dark" disableTransitionOnChange> <ThemeProvider defaultTheme="dark" disableTransitionOnChange>
<App /> <TooltipProvider>
<App />
</TooltipProvider>
</ThemeProvider> </ThemeProvider>
</QueryClientProvider> </QueryClientProvider>
</React.StrictMode> </React.StrictMode>