Files
metabuilder/hooks/workflow-editor/useWorkflowCanvas.ts
2026-03-09 22:30:41 +00:00

168 lines
3.9 KiB
TypeScript

/**
* useWorkflowCanvas Hook
* Manages canvas transform state: zoom, pan, viewport
*/
import { useState, useCallback, RefObject } from 'react';
export interface CanvasTransform {
zoom: number;
pan: { x: number; y: number };
}
export interface UseWorkflowCanvasReturn {
// State
zoom: number;
pan: { x: number; y: number };
isPanning: boolean;
// Zoom controls
zoomIn: () => void;
zoomOut: () => void;
resetZoom: () => void;
setZoom: (zoom: number) => void;
// Pan controls
setPan: (x: number, y: number) => void;
panBy: (dx: number, dy: number) => void;
resetPan: () => void;
// Pan interaction handlers
startPan: (clientX: number, clientY: number) => void;
updatePan: (clientX: number, clientY: number) => void;
endPan: () => void;
// Combined reset
resetView: () => void;
// Screen/canvas coordinate conversion
screenToCanvas: (screenX: number, screenY: number, canvasRect: DOMRect) => { x: number; y: number };
canvasToScreen: (canvasX: number, canvasY: number, canvasRect: DOMRect) => { x: number; y: number };
}
export interface UseWorkflowCanvasOptions {
initialZoom?: number;
initialPan?: { x: number; y: number };
minZoom?: number;
maxZoom?: number;
zoomStep?: number;
}
export function useWorkflowCanvas(options: UseWorkflowCanvasOptions = {}): UseWorkflowCanvasReturn {
const {
initialZoom = 1,
initialPan = { x: 0, y: 0 },
minZoom = 0.25,
maxZoom = 2,
zoomStep = 0.25,
} = options;
const [zoom, setZoomState] = useState(initialZoom);
const [pan, setPanState] = useState(initialPan);
const [isPanning, setIsPanning] = useState(false);
const [panStart, setPanStart] = useState({ x: 0, y: 0 });
// Clamp zoom to valid range
const clampZoom = useCallback(
(value: number) => Math.min(maxZoom, Math.max(minZoom, value)),
[minZoom, maxZoom]
);
// Zoom controls
const zoomIn = useCallback(() => {
setZoomState((prev) => clampZoom(prev + zoomStep));
}, [clampZoom, zoomStep]);
const zoomOut = useCallback(() => {
setZoomState((prev) => clampZoom(prev - zoomStep));
}, [clampZoom, zoomStep]);
const resetZoom = useCallback(() => {
setZoomState(1);
}, []);
const setZoom = useCallback(
(value: number) => {
setZoomState(clampZoom(value));
},
[clampZoom]
);
// Pan controls
const setPan = useCallback((x: number, y: number) => {
setPanState({ x, y });
}, []);
const panBy = useCallback((dx: number, dy: number) => {
setPanState((prev) => ({ x: prev.x + dx, y: prev.y + dy }));
}, []);
const resetPan = useCallback(() => {
setPanState({ x: 0, y: 0 });
}, []);
// Pan interaction handlers
const startPan = useCallback(
(clientX: number, clientY: number) => {
setIsPanning(true);
setPanStart({ x: clientX - pan.x, y: clientY - pan.y });
},
[pan]
);
const updatePan = useCallback(
(clientX: number, clientY: number) => {
if (isPanning) {
setPanState({ x: clientX - panStart.x, y: clientY - panStart.y });
}
},
[isPanning, panStart]
);
const endPan = useCallback(() => {
setIsPanning(false);
}, []);
// Combined reset
const resetView = useCallback(() => {
setZoomState(1);
setPanState({ x: 0, y: 0 });
}, []);
// Coordinate conversion
const screenToCanvas = useCallback(
(screenX: number, screenY: number, canvasRect: DOMRect) => ({
x: (screenX - canvasRect.left - pan.x) / zoom,
y: (screenY - canvasRect.top - pan.y) / zoom,
}),
[pan, zoom]
);
const canvasToScreen = useCallback(
(canvasX: number, canvasY: number, canvasRect: DOMRect) => ({
x: canvasX * zoom + pan.x + canvasRect.left,
y: canvasY * zoom + pan.y + canvasRect.top,
}),
[pan, zoom]
);
return {
zoom,
pan,
isPanning,
zoomIn,
zoomOut,
resetZoom,
setZoom,
setPan,
panBy,
resetPan,
startPan,
updatePan,
endPan,
resetView,
screenToCanvas,
canvasToScreen,
};
}