update
This commit is contained in:
parent
141936a937
commit
eb9764176c
33
web_app/package-lock.json
generated
33
web_app/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
<Cropper
|
||||
maxHeight={imageHeight}
|
||||
maxWidth={imageWidth}
|
||||
minHeight={Math.min(256, imageHeight)}
|
||||
minWidth={Math.min(256, imageWidth)}
|
||||
minHeight={Math.min(512, imageHeight)}
|
||||
minWidth={Math.min(512, imageWidth)}
|
||||
scale={getCurScale()}
|
||||
show={settings.showCropper}
|
||||
/>
|
||||
|
||||
<Extender
|
||||
maxHeight={imageHeight}
|
||||
maxWidth={imageWidth}
|
||||
minHeight={Math.min(256, imageHeight)}
|
||||
minWidth={Math.min(256, imageWidth)}
|
||||
minHeight={Math.min(512, imageHeight)}
|
||||
minWidth={Math.min(512, imageWidth)}
|
||||
scale={getCurScale()}
|
||||
show={settings.showExpender}
|
||||
show={settings.showExtender}
|
||||
/>
|
||||
|
||||
{interactiveSegState.isInteractiveSeg ? (
|
||||
|
@ -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<EVData>({
|
||||
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<HTMLDivElement>) => {
|
||||
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"
|
||||
>
|
||||
<div
|
||||
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 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]"
|
||||
data-ord="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")}
|
||||
{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) ? (
|
||||
<>
|
||||
<div
|
||||
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 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", "")}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{[EXTENDER_X, EXTENDER_ALL].includes(extenderDirection) ? (
|
||||
<>
|
||||
<div
|
||||
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 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>
|
||||
)
|
||||
}
|
||||
|
||||
const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
setIsMoving(true)
|
||||
setEVData({
|
||||
initX: x,
|
||||
initY: y,
|
||||
@ -345,7 +359,7 @@ const Extender = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
)}
|
||||
style={{
|
||||
@ -362,7 +376,7 @@ const Extender = (props: Props) => {
|
||||
const createBorder = () => {
|
||||
return (
|
||||
<div
|
||||
className="outline-dashed outline-primary"
|
||||
className={cn("outline-dashed outline-primary")}
|
||||
style={{
|
||||
height,
|
||||
width,
|
@ -79,10 +79,6 @@ export default function FileManager(props: Props) {
|
||||
state.updateFileManagerState,
|
||||
])
|
||||
|
||||
useHotKey("f", () => {
|
||||
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<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(() => {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
<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">
|
||||
@ -103,10 +98,31 @@ const Header = () => {
|
||||
<ImageUploadButton
|
||||
disabled={isInpainting}
|
||||
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)
|
||||
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 ? (
|
||||
<IconButton
|
||||
disabled={isInpainting}
|
||||
tooltip="Rerun last mask"
|
||||
tooltip="Rerun previous mask"
|
||||
onClick={handleRerunLastMask}
|
||||
onMouseEnter={onRerunMouseEnter}
|
||||
onMouseLeave={onRerunMouseLeave}
|
||||
|
@ -314,7 +314,7 @@ export function SettingsDialog() {
|
||||
|
||||
<Separator />
|
||||
|
||||
<FormField
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="enableUploadMask"
|
||||
render={({ field }) => (
|
||||
@ -334,7 +334,7 @@ export function SettingsDialog() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<Separator /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -74,7 +74,6 @@ export function Shortcuts() {
|
||||
/>
|
||||
<ShortCut content="Cancel Drawing" keys={["Esc"]} />
|
||||
|
||||
<ShortCut content="Rerun last mask" keys={["R"]} />
|
||||
<ShortCut content="Undo" keys={[CmdOrCtrl(), "Z"]} />
|
||||
<ShortCut content="Redo" keys={[CmdOrCtrl(), "Shift", "Z"]} />
|
||||
<ShortCut content="Copy Result" keys={[CmdOrCtrl(), "C"]} />
|
||||
|
@ -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 }) => (
|
||||
<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 [
|
||||
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 = () => {
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center pr-2">
|
||||
<Label htmlFor="controlnet">Controlnet</Label>
|
||||
<LabelTitle text="Controlnet" />
|
||||
<Switch
|
||||
id="controlnet"
|
||||
checked={settings.enableControlnet}
|
||||
@ -148,7 +245,11 @@ const SidePanel = () => {
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
id="lcm-lora"
|
||||
checked={settings.enableLCMLora}
|
||||
@ -170,7 +271,7 @@ const SidePanel = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center pr-2">
|
||||
<Label htmlFor="freeu">Freeu</Label>
|
||||
<LabelTitle text="Freeu" />
|
||||
<Switch
|
||||
id="freeu"
|
||||
checked={settings.enableFreeu}
|
||||
@ -182,9 +283,11 @@ const SidePanel = () => {
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-center gap-6">
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<Label htmlFor="freeu-s1" disabled={!settings.enableFreeu}>
|
||||
s1
|
||||
</Label>
|
||||
<LabelTitle
|
||||
htmlFor="freeu-s1"
|
||||
text="s1"
|
||||
disabled={!settings.enableFreeu}
|
||||
/>
|
||||
<NumberInput
|
||||
id="freeu-s1"
|
||||
className="w-14"
|
||||
@ -199,9 +302,11 @@ const SidePanel = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<Label htmlFor="freeu-s2" disabled={!settings.enableFreeu}>
|
||||
s2
|
||||
</Label>
|
||||
<LabelTitle
|
||||
htmlFor="freeu-s2"
|
||||
text="s2"
|
||||
disabled={!settings.enableFreeu}
|
||||
/>
|
||||
<NumberInput
|
||||
id="freeu-s2"
|
||||
className="w-14"
|
||||
@ -219,9 +324,11 @@ const SidePanel = () => {
|
||||
|
||||
<div className="flex justify-center gap-6">
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<Label htmlFor="freeu-b1" disabled={!settings.enableFreeu}>
|
||||
b1
|
||||
</Label>
|
||||
<LabelTitle
|
||||
htmlFor="freeu-b1"
|
||||
text="b1"
|
||||
disabled={!settings.enableFreeu}
|
||||
/>
|
||||
<NumberInput
|
||||
id="freeu-b1"
|
||||
className="w-14"
|
||||
@ -236,9 +343,11 @@ const SidePanel = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<Label htmlFor="freeu-b2" disabled={!settings.enableFreeu}>
|
||||
b2
|
||||
</Label>
|
||||
<LabelTitle
|
||||
htmlFor="freeu-b2"
|
||||
text="b2"
|
||||
disabled={!settings.enableFreeu}
|
||||
/>
|
||||
<NumberInput
|
||||
id="freeu-b2"
|
||||
className="w-14"
|
||||
@ -266,7 +375,11 @@ const SidePanel = () => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Textarea
|
||||
rows={4}
|
||||
@ -336,7 +449,10 @@ const SidePanel = () => {
|
||||
}
|
||||
return (
|
||||
<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>
|
||||
<Slider
|
||||
className="w-[180px]"
|
||||
@ -370,7 +486,11 @@ const SidePanel = () => {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Slider
|
||||
className="w-[180px]"
|
||||
@ -397,22 +517,145 @@ const SidePanel = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const renderExpender = () => {
|
||||
const renderExtender = () => {
|
||||
return (
|
||||
<>
|
||||
<RowContainer>
|
||||
<Label htmlFor="Expender">Expender</Label>
|
||||
<Switch
|
||||
id="expender"
|
||||
checked={settings.showExpender}
|
||||
onCheckedChange={(value) => {
|
||||
updateSettings({ showExpender: value })
|
||||
if (value) {
|
||||
updateSettings({ showCropper: false })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</RowContainer>
|
||||
<div className="flex flex-col gap-4">
|
||||
<RowContainer>
|
||||
<LabelTitle text="Extender" />
|
||||
<Switch
|
||||
id="extender"
|
||||
checked={settings.showExtender}
|
||||
onCheckedChange={(value) => {
|
||||
updateSettings({ showExtender: value })
|
||||
if (value) {
|
||||
updateSettings({ showCropper: false })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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 />
|
||||
</>
|
||||
)
|
||||
@ -466,23 +709,23 @@ const SidePanel = () => {
|
||||
>
|
||||
<div className="flex flex-col gap-4 mt-4">
|
||||
<RowContainer>
|
||||
<Label htmlFor="cropper">Cropper</Label>
|
||||
<LabelTitle text="Cropper" />
|
||||
<Switch
|
||||
id="cropper"
|
||||
checked={settings.showCropper}
|
||||
onCheckedChange={(value) => {
|
||||
updateSettings({ showCropper: value })
|
||||
if (value) {
|
||||
updateSettings({ showExpender: false })
|
||||
updateSettings({ showExtender: false })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</RowContainer>
|
||||
|
||||
{renderExpender()}
|
||||
{renderExtender()}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label htmlFor="steps">Steps</Label>
|
||||
<LabelTitle htmlFor="steps" text="Steps" />
|
||||
<RowContainer>
|
||||
<Slider
|
||||
className="w-[180px]"
|
||||
@ -506,12 +749,16 @@ const SidePanel = () => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<Slider
|
||||
className="w-[180px]"
|
||||
defaultValue={[750]}
|
||||
min={100}
|
||||
min={0}
|
||||
max={1500}
|
||||
step={1}
|
||||
value={[Math.floor(settings.sdGuidanceScale * 100)]}
|
||||
@ -535,7 +782,7 @@ const SidePanel = () => {
|
||||
{renderStrength()}
|
||||
|
||||
<RowContainer>
|
||||
<Label htmlFor="sampler">Sampler</Label>
|
||||
<LabelTitle text="Sampler" />
|
||||
<Select
|
||||
value={settings.sdSampler as string}
|
||||
onValueChange={(value) => {
|
||||
@ -563,7 +810,11 @@ const SidePanel = () => {
|
||||
|
||||
<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">
|
||||
<Switch
|
||||
id="seed"
|
||||
@ -594,7 +845,10 @@ const SidePanel = () => {
|
||||
{renderLCMLora()}
|
||||
|
||||
<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>
|
||||
<Slider
|
||||
className="w-[180px]"
|
||||
@ -620,7 +874,11 @@ const SidePanel = () => {
|
||||
</div>
|
||||
|
||||
<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
|
||||
id="match-histograms"
|
||||
checked={settings.sdMatchHistograms}
|
||||
|
@ -4,12 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Input } from "./input"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./tooltip"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
|
||||
|
||||
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",
|
||||
@ -72,27 +67,23 @@ export interface IconButtonProps extends ButtonProps {
|
||||
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
({ tooltip, children, ...rest }, ref) => {
|
||||
return (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
className="cursor-default bg-background"
|
||||
>
|
||||
<div className="icon-button-icon-wrapper">{children}</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
className="cursor-default bg-background"
|
||||
>
|
||||
<div className="icon-button-icon-wrapper">{children}</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -20,7 +20,12 @@ const Label = React.forwardRef<
|
||||
>(({ className, disabled, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className, disabled ? "opacity-50" : "")}
|
||||
className={cn(
|
||||
labelVariants(),
|
||||
className,
|
||||
disabled ? "opacity-50" : "",
|
||||
"select-none"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
42
web_app/src/components/ui/radio-group.tsx
Normal file
42
web_app/src/components/ui/radio-group.tsx
Normal 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 }
|
@ -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}
|
||||
/>
|
||||
))
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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
|
@ -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<AppState & AppAction>()(
|
||||
}
|
||||
return targetFile
|
||||
},
|
||||
|
||||
// todo: 传入 custom mask,单独逻辑
|
||||
runInpainting: async () => {
|
||||
const {
|
||||
isInpainting,
|
||||
@ -372,6 +385,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
|
||||
imageHeight,
|
||||
settings,
|
||||
cropperState,
|
||||
extenderState,
|
||||
} = get()
|
||||
if (isInpainting) {
|
||||
return
|
||||
@ -398,7 +412,11 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
|
||||
// 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<AppState & AppAction>()(
|
||||
}
|
||||
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<AppState & AppAction>()(
|
||||
targetFile,
|
||||
settings,
|
||||
cropperState,
|
||||
extenderState,
|
||||
dataURItoBlob(maskCanvas.toDataURL()),
|
||||
paintByExampleFile
|
||||
)
|
||||
@ -465,7 +488,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
|
||||
|
||||
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<AppState & AppAction>()(
|
||||
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<AppState & AppAction>()(
|
||||
)
|
||||
state.editorState = castDraft(defaultValues.editorState)
|
||||
state.cropperState = defaultValues.cropperState
|
||||
}),
|
||||
})
|
||||
},
|
||||
|
||||
setCustomFile: (file: File) =>
|
||||
set((state) => {
|
||||
@ -822,6 +846,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
|
||||
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<AppState & AppAction>()(
|
||||
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<AppState & AppAction>()(
|
||||
})),
|
||||
{
|
||||
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]) =>
|
||||
|
@ -67,7 +67,7 @@ export enum SDSampler {
|
||||
kEulerA = "k_euler_a",
|
||||
dpmPlusPlus = "dpm++",
|
||||
uni_pc = "uni_pc",
|
||||
lcm = "lcm",
|
||||
// lcm = "lcm",
|
||||
}
|
||||
|
||||
export interface FreeuConfig {
|
||||
|
@ -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) {
|
||||
return fetch(src)
|
||||
.then(function (res) {
|
||||
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider defaultTheme="dark" disableTransitionOnChange>
|
||||
<App />
|
||||
<TooltipProvider>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
|
Loading…
Reference in New Issue
Block a user