import { useStore } from "@/lib/states" import { ExtenderDirection } from "@/lib/types" import { cn } from "@/lib/utils" import React, { useEffect, useState } from "react" import { twMerge } from "tailwind-merge" 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 { scale: number minHeight: number minWidth: number show: boolean } const clamp = ( newPos: number, newLength: number, oldPos: number, minLength: number ) => { if (newLength < minLength) { if (newPos === oldPos) { return [newPos, minLength] } return [newPos + newLength - minLength, minLength] } return [newPos, newLength] } const Extender = (props: Props) => { const { minHeight, minWidth, scale, show } = props const [ isInpainting, imageHeight, imageWdith, isSD, { x, y, width, height }, setX, setY, setWidth, setHeight, extenderDirection, isResizing, setIsResizing, ] = useStore((state) => [ state.isInpainting, state.imageHeight, state.imageWidth, state.isSD(), state.extenderState, state.setExtenderX, state.setExtenderY, state.setExtenderWidth, state.setExtenderHeight, state.settings.extenderDirection, state.isCropperExtenderResizing, state.setIsCropperExtenderResizing, ]) const [evData, setEVData] = useState({ 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, minWidth) } const clampTopBottom = (newY: number, newHeight: number) => { return clamp(newY, newHeight, y, minHeight) } 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 let clampedY = newY let clampedHeight = newHeight if (extenderDirection === ExtenderDirection.xy) { 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 let [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight) if (extenderDirection === ExtenderDirection.xy) { if (clampedHeight < Math.abs(clampedY) + imageHeight) { clampedHeight = Math.abs(clampedY) + imageHeight } } setHeight(clampedHeight) setY(clampedY) } const moveLeft = () => { const newWidth = evData.initWidth - offsetX const newX = evData.initX + offsetX let clampedX = newX let clampedWidth = newWidth if (extenderDirection === ExtenderDirection.xy) { 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 let [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth) if (extenderDirection === ExtenderDirection.xy) { if (clampedWidth < Math.abs(clampedX) + imageWdith) { clampedWidth = Math.abs(clampedX) + imageWdith } } 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 } } } const onPointerDone = () => { if (isResizing) { setIsResizing(false) } } useEffect(() => { if (isResizing) { 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, width, height, evData]) const onCropPointerDown = (e: React.PointerEvent) => { 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 createDragHandle = (cursor: string, side1: string, side2: string) => { const sideLength = 12 const halfSideLength = sideLength / 2 const draghandleCls = `w-[${sideLength}px] h-[${sideLength}px] z-[4] absolute content-[''] block border-2 border-primary borde pointer-events-auto hover:bg-primary` let xTrans = "0" let yTrans = "0" let side2Key = side2 let side2Val = `${-halfSideLength}px` if (side2 === "") { side2Val = "50%" if (side1 === "left" || side1 === "right") { side2Key = "top" yTrans = "-50%" } else { side2Key = "left" xTrans = "-50%" } } return (
) } const createCropSelection = () => { return (
{[ExtenderDirection.y, ExtenderDirection.xy].includes( extenderDirection ) ? ( <>
{createDragHandle("cursor-ns-resize", "top", "")} {createDragHandle("cursor-ns-resize", "bottom", "")} ) : ( <> )} {[ExtenderDirection.x, ExtenderDirection.xy].includes( extenderDirection ) ? ( <>
{createDragHandle("cursor-ew-resize", "left", "")} {createDragHandle("cursor-ew-resize", "right", "")} ) : ( <> )} {extenderDirection === ExtenderDirection.xy ? ( <> {createDragHandle("cursor-nw-resize", "top", "left")} {createDragHandle("cursor-ne-resize", "top", "right")} {createDragHandle("cursor-sw-resize", "bottom", "left")} {createDragHandle("cursor-se-resize", "bottom", "right")} ) : ( <> )}
) } const onInfoBarPointerDown = (e: React.PointerEvent) => { setEVData({ initX: x, initY: y, initHeight: height, initWidth: width, startResizeX: e.clientX, startResizeY: e.clientY, ord: "", }) } const createInfoBar = () => { return (
{/* TODO: 移动的时候会显示 brush */} {width} x {height}
) } const createBorder = () => { return (
) } if (show === false || !isSD) { return null } return (
{createBorder()} {createInfoBar()} {createCropSelection()}
) } export default Extender