♻️ refactor!: massive refactor

This commit is contained in:
2026-05-31 03:04:30 +07:00
parent c1f12c7201
commit df3c944547
86 changed files with 3691 additions and 1129 deletions
+130 -36
View File
@@ -23,7 +23,13 @@ type UseCanvasInteractionsOptions = {
onInteractionEnd?: () => void;
onContextMenu?: (x: number, y: number) => void;
onFillLayer?: (layerId: string, x: number, y: number) => void;
onBrushStrokeStart?: (layerId: string, x: number, y: number, layerWidth: number, layerHeight: number) => void;
onBrushStrokeStart?: (
layerId: string,
x: number,
y: number,
layerWidth: number,
layerHeight: number,
) => void;
onBrushStrokeMove?: (layerId: string, x: number, y: number) => void;
onBrushStrokeEnd?: (layerId: string) => void;
};
@@ -53,7 +59,11 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
onBrushStrokeMove,
onBrushStrokeEnd,
} = options;
const [viewport, setViewport] = React.useState<Viewport>({ x: 0, y: 0, scale: 1 });
const [viewport, setViewport] = React.useState<Viewport>({
x: 0,
y: 0,
scale: 1,
});
const containerRef = React.useRef<HTMLDivElement>(null);
const isPanning = React.useRef(false);
const lastPos = React.useRef({ x: 0, y: 0 });
@@ -64,7 +74,12 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
const interactionActiveRef = React.useRef(false);
const dragMoveRafRef = React.useRef<number | null>(null);
const resizeMoveRafRef = React.useRef<number | null>(null);
const resizeMovePendingRef = React.useRef<{ width: number; height: number; x: number; y: number } | null>(null);
const resizeMovePendingRef = React.useRef<{
width: number;
height: number;
x: number;
y: number;
} | null>(null);
const dragRef = React.useRef<{
id: string;
startLayerX: number;
@@ -95,7 +110,12 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
startRotation: number;
lastRotation: number;
} | null>(null);
const brushRef = React.useRef<{ id: string; lastX: number; lastY: number; rect: DOMRect } | null>(null);
const brushRef = React.useRef<{
id: string;
lastX: number;
lastY: number;
rect: DOMRect;
} | null>(null);
const pinchRef = React.useRef<{
active: boolean;
@@ -119,7 +139,10 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
onInteractionEnd?.();
}
function clientDist(t0: Pick<Touch, "clientX" | "clientY">, t1: Pick<Touch, "clientX" | "clientY">) {
function clientDist(
t0: Pick<Touch, "clientX" | "clientY">,
t1: Pick<Touch, "clientX" | "clientY">,
) {
const dx = t0.clientX - t1.clientX;
const dy = t0.clientY - t1.clientY;
return Math.sqrt(dx * dx + dy * dy);
@@ -130,12 +153,16 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
e.stopPropagation();
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;
const factor = e.ctrlKey || e.metaKey ? 1 - e.deltaY * 0.01 : 1 - e.deltaY * ZOOM_FACTOR;
const factor =
e.ctrlKey || e.metaKey ? 1 - e.deltaY * 0.01 : 1 - e.deltaY * ZOOM_FACTOR;
if (!Number.isFinite(factor) || factor === 0) return;
const pivotX = e.clientX - rect.left;
const pivotY = e.clientY - rect.top;
setViewport((vp) => {
const nextScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, vp.scale * factor));
const nextScale = Math.max(
MIN_SCALE,
Math.min(MAX_SCALE, vp.scale * factor),
);
const scaleChange = nextScale / vp.scale;
return {
x: pivotX - (pivotX - vp.x) * scaleChange,
@@ -147,7 +174,10 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
function zoomBy(factor: number, pivotX: number, pivotY: number) {
setViewport((vp) => {
const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, vp.scale * factor));
const newScale = Math.max(
MIN_SCALE,
Math.min(MAX_SCALE, vp.scale * factor),
);
const scaleChange = newScale / vp.scale;
return {
x: pivotX - (pivotX - vp.x) * scaleChange,
@@ -193,13 +223,21 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
React.useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.code === "Space") {
if (!(e.target instanceof HTMLElement) || /^(input|textarea|select)$/i.test(e.target.tagName)) return;
if (
!(e.target instanceof HTMLElement) ||
/^(input|textarea|select)$/i.test(e.target.tagName)
)
return;
if (!isSpacePan) setIsSpacePan(true);
e.preventDefault();
return;
}
if (e.key === "Shift") {
if (!(e.target instanceof HTMLElement) || /^(input|textarea|select)$/i.test(e.target.tagName)) return;
if (
!(e.target instanceof HTMLElement) ||
/^(input|textarea|select)$/i.test(e.target.tagName)
)
return;
setIsShiftPressed(true);
return;
}
@@ -231,11 +269,16 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
}
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
window.addEventListener("wheel", handleWheelCaptured, { capture: true, passive: false });
window.addEventListener("wheel", handleWheelCaptured, {
capture: true,
passive: false,
});
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
window.removeEventListener("wheel", handleWheelCaptured, { capture: true });
window.removeEventListener("wheel", handleWheelCaptured, {
capture: true,
});
};
}, [isSpacePan]);
@@ -281,7 +324,8 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
}
const toolDef = getToolDefinition(tool);
if (toolDef?.allowsLayerRotate && rotateRef.current && onRotateLayer) {
const { centerX, centerY, startAngle, startRotation, id } = rotateRef.current;
const { centerX, centerY, startAngle, startRotation, id } =
rotateRef.current;
const currentAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
const delta = currentAngle - startAngle;
const nextRotation = startRotation + (delta * 180) / Math.PI;
@@ -312,7 +356,13 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
nextHeight = Math.max(8, resizeRef.current.startHeight - dy);
}
if (!e.shiftKey && (corner === "br" || corner === "bl" || corner === "tr" || corner === "tl")) {
if (
!e.shiftKey &&
(corner === "br" ||
corner === "bl" ||
corner === "tr" ||
corner === "tl")
) {
const aspect = resizeRef.current.aspect || 1;
if (Math.abs(dx) > Math.abs(dy)) {
nextHeight = Math.max(8, nextWidth / aspect);
@@ -321,8 +371,10 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
}
}
if (corner === "bl" || corner === "tl") offsetX = resizeRef.current.startWidth - nextWidth;
if (corner === "tr" || corner === "tl") offsetY = resizeRef.current.startHeight - nextHeight;
if (corner === "bl" || corner === "tl")
offsetX = resizeRef.current.startWidth - nextWidth;
if (corner === "tr" || corner === "tl")
offsetY = resizeRef.current.startHeight - nextHeight;
resizeRef.current.lastWidth = nextWidth;
resizeRef.current.lastHeight = nextHeight;
@@ -338,7 +390,11 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
if (!resizeRef.current || !resizeMovePendingRef.current) return;
const pending = resizeMovePendingRef.current;
onResizeLayer(resizeRef.current.id, pending.width, pending.height);
if ((pending.x !== resizeRef.current.startLayerX || pending.y !== resizeRef.current.startLayerY) && onMoveLayer) {
if (
(pending.x !== resizeRef.current.startLayerX ||
pending.y !== resizeRef.current.startLayerY) &&
onMoveLayer
) {
onMoveLayer(resizeRef.current.id, pending.x, pending.y);
}
});
@@ -349,14 +405,19 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
const dy = (e.clientY - dragRef.current.startEventY) / viewport.scale;
const nextX = dragRef.current.startLayerX + dx;
const nextY = dragRef.current.startLayerY + dy;
if (dragRef.current.lastX === nextX && dragRef.current.lastY === nextY) return;
if (dragRef.current.lastX === nextX && dragRef.current.lastY === nextY)
return;
dragRef.current.lastX = nextX;
dragRef.current.lastY = nextY;
if (dragMoveRafRef.current !== null) return;
dragMoveRafRef.current = window.requestAnimationFrame(() => {
dragMoveRafRef.current = null;
if (!dragRef.current) return;
onMoveLayer(dragRef.current.id, dragRef.current.lastX, dragRef.current.lastY);
onMoveLayer(
dragRef.current.id,
dragRef.current.lastX,
dragRef.current.lastY,
);
});
}
}
@@ -372,7 +433,11 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
if (resizeRef.current && resizeMovePendingRef.current && onResizeLayer) {
const pending = resizeMovePendingRef.current;
onResizeLayer(resizeRef.current.id, pending.width, pending.height);
if ((pending.x !== resizeRef.current.startLayerX || pending.y !== resizeRef.current.startLayerY) && onMoveLayer) {
if (
(pending.x !== resizeRef.current.startLayerX ||
pending.y !== resizeRef.current.startLayerY) &&
onMoveLayer
) {
onMoveLayer(resizeRef.current.id, pending.x, pending.y);
}
}
@@ -381,11 +446,13 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
isMiddleMousePan.current = false;
if (dragRef.current && onMoveLayerEnd) {
const { id, lastX, lastY } = dragRef.current;
if (typeof lastX === "number" && typeof lastY === "number") onMoveLayerEnd(id, lastX, lastY);
if (typeof lastX === "number" && typeof lastY === "number")
onMoveLayerEnd(id, lastX, lastY);
}
if (resizeRef.current && onResizeLayerEnd) {
const { id, lastWidth, lastHeight } = resizeRef.current;
if (typeof lastWidth === "number" && typeof lastHeight === "number") onResizeLayerEnd(id, lastWidth, lastHeight);
if (typeof lastWidth === "number" && typeof lastHeight === "number")
onResizeLayerEnd(id, lastWidth, lastHeight);
}
if (rotateRef.current && onRotateLayerEnd) {
const { id, lastRotation } = rotateRef.current;
@@ -402,7 +469,10 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
endInteraction();
}
function onLayerPointerDown(e: React.PointerEvent<HTMLDivElement>, layer: Layer) {
function onLayerPointerDown(
e: React.PointerEvent<HTMLDivElement>,
layer: Layer,
) {
if (isSpacePan) return;
e.stopPropagation();
const toolDef = getToolDefinition(tool);
@@ -413,9 +483,14 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
onSelectLayer(layer.id);
if (tool === "brush" && onBrushStrokeStart) {
e.currentTarget.setPointerCapture(e.pointerId);
brushRef.current = { id: layer.id, lastX: localX, lastY: localY, rect: e.currentTarget.getBoundingClientRect() };
const lw = layer.width ?? Math.round(200 * layer.scale);
const lh = layer.height ?? Math.round(150 * layer.scale);
brushRef.current = {
id: layer.id,
lastX: localX,
lastY: localY,
rect: e.currentTarget.getBoundingClientRect(),
};
const lw = layer.width;
const lh = layer.height;
onBrushStrokeStart(layer.id, localX, localY, lw, lh);
beginInteraction();
} else if (onFillLayer) {
@@ -437,13 +512,17 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
onSelectLayer(layer.id);
}
function onResizeHandleDown(e: React.PointerEvent<HTMLButtonElement>, layer: Layer, corner: string) {
function onResizeHandleDown(
e: React.PointerEvent<HTMLButtonElement>,
layer: Layer,
corner: string,
) {
if (!getToolDefinition(tool)?.allowsLayerResize || !onResizeLayer) return;
e.stopPropagation();
e.currentTarget.setPointerCapture(e.pointerId);
rotateRef.current = null;
const width = layer.width ?? Math.round(200 * layer.scale);
const height = layer.height ?? Math.round(150 * layer.scale);
const width = layer.width;
const height = layer.height;
resizeRef.current = {
id: layer.id,
startWidth: width,
@@ -461,16 +540,27 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
onSelectLayer(layer.id);
}
function onRotateHandleDown(e: React.PointerEvent<HTMLButtonElement>, layer: Layer) {
function onRotateHandleDown(
e: React.PointerEvent<HTMLButtonElement>,
layer: Layer,
) {
if (!getToolDefinition(tool)?.allowsLayerRotate || !onRotateLayer) return;
e.stopPropagation();
e.currentTarget.setPointerCapture(e.pointerId);
resizeRef.current = null;
const rect = containerRef.current?.getBoundingClientRect();
const width = layer.width ?? Math.round(200 * layer.scale);
const height = layer.height ?? Math.round(150 * layer.scale);
const centerX = (rect?.left ?? 0) + viewport.x + layer.x * viewport.scale + (width * viewport.scale) / 2;
const centerY = (rect?.top ?? 0) + viewport.y + layer.y * viewport.scale + (height * viewport.scale) / 2;
const width = layer.width;
const height = layer.height;
const centerX =
(rect?.left ?? 0) +
viewport.x +
layer.x * viewport.scale +
(width * viewport.scale) / 2;
const centerY =
(rect?.top ?? 0) +
viewport.y +
layer.y * viewport.scale +
(height * viewport.scale) / 2;
const startAngle = Math.atan2(e.clientY - centerY, e.clientX - centerX);
rotateRef.current = {
id: layer.id,
@@ -488,7 +578,8 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
if (e.touches.length === 2) {
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
const midX =
(e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
pinchRef.current = {
active: true,
@@ -507,7 +598,10 @@ export function useCanvasInteractions(options: UseCanvasInteractionsOptions) {
e.preventDefault();
const px = clientDist(e.touches[0], e.touches[1]);
const ratio = px / pinchRef.current.initialPinchPx;
const nextScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, pinchRef.current.initialScale * ratio));
const nextScale = Math.max(
MIN_SCALE,
Math.min(MAX_SCALE, pinchRef.current.initialScale * ratio),
);
const scaleChange = nextScale / pinchRef.current.initialScale;
const { pivotX, pivotY, initialX, initialY } = pinchRef.current;
setViewport({