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",
|
"@heroicons/react": "^2.0.18",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@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-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",
|
||||||
@ -27,6 +28,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"flexsearch": "^0.7.21",
|
"flexsearch": "^0.7.21",
|
||||||
|
"immer": "^10.0.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.292.0",
|
"lucide-react": "^0.292.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
@ -39,7 +41,8 @@
|
|||||||
"react-zoom-pan-pinch": "^3.3.0",
|
"react-zoom-pan-pinch": "^3.3.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"zustand": "^4.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/flexsearch": "^0.7.3",
|
"@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": {
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-popover": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
|
||||||
@ -4023,6 +4095,15 @@
|
|||||||
"node": ">= 4"
|
"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": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -5770,6 +5859,33 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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",
|
"@heroicons/react": "^2.0.18",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@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-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",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"flexsearch": "^0.7.21",
|
"flexsearch": "^0.7.21",
|
||||||
|
"immer": "^10.0.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.292.0",
|
"lucide-react": "^0.292.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
@ -41,7 +43,8 @@
|
|||||||
"react-zoom-pan-pinch": "^3.3.0",
|
"react-zoom-pan-pinch": "^3.3.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"zustand": "^4.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/flexsearch": "^0.7.3",
|
"@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 { CursorArrowRaysIcon } from "@heroicons/react/24/outline"
|
||||||
import { useToast } from "@/components/ui/use-toast"
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
import {
|
import {
|
||||||
|
ReactZoomPanPinchContentRef,
|
||||||
ReactZoomPanPinchRef,
|
ReactZoomPanPinchRef,
|
||||||
TransformComponent,
|
TransformComponent,
|
||||||
TransformWrapper,
|
TransformWrapper,
|
||||||
@ -55,6 +56,8 @@ import { Slider } from "./ui/slider"
|
|||||||
import { PluginName } from "@/lib/types"
|
import { PluginName } from "@/lib/types"
|
||||||
import { useHotkeys } from "react-hotkeys-hook"
|
import { useHotkeys } from "react-hotkeys-hook"
|
||||||
import { useStore } from "@/lib/states"
|
import { useStore } from "@/lib/states"
|
||||||
|
import Cropper from "./Cropper"
|
||||||
|
import { HotkeysEvent } from "react-hotkeys-hook/dist/types"
|
||||||
|
|
||||||
const TOOLBAR_HEIGHT = 200
|
const TOOLBAR_HEIGHT = 200
|
||||||
const MIN_BRUSH_SIZE = 10
|
const MIN_BRUSH_SIZE = 10
|
||||||
@ -193,7 +196,7 @@ export default function Editor(props: EditorProps) {
|
|||||||
const windowSize = useWindowSize()
|
const windowSize = useWindowSize()
|
||||||
const windowCenterX = windowSize.width / 2
|
const windowCenterX = windowSize.width / 2
|
||||||
const windowCenterY = windowSize.height / 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
|
// Indicates that the image has been loaded and is centered on first load
|
||||||
const [initialCentered, setInitialCentered] = useState(false)
|
const [initialCentered, setInitialCentered] = useState(false)
|
||||||
|
|
||||||
@ -1119,9 +1122,8 @@ export default function Editor(props: EditorProps) {
|
|||||||
context,
|
context,
|
||||||
])
|
])
|
||||||
|
|
||||||
const undo = () => {
|
const undo = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
|
||||||
// TODO: prevent default event
|
keyboardEvent.preventDefault()
|
||||||
console.log("undo")
|
|
||||||
if (runMannually && curLineGroup.length !== 0) {
|
if (runMannually && curLineGroup.length !== 0) {
|
||||||
undoStroke()
|
undoStroke()
|
||||||
} else {
|
} else {
|
||||||
@ -1186,7 +1188,8 @@ export default function Editor(props: EditorProps) {
|
|||||||
// draw(newRenders[newRenders.length - 1], [])
|
// draw(newRenders[newRenders.length - 1], [])
|
||||||
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
|
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
|
||||||
|
|
||||||
const redo = () => {
|
const redo = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
|
||||||
|
keyboardEvent.preventDefault()
|
||||||
if (runMannually && redoCurLines.length !== 0) {
|
if (runMannually && redoCurLines.length !== 0) {
|
||||||
redoStroke()
|
redoStroke()
|
||||||
} else {
|
} else {
|
||||||
@ -1194,29 +1197,12 @@ export default function Editor(props: EditorProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Cmd+shift+Z
|
useHotkeys("shift+ctrl+z,shift+meta+z", redo, undefined, [
|
||||||
const redoPredicate = (event: KeyboardEvent) => {
|
redoStroke,
|
||||||
const isCmdZ =
|
redoRender,
|
||||||
(event.metaKey || event.ctrlKey) &&
|
runMannually,
|
||||||
event.shiftKey &&
|
redoCurLines,
|
||||||
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,
|
|
||||||
// ])
|
|
||||||
|
|
||||||
const disableRedo = () => {
|
const disableRedo = () => {
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
@ -1410,12 +1396,13 @@ export default function Editor(props: EditorProps) {
|
|||||||
let s = minScale
|
let s = minScale
|
||||||
if (viewportRef.current?.state?.scale !== undefined) {
|
if (viewportRef.current?.state?.scale !== undefined) {
|
||||||
s = viewportRef.current?.state.scale
|
s = viewportRef.current?.state.scale
|
||||||
|
console.log("!!!!!!")
|
||||||
}
|
}
|
||||||
return s!
|
return s!
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBrushStyle = (_x: number, _y: number) => {
|
const getBrushStyle = (_x: number, _y: number) => {
|
||||||
const curScale = getCurScale()
|
const curScale = scale
|
||||||
return {
|
return {
|
||||||
width: `${brushSize * curScale}px`,
|
width: `${brushSize * curScale}px`,
|
||||||
height: `${brushSize * curScale}px`,
|
height: `${brushSize * curScale}px`,
|
||||||
@ -1463,7 +1450,12 @@ export default function Editor(props: EditorProps) {
|
|||||||
const renderCanvas = () => {
|
const renderCanvas = () => {
|
||||||
return (
|
return (
|
||||||
<TransformWrapper
|
<TransformWrapper
|
||||||
ref={viewportRef}
|
// ref={viewportRef}
|
||||||
|
ref={(r) => {
|
||||||
|
if (r) {
|
||||||
|
viewportRef.current = r
|
||||||
|
}
|
||||||
|
}}
|
||||||
panning={{ disabled: !isPanning, velocityDisabled: true }}
|
panning={{ disabled: !isPanning, velocityDisabled: true }}
|
||||||
wheel={{ step: 0.05 }}
|
wheel={{ step: 0.05 }}
|
||||||
centerZoomedOut
|
centerZoomedOut
|
||||||
@ -1546,14 +1538,15 @@ export default function Editor(props: EditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <Croper
|
<Cropper
|
||||||
maxHeight={imageHeight}
|
maxHeight={imageHeight}
|
||||||
maxWidth={imageWidth}
|
maxWidth={imageWidth}
|
||||||
minHeight={Math.min(256, imageHeight)}
|
minHeight={Math.min(256, imageHeight)}
|
||||||
minWidth={Math.min(256, imageWidth)}
|
minWidth={Math.min(256, imageWidth)}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
show={isDiffusionModels && settings.showCroper}
|
show={true}
|
||||||
/> */}
|
// show={isDiffusionModels && settings.showCroper}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* {isInteractiveSeg ? <InteractiveSeg /> : <></>} */}
|
{/* {isInteractiveSeg ? <InteractiveSeg /> : <></>} */}
|
||||||
</TransformComponent>
|
</TransformComponent>
|
||||||
|
@ -197,11 +197,12 @@ export default function FileManager(props: Props) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFileManagerLayout("masonry")
|
setFileManagerLayout("masonry")
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<ViewGridIcon
|
||||||
className={
|
className={
|
||||||
fileManagerState.layout !== "masonry" ? "opacity-50" : ""
|
fileManagerState.layout !== "masonry" ? "opacity-50" : ""
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<ViewGridIcon />
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,6 @@ import { useToggle } from "@uidotdev/usehooks"
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
@ -11,6 +11,13 @@ type FileManagerState = {
|
|||||||
searchText: string
|
searchText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CropperState = {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
type AppState = {
|
type AppState = {
|
||||||
file: File | null
|
file: File | null
|
||||||
imageHeight: number
|
imageHeight: number
|
||||||
@ -26,6 +33,7 @@ type AppState = {
|
|||||||
prompt: string
|
prompt: string
|
||||||
|
|
||||||
fileManagerState: FileManagerState
|
fileManagerState: FileManagerState
|
||||||
|
cropperState: CropperState
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppAction = {
|
type AppAction = {
|
||||||
@ -33,13 +41,19 @@ type AppAction = {
|
|||||||
setIsInpainting: (newValue: boolean) => void
|
setIsInpainting: (newValue: boolean) => void
|
||||||
setBrushSize: (newValue: number) => void
|
setBrushSize: (newValue: number) => void
|
||||||
setImageSize: (width: number, height: number) => void
|
setImageSize: (width: number, height: number) => void
|
||||||
|
setPrompt: (newValue: string) => void
|
||||||
|
|
||||||
setFileManagerSortBy: (newValue: SortBy) => void
|
setFileManagerSortBy: (newValue: SortBy) => void
|
||||||
setFileManagerSortOrder: (newValue: SortOrder) => void
|
setFileManagerSortOrder: (newValue: SortOrder) => void
|
||||||
setFileManagerLayout: (
|
setFileManagerLayout: (
|
||||||
newValue: AppState["fileManagerState"]["layout"]
|
newValue: AppState["fileManagerState"]["layout"]
|
||||||
) => void
|
) => void
|
||||||
setFileManagerSearchText: (newValue: string) => 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>()(
|
export const useStore = create<AppState & AppAction>()(
|
||||||
@ -56,6 +70,12 @@ export const useStore = create<AppState & AppAction>()(
|
|||||||
isInteractiveSegRunning: false,
|
isInteractiveSegRunning: false,
|
||||||
interactiveSegClicks: [],
|
interactiveSegClicks: [],
|
||||||
prompt: "",
|
prompt: "",
|
||||||
|
cropperState: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
fileManagerState: {
|
fileManagerState: {
|
||||||
sortBy: SortBy.CTIME,
|
sortBy: SortBy.CTIME,
|
||||||
sortOrder: SortOrder.DESCENDING,
|
sortOrder: SortOrder.DESCENDING,
|
||||||
@ -66,15 +86,18 @@ export const useStore = create<AppState & AppAction>()(
|
|||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
state.isInpainting = newValue
|
state.isInpainting = newValue
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setFile: (file: File) =>
|
setFile: (file: File) =>
|
||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
// TODO: 清空各种状态
|
// TODO: 清空各种状态
|
||||||
state.file = file
|
state.file = file
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setBrushSize: (newValue: number) =>
|
setBrushSize: (newValue: number) =>
|
||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
state.brushSize = newValue
|
state.brushSize = newValue
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setImageSize: (width: number, height: number) => {
|
setImageSize: (width: number, height: number) => {
|
||||||
// 根据图片尺寸调整 brushSize 的 scale
|
// 根据图片尺寸调整 brushSize 的 scale
|
||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
@ -83,22 +106,47 @@ export const useStore = create<AppState & AppAction>()(
|
|||||||
state.brushSizeScale = Math.max(Math.min(width, height), 512) / 512
|
state.brushSizeScale = Math.max(Math.min(width, height), 512) / 512
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
setPrompt: (newValue: string) =>
|
setPrompt: (newValue: string) =>
|
||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
state.prompt = newValue
|
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) =>
|
setFileManagerSortBy: (newValue: SortBy) =>
|
||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
state.fileManagerState.sortBy = newValue
|
state.fileManagerState.sortBy = newValue
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setFileManagerSortOrder: (newValue: SortOrder) =>
|
setFileManagerSortOrder: (newValue: SortOrder) =>
|
||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
state.fileManagerState.sortOrder = newValue
|
state.fileManagerState.sortOrder = newValue
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setFileManagerLayout: (newValue: "rows" | "masonry") =>
|
setFileManagerLayout: (newValue: "rows" | "masonry") =>
|
||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
state.fileManagerState.layout = newValue
|
state.fileManagerState.layout = newValue
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setFileManagerSearchText: (newValue: string) =>
|
setFileManagerSearchText: (newValue: string) =>
|
||||||
set((state: AppState) => {
|
set((state: AppState) => {
|
||||||
state.fileManagerState.searchText = newValue
|
state.fileManagerState.searchText = newValue
|
||||||
|
Loading…
Reference in New Issue
Block a user