|
|
|
|
@@ -13,15 +13,40 @@ interface CropBoxOverlayProps {
|
|
|
|
|
bLocked: boolean;
|
|
|
|
|
nodeId: string;
|
|
|
|
|
onWidgetChange: (nodeId: string, name: string, value: unknown) => void;
|
|
|
|
|
square?: boolean;
|
|
|
|
|
xreal?: number;
|
|
|
|
|
yreal?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function snapPhysicalSquare(
|
|
|
|
|
anchorX: number, anchorY: number,
|
|
|
|
|
moverX: number, moverY: number,
|
|
|
|
|
xreal: number, yreal: number,
|
|
|
|
|
) {
|
|
|
|
|
const dx = moverX - anchorX;
|
|
|
|
|
const dy = moverY - anchorY;
|
|
|
|
|
const ax = xreal > 0 ? xreal : 1;
|
|
|
|
|
const ay = yreal > 0 ? yreal : 1;
|
|
|
|
|
const shortPhys = Math.min(Math.abs(dx) * ax, Math.abs(dy) * ay);
|
|
|
|
|
const sx = dx >= 0 ? 1 : -1;
|
|
|
|
|
const sy = dy >= 0 ? 1 : -1;
|
|
|
|
|
return {
|
|
|
|
|
x: anchorX + sx * (shortPhys / ax),
|
|
|
|
|
y: anchorY + sy * (shortPhys / ay),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function CropBoxOverlay({
|
|
|
|
|
image, x1, y1, x2, y2,
|
|
|
|
|
aLocked, bLocked,
|
|
|
|
|
nodeId, onWidgetChange,
|
|
|
|
|
square = false,
|
|
|
|
|
xreal = 1,
|
|
|
|
|
yreal = 1,
|
|
|
|
|
}: CropBoxOverlayProps) {
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const [dragging, setDragging] = useState<string | null>(null);
|
|
|
|
|
const panStartRef = useRef<{ fx: number; fy: number; x1: number; y1: number; x2: number; y2: number } | null>(null);
|
|
|
|
|
|
|
|
|
|
const getCoords = useCallback((e: React.PointerEvent<Element>) => {
|
|
|
|
|
return pointerToFraction(e, containerRef.current!);
|
|
|
|
|
@@ -30,28 +55,66 @@ export default function CropBoxOverlay({
|
|
|
|
|
const onPointerDown = useCallback((point: string) => (e: React.PointerEvent<Element>) => {
|
|
|
|
|
if (point === 'p1' && aLocked) return;
|
|
|
|
|
if (point === 'p2' && bLocked) return;
|
|
|
|
|
if (point === 'rect' && (aLocked || bLocked)) return;
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
|
|
|
if (point === 'rect') {
|
|
|
|
|
const { fx, fy } = getCoords(e);
|
|
|
|
|
panStartRef.current = { fx, fy, x1, y1, x2, y2 };
|
|
|
|
|
}
|
|
|
|
|
setDragging(point);
|
|
|
|
|
}, [aLocked, bLocked]);
|
|
|
|
|
}, [aLocked, bLocked, getCoords, x1, y1, x2, y2]);
|
|
|
|
|
|
|
|
|
|
const onPointerMove = useCallback((e: React.PointerEvent<Element>) => {
|
|
|
|
|
if (!dragging || !containerRef.current) return;
|
|
|
|
|
const { fx, fy } = getCoords(e);
|
|
|
|
|
const vx = parseFloat(fx.toFixed(3));
|
|
|
|
|
const vy = parseFloat(fy.toFixed(3));
|
|
|
|
|
if (dragging === 'p1') {
|
|
|
|
|
onWidgetChange(nodeId, 'x1', vx);
|
|
|
|
|
onWidgetChange(nodeId, 'y1', vy);
|
|
|
|
|
} else {
|
|
|
|
|
onWidgetChange(nodeId, 'x2', vx);
|
|
|
|
|
onWidgetChange(nodeId, 'y2', vy);
|
|
|
|
|
|
|
|
|
|
if (dragging === 'rect') {
|
|
|
|
|
const start = panStartRef.current;
|
|
|
|
|
if (!start) return;
|
|
|
|
|
const left = Math.min(start.x1, start.x2);
|
|
|
|
|
const right = Math.max(start.x1, start.x2);
|
|
|
|
|
const top = Math.min(start.y1, start.y2);
|
|
|
|
|
const bottom = Math.max(start.y1, start.y2);
|
|
|
|
|
let dx = fx - start.fx;
|
|
|
|
|
let dy = fy - start.fy;
|
|
|
|
|
dx = Math.max(-left, Math.min(1 - right, dx));
|
|
|
|
|
dy = Math.max(-top, Math.min(1 - bottom, dy));
|
|
|
|
|
const nx1 = parseFloat((start.x1 + dx).toFixed(3));
|
|
|
|
|
const ny1 = parseFloat((start.y1 + dy).toFixed(3));
|
|
|
|
|
const nx2 = parseFloat((start.x2 + dx).toFixed(3));
|
|
|
|
|
const ny2 = parseFloat((start.y2 + dy).toFixed(3));
|
|
|
|
|
onWidgetChange(nodeId, 'x1', nx1);
|
|
|
|
|
onWidgetChange(nodeId, 'y1', ny1);
|
|
|
|
|
onWidgetChange(nodeId, 'x2', nx2);
|
|
|
|
|
onWidgetChange(nodeId, 'y2', ny2);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}, [dragging, getCoords, nodeId, onWidgetChange]);
|
|
|
|
|
|
|
|
|
|
let vx = fx;
|
|
|
|
|
let vy = fy;
|
|
|
|
|
if (square) {
|
|
|
|
|
const anchorX = dragging === 'p2' ? x1 : x2;
|
|
|
|
|
const anchorY = dragging === 'p2' ? y1 : y2;
|
|
|
|
|
const snapped = snapPhysicalSquare(anchorX, anchorY, fx, fy, xreal, yreal);
|
|
|
|
|
vx = snapped.x;
|
|
|
|
|
vy = snapped.y;
|
|
|
|
|
}
|
|
|
|
|
const vxR = parseFloat(vx.toFixed(3));
|
|
|
|
|
const vyR = parseFloat(vy.toFixed(3));
|
|
|
|
|
if (dragging === 'p1') {
|
|
|
|
|
onWidgetChange(nodeId, 'x1', vxR);
|
|
|
|
|
onWidgetChange(nodeId, 'y1', vyR);
|
|
|
|
|
} else {
|
|
|
|
|
onWidgetChange(nodeId, 'x2', vxR);
|
|
|
|
|
onWidgetChange(nodeId, 'y2', vyR);
|
|
|
|
|
}
|
|
|
|
|
}, [dragging, getCoords, nodeId, onWidgetChange, square, xreal, yreal, x1, y1, x2, y2]);
|
|
|
|
|
|
|
|
|
|
const onPointerUp = useCallback(() => {
|
|
|
|
|
setDragging(null);
|
|
|
|
|
panStartRef.current = null;
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const left = Math.min(x1, x2);
|
|
|
|
|
@@ -75,13 +138,14 @@ export default function CropBoxOverlay({
|
|
|
|
|
<div className="crop-dim" style={{ left: 0, top: `${bottom * 100}%`, width: '100%', height: `${(1 - bottom) * 100}%` }} />
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className="crop-rect"
|
|
|
|
|
className={`crop-rect ${aLocked || bLocked ? 'crop-rect-locked' : ''}`}
|
|
|
|
|
style={{
|
|
|
|
|
left: `${left * 100}%`,
|
|
|
|
|
top: `${top * 100}%`,
|
|
|
|
|
width: `${(right - left) * 100}%`,
|
|
|
|
|
height: `${(bottom - top) * 100}%`,
|
|
|
|
|
}}
|
|
|
|
|
onPointerDown={onPointerDown('rect')}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|