wip
This commit is contained in:
parent
43433c50eb
commit
973987dfbb
118
web_app/package-lock.json
generated
118
web_app/package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
@ -27,6 +28,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"flexsearch": "^0.7.21",
|
||||
"immer": "^10.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.292.0",
|
||||
"mitt": "^3.0.1",
|
||||
@ -39,7 +41,8 @@
|
||||
"react-zoom-pan-pinch": "^3.3.0",
|
||||
"recoil": "^0.7.7",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/flexsearch": "^0.7.3",
|
||||
@ -1564,6 +1567,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz",
|
||||
"integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==",
|
||||
"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-id": "1.0.1",
|
||||
"@radix-ui/react-menu": "2.0.6",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-controllable-state": "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-focus-guards": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
|
||||
@ -1655,6 +1687,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz",
|
||||
"integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-collection": "1.0.3",
|
||||
"@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-dismissable-layer": "1.0.5",
|
||||
"@radix-ui/react-focus-guards": "1.0.1",
|
||||
"@radix-ui/react-focus-scope": "1.0.4",
|
||||
"@radix-ui/react-id": "1.0.1",
|
||||
"@radix-ui/react-popper": "1.1.3",
|
||||
"@radix-ui/react-portal": "1.0.4",
|
||||
"@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-slot": "1.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.5"
|
||||
},
|
||||
"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-popover": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
|
||||
@ -4023,6 +4095,15 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
|
||||
"integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@ -5652,6 +5733,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -5770,6 +5859,33 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz",
|
||||
"integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
@ -29,6 +30,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"flexsearch": "^0.7.21",
|
||||
"immer": "^10.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.292.0",
|
||||
"mitt": "^3.0.1",
|
||||
@ -41,7 +43,8 @@
|
||||
"react-zoom-pan-pinch": "^3.3.0",
|
||||
"recoil": "^0.7.7",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/flexsearch": "^0.7.3",
|
||||
|
418
web_app/src/components/Cropper.tsx
Normal file
418
web_app/src/components/Cropper.tsx
Normal file
@ -0,0 +1,418 @@
|
||||
import { useStore } from "@/lib/states"
|
||||
import React, { useEffect, useState } from "react"
|
||||
|
||||
const DOC_MOVE_OPTS = { capture: true, passive: false }
|
||||
|
||||
const DRAG_HANDLE_BORDER = 2
|
||||
|
||||
interface EVData {
|
||||
initX: number
|
||||
initY: number
|
||||
initHeight: number
|
||||
initWidth: number
|
||||
startResizeX: number
|
||||
startResizeY: number
|
||||
ord: string // top/right/bottom/left
|
||||
}
|
||||
|
||||
interface Props {
|
||||
maxHeight: number
|
||||
maxWidth: number
|
||||
scale: number
|
||||
minHeight: number
|
||||
minWidth: number
|
||||
show: boolean
|
||||
}
|
||||
|
||||
const clamp = (
|
||||
newPos: number,
|
||||
newLength: number,
|
||||
oldPos: number,
|
||||
oldLength: number,
|
||||
minLength: number,
|
||||
maxLength: number
|
||||
) => {
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
return [newPos, newLength]
|
||||
}
|
||||
|
||||
const Cropper = (props: Props) => {
|
||||
const { minHeight, minWidth, maxHeight, maxWidth, scale, show } = props
|
||||
|
||||
const [
|
||||
isInpainting,
|
||||
{ x, y, width, height },
|
||||
setX,
|
||||
setY,
|
||||
setWidth,
|
||||
setHeight,
|
||||
] = useStore((state) => [
|
||||
state.isInpainting,
|
||||
state.cropperState,
|
||||
state.setCropperX,
|
||||
state.setCropperY,
|
||||
state.setCropperWidth,
|
||||
state.setCropperHeight,
|
||||
])
|
||||
// const [x, setX] = useRecoilState(croperX)
|
||||
// const [y, setY] = useRecoilState(croperY)
|
||||
// const [height, setHeight] = useRecoilState(croperHeight)
|
||||
// const [width, setWidth] = useRecoilState(croperWidth)
|
||||
// const isInpainting = useRecoilValue(isInpaintingState)
|
||||
|
||||
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])
|
||||
|
||||
const [evData, setEVData] = useState<EVData>({
|
||||
initX: 0,
|
||||
initY: 0,
|
||||
initHeight: 0,
|
||||
initWidth: 0,
|
||||
startResizeX: 0,
|
||||
startResizeY: 0,
|
||||
ord: "top",
|
||||
})
|
||||
|
||||
const onDragFocus = () => {
|
||||
console.log("focus")
|
||||
}
|
||||
|
||||
const clampLeftRight = (newX: number, newWidth: number) => {
|
||||
return clamp(newX, newWidth, x, width, minWidth, maxWidth)
|
||||
}
|
||||
|
||||
const clampTopBottom = (newY: number, newHeight: number) => {
|
||||
return clamp(newY, newHeight, y, height, minHeight, maxHeight)
|
||||
}
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
const curX = e.clientX
|
||||
const curY = e.clientY
|
||||
|
||||
const offsetY = Math.round((curY - evData.startResizeY) / scale)
|
||||
const offsetX = Math.round((curX - evData.startResizeX) / scale)
|
||||
|
||||
const moveTop = () => {
|
||||
const newHeight = evData.initHeight - offsetY
|
||||
const newY = evData.initY + offsetY
|
||||
const [clampedY, clampedHeight] = clampTopBottom(newY, newHeight)
|
||||
setHeight(clampedHeight)
|
||||
setY(clampedY)
|
||||
}
|
||||
|
||||
const moveBottom = () => {
|
||||
const newHeight = evData.initHeight + offsetY
|
||||
const [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight)
|
||||
setHeight(clampedHeight)
|
||||
setY(clampedY)
|
||||
}
|
||||
|
||||
const moveLeft = () => {
|
||||
const newWidth = evData.initWidth - offsetX
|
||||
const newX = evData.initX + offsetX
|
||||
const [clampedX, clampedWidth] = clampLeftRight(newX, newWidth)
|
||||
setWidth(clampedWidth)
|
||||
setX(clampedX)
|
||||
}
|
||||
|
||||
const moveRight = () => {
|
||||
const newWidth = evData.initWidth + offsetX
|
||||
const [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth)
|
||||
setWidth(clampedWidth)
|
||||
setX(clampedX)
|
||||
}
|
||||
|
||||
if (isResizing) {
|
||||
switch (evData.ord) {
|
||||
case "topleft": {
|
||||
moveTop()
|
||||
moveLeft()
|
||||
break
|
||||
}
|
||||
case "topright": {
|
||||
moveTop()
|
||||
moveRight()
|
||||
break
|
||||
}
|
||||
case "bottomleft": {
|
||||
moveBottom()
|
||||
moveLeft()
|
||||
break
|
||||
}
|
||||
case "bottomright": {
|
||||
moveBottom()
|
||||
moveRight()
|
||||
break
|
||||
}
|
||||
case "top": {
|
||||
moveTop()
|
||||
break
|
||||
}
|
||||
case "right": {
|
||||
moveRight()
|
||||
break
|
||||
}
|
||||
case "bottom": {
|
||||
moveBottom()
|
||||
break
|
||||
}
|
||||
case "left": {
|
||||
moveLeft()
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
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) {
|
||||
document.addEventListener("pointermove", onPointerMove, DOC_MOVE_OPTS)
|
||||
document.addEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
|
||||
document.addEventListener("pointercancel", onPointerDone, DOC_MOVE_OPTS)
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"pointermove",
|
||||
onPointerMove,
|
||||
DOC_MOVE_OPTS
|
||||
)
|
||||
document.removeEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
|
||||
document.removeEventListener(
|
||||
"pointercancel",
|
||||
onPointerDone,
|
||||
DOC_MOVE_OPTS
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [isResizing, isMoving, width, height, evData])
|
||||
|
||||
const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const { ord } = (e.target as HTMLElement).dataset
|
||||
if (ord) {
|
||||
setIsResizing(true)
|
||||
setEVData({
|
||||
initX: x,
|
||||
initY: y,
|
||||
initHeight: height,
|
||||
initWidth: width,
|
||||
startResizeX: e.clientX,
|
||||
startResizeY: e.clientY,
|
||||
ord,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const createCropSelection = () => {
|
||||
return (
|
||||
<div
|
||||
className="drag-elements"
|
||||
onFocus={onDragFocus}
|
||||
onPointerDown={onCropPointerDown}
|
||||
>
|
||||
<div
|
||||
className="drag-bar ord-top"
|
||||
data-ord="top"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-bar ord-right"
|
||||
data-ord="right"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-bar ord-bottom"
|
||||
data-ord="bottom"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-bar ord-left"
|
||||
data-ord="left"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-topleft"
|
||||
data-ord="topleft"
|
||||
aria-label="topleft"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-topright"
|
||||
data-ord="topright"
|
||||
aria-label="topright"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-bottomleft"
|
||||
data-ord="bottomleft"
|
||||
aria-label="bottomleft"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-bottomright"
|
||||
data-ord="bottomright"
|
||||
aria-label="bottomright"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-top"
|
||||
data-ord="top"
|
||||
aria-label="top"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-right"
|
||||
data-ord="right"
|
||||
aria-label="right"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-bottom"
|
||||
data-ord="bottom"
|
||||
aria-label="bottom"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-left"
|
||||
data-ord="left"
|
||||
aria-label="left"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
setIsMoving(true)
|
||||
setEVData({
|
||||
initX: x,
|
||||
initY: y,
|
||||
initHeight: height,
|
||||
initWidth: width,
|
||||
startResizeX: e.clientX,
|
||||
startResizeY: e.clientY,
|
||||
ord: "",
|
||||
})
|
||||
}
|
||||
|
||||
const createInfoBar = () => {
|
||||
return (
|
||||
<div
|
||||
className="border absolute pointer-events-auto text-[1rem] px-[0.8rem] py-[0.2rem] flex items-center justify-center gap-[12px] rounded-full hover:cursor-move"
|
||||
onPointerDown={onInfoBarPointerDown}
|
||||
style={{
|
||||
transform: `scale(${1 / scale})`,
|
||||
top: `${10 / scale}px`,
|
||||
left: `${10 / scale}px`,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{width} x {height}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const createBorder = () => {
|
||||
return (
|
||||
<div
|
||||
className="outline-dashed outline-primary"
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
outlineWidth: `${DRAG_HANDLE_BORDER / scale}px`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute h-full w-full overflow-hidden pointer-events-none"
|
||||
style={{ visibility: show ? "visible" : "hidden" }}
|
||||
>
|
||||
<div
|
||||
className="relative pointer-events-none"
|
||||
style={{ height, width, left: x, top: y }}
|
||||
>
|
||||
{createBorder()}
|
||||
{createInfoBar()}
|
||||
{createCropSelection()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cropper
|
@ -2,6 +2,7 @@ import { SyntheticEvent, useCallback, useEffect, useRef, useState } from "react"
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/outline"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
ReactZoomPanPinchContentRef,
|
||||
ReactZoomPanPinchRef,
|
||||
TransformComponent,
|
||||
TransformWrapper,
|
||||
@ -55,6 +56,8 @@ import { Slider } from "./ui/slider"
|
||||
import { PluginName } from "@/lib/types"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { useStore } from "@/lib/states"
|
||||
import Cropper from "./Cropper"
|
||||
import { HotkeysEvent } from "react-hotkeys-hook/dist/types"
|
||||
|
||||
const TOOLBAR_HEIGHT = 200
|
||||
const MIN_BRUSH_SIZE = 10
|
||||
@ -193,7 +196,7 @@ export default function Editor(props: EditorProps) {
|
||||
const windowSize = useWindowSize()
|
||||
const windowCenterX = windowSize.width / 2
|
||||
const windowCenterY = windowSize.height / 2
|
||||
const viewportRef = useRef<ReactZoomPanPinchRef | null>(null)
|
||||
const viewportRef = useRef<ReactZoomPanPinchContentRef | null>(null)
|
||||
// Indicates that the image has been loaded and is centered on first load
|
||||
const [initialCentered, setInitialCentered] = useState(false)
|
||||
|
||||
@ -1119,9 +1122,8 @@ export default function Editor(props: EditorProps) {
|
||||
context,
|
||||
])
|
||||
|
||||
const undo = () => {
|
||||
// TODO: prevent default event
|
||||
console.log("undo")
|
||||
const undo = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
|
||||
keyboardEvent.preventDefault()
|
||||
if (runMannually && curLineGroup.length !== 0) {
|
||||
undoStroke()
|
||||
} else {
|
||||
@ -1186,7 +1188,8 @@ export default function Editor(props: EditorProps) {
|
||||
// draw(newRenders[newRenders.length - 1], [])
|
||||
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
|
||||
|
||||
const redo = () => {
|
||||
const redo = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
|
||||
keyboardEvent.preventDefault()
|
||||
if (runMannually && redoCurLines.length !== 0) {
|
||||
redoStroke()
|
||||
} else {
|
||||
@ -1194,29 +1197,12 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Cmd+shift+Z
|
||||
const redoPredicate = (event: KeyboardEvent) => {
|
||||
const isCmdZ =
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === "z"
|
||||
// Handle tab switch
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault()
|
||||
}
|
||||
if (isCmdZ) {
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// useKey(redoPredicate, redo, undefined, [
|
||||
// redoStroke,
|
||||
// redoRender,
|
||||
// runMannually,
|
||||
// redoCurLines,
|
||||
// ])
|
||||
useHotkeys("shift+ctrl+z,shift+meta+z", redo, undefined, [
|
||||
redoStroke,
|
||||
redoRender,
|
||||
runMannually,
|
||||
redoCurLines,
|
||||
])
|
||||
|
||||
const disableRedo = () => {
|
||||
if (isProcessing) {
|
||||
@ -1410,12 +1396,13 @@ export default function Editor(props: EditorProps) {
|
||||
let s = minScale
|
||||
if (viewportRef.current?.state?.scale !== undefined) {
|
||||
s = viewportRef.current?.state.scale
|
||||
console.log("!!!!!!")
|
||||
}
|
||||
return s!
|
||||
}
|
||||
|
||||
const getBrushStyle = (_x: number, _y: number) => {
|
||||
const curScale = getCurScale()
|
||||
const curScale = scale
|
||||
return {
|
||||
width: `${brushSize * curScale}px`,
|
||||
height: `${brushSize * curScale}px`,
|
||||
@ -1463,7 +1450,12 @@ export default function Editor(props: EditorProps) {
|
||||
const renderCanvas = () => {
|
||||
return (
|
||||
<TransformWrapper
|
||||
ref={viewportRef}
|
||||
// ref={viewportRef}
|
||||
ref={(r) => {
|
||||
if (r) {
|
||||
viewportRef.current = r
|
||||
}
|
||||
}}
|
||||
panning={{ disabled: !isPanning, velocityDisabled: true }}
|
||||
wheel={{ step: 0.05 }}
|
||||
centerZoomedOut
|
||||
@ -1546,14 +1538,15 @@ export default function Editor(props: EditorProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <Croper
|
||||
<Cropper
|
||||
maxHeight={imageHeight}
|
||||
maxWidth={imageWidth}
|
||||
minHeight={Math.min(256, imageHeight)}
|
||||
minWidth={Math.min(256, imageWidth)}
|
||||
scale={scale}
|
||||
show={isDiffusionModels && settings.showCroper}
|
||||
/> */}
|
||||
show={true}
|
||||
// show={isDiffusionModels && settings.showCroper}
|
||||
/>
|
||||
|
||||
{/* {isInteractiveSeg ? <InteractiveSeg /> : <></>} */}
|
||||
</TransformComponent>
|
||||
|
@ -197,11 +197,12 @@ export default function FileManager(props: Props) {
|
||||
onClick={() => {
|
||||
setFileManagerLayout("masonry")
|
||||
}}
|
||||
>
|
||||
<ViewGridIcon
|
||||
className={
|
||||
fileManagerState.layout !== "masonry" ? "opacity-50" : ""
|
||||
}
|
||||
>
|
||||
<ViewGridIcon />
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,6 @@ import { useToggle } from "@uidotdev/usehooks"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
|
@ -11,6 +11,13 @@ type FileManagerState = {
|
||||
searchText: string
|
||||
}
|
||||
|
||||
type CropperState = {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
file: File | null
|
||||
imageHeight: number
|
||||
@ -26,6 +33,7 @@ type AppState = {
|
||||
prompt: string
|
||||
|
||||
fileManagerState: FileManagerState
|
||||
cropperState: CropperState
|
||||
}
|
||||
|
||||
type AppAction = {
|
||||
@ -33,13 +41,19 @@ type AppAction = {
|
||||
setIsInpainting: (newValue: boolean) => void
|
||||
setBrushSize: (newValue: number) => void
|
||||
setImageSize: (width: number, height: number) => void
|
||||
setPrompt: (newValue: string) => void
|
||||
|
||||
setFileManagerSortBy: (newValue: SortBy) => void
|
||||
setFileManagerSortOrder: (newValue: SortOrder) => void
|
||||
setFileManagerLayout: (
|
||||
newValue: AppState["fileManagerState"]["layout"]
|
||||
) => void
|
||||
setFileManagerSearchText: (newValue: string) => void
|
||||
setPrompt: (newValue: string) => void
|
||||
|
||||
setCropperX: (newValue: number) => void
|
||||
setCropperY: (newValue: number) => void
|
||||
setCropperWidth: (newValue: number) => void
|
||||
setCropperHeight: (newValue: number) => void
|
||||
}
|
||||
|
||||
export const useStore = create<AppState & AppAction>()(
|
||||
@ -56,6 +70,12 @@ export const useStore = create<AppState & AppAction>()(
|
||||
isInteractiveSegRunning: false,
|
||||
interactiveSegClicks: [],
|
||||
prompt: "",
|
||||
cropperState: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
fileManagerState: {
|
||||
sortBy: SortBy.CTIME,
|
||||
sortOrder: SortOrder.DESCENDING,
|
||||
@ -66,15 +86,18 @@ export const useStore = create<AppState & AppAction>()(
|
||||
set((state: AppState) => {
|
||||
state.isInpainting = newValue
|
||||
}),
|
||||
|
||||
setFile: (file: File) =>
|
||||
set((state: AppState) => {
|
||||
// TODO: 清空各种状态
|
||||
state.file = file
|
||||
}),
|
||||
|
||||
setBrushSize: (newValue: number) =>
|
||||
set((state: AppState) => {
|
||||
state.brushSize = newValue
|
||||
}),
|
||||
|
||||
setImageSize: (width: number, height: number) => {
|
||||
// 根据图片尺寸调整 brushSize 的 scale
|
||||
set((state: AppState) => {
|
||||
@ -83,22 +106,47 @@ export const useStore = create<AppState & AppAction>()(
|
||||
state.brushSizeScale = Math.max(Math.min(width, height), 512) / 512
|
||||
})
|
||||
},
|
||||
|
||||
setPrompt: (newValue: string) =>
|
||||
set((state: AppState) => {
|
||||
state.prompt = newValue
|
||||
}),
|
||||
|
||||
setCropperX: (newValue: number) =>
|
||||
set((state: AppState) => {
|
||||
state.cropperState.x = newValue
|
||||
}),
|
||||
|
||||
setCropperY: (newValue: number) =>
|
||||
set((state: AppState) => {
|
||||
state.cropperState.y = newValue
|
||||
}),
|
||||
|
||||
setCropperWidth: (newValue: number) =>
|
||||
set((state: AppState) => {
|
||||
state.cropperState.width = newValue
|
||||
}),
|
||||
|
||||
setCropperHeight: (newValue: number) =>
|
||||
set((state: AppState) => {
|
||||
state.cropperState.height = newValue
|
||||
}),
|
||||
|
||||
setFileManagerSortBy: (newValue: SortBy) =>
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState.sortBy = newValue
|
||||
}),
|
||||
|
||||
setFileManagerSortOrder: (newValue: SortOrder) =>
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState.sortOrder = newValue
|
||||
}),
|
||||
|
||||
setFileManagerLayout: (newValue: "rows" | "masonry") =>
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState.layout = newValue
|
||||
}),
|
||||
|
||||
setFileManagerSearchText: (newValue: string) =>
|
||||
set((state: AppState) => {
|
||||
state.fileManagerState.searchText = newValue
|
||||
|
Loading…
Reference in New Issue
Block a user