import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react"; import { Button, InputNumber, Modal, Tooltip } from "antd"; import { Grid2x2, ListRestart, PanelTop, Redo2, Rows3, Trash2, Undo2, ZoomIn, ZoomOut } from "lucide-react"; import { useTranslation } from "react-i18next"; import { readImageMeta } from "@/lib/image-utils"; import type { ImageSplitParams } from "@/lib/canvas/canvas-image-data"; import { useImageEditorViewport } from "@/components/canvas/use-image-editor-viewport"; export type CanvasImageSplitParams = ImageSplitParams; const defaultParams: CanvasImageSplitParams = { rows: 2, columns: 2, horizontalLines: [0.5], verticalLines: [0.5] }; const maxGridSize = 12; type ActiveLine = { axis: "horizontal" | "vertical"; index: number } | null; export function CanvasNodeSplitDialog({ dataUrl, open, onClose, onConfirm }: { dataUrl: string; open: boolean; onClose: () => void; onConfirm: (params: CanvasImageSplitParams) => void }) { const { t } = useTranslation(); const [params, setParams] = useState(defaultParams); const [image, setImage] = useState<{ width: number; height: number } | null>(null); const [active, setActive] = useState(null); const historyRef = useRef([]); const redoRef = useRef([]); const dragAbortRef = useRef(null); const [historySize, setHistorySize] = useState(0); const [redoSize, setRedoSize] = useState(0); const viewport = useImageEditorViewport(image, open); const previewRef = viewport.stageRef; const horizontalLines = params.horizontalLines || []; const verticalLines = params.verticalLines || []; const rows = horizontalLines.length + 1; const columns = verticalLines.length + 1; const total = rows * columns; const pieceSize = image ? { width: Math.max(1, Math.floor(image.width / columns)), height: Math.max(1, Math.floor(image.height / rows)) } : null; useEffect(() => { if (!open) return; setParams(defaultParams); setActive(null); setImage(null); historyRef.current = []; redoRef.current = []; setHistorySize(0); setRedoSize(0); }, [dataUrl, open]); useEffect(() => { if (!open) return; void readImageMeta(dataUrl).then(setImage); }, [dataUrl, open]); useEffect(() => { if (!open) dragAbortRef.current?.abort(); return () => dragAbortRef.current?.abort(); }, [open]); const update = (key: "rows" | "columns", value: string | number | null) => { const count = clampGrid(value ?? params[key]); pushHistory(historyRef, redoRef, params, setHistorySize, setRedoSize); setActive(null); setParams((current) => ({ ...current, [key]: count, [key === "rows" ? "horizontalLines" : "verticalLines"]: buildGridLines(count) })); }; const addLine = (axis: "horizontal" | "vertical") => { pushHistory(historyRef, redoRef, params, setHistorySize, setRedoSize); const key = axis === "horizontal" ? "horizontalLines" : "verticalLines"; const spot = findLineSpot(params[key] || []); const lines = [...(params[key] || []), spot].sort((a, b) => a - b); setActive({ axis, index: lines.indexOf(spot) }); setParams({ ...params, [key]: lines, rows: axis === "horizontal" ? lines.length + 1 : params.rows, columns: axis === "vertical" ? lines.length + 1 : params.columns }); }; const deleteLine = () => { if (!active) return; pushHistory(historyRef, redoRef, params, setHistorySize, setRedoSize); setParams((current) => { const key = active.axis === "horizontal" ? "horizontalLines" : "verticalLines"; const lines = (current[key] || []).filter((_, index) => index !== active.index); return { ...current, [key]: lines, rows: active.axis === "horizontal" ? lines.length + 1 : current.rows, columns: active.axis === "vertical" ? lines.length + 1 : current.columns }; }); setActive(null); }; const startDrag = (axis: "horizontal" | "vertical", index: number, event: ReactPointerEvent) => { event.preventDefault(); event.stopPropagation(); setActive({ axis, index }); const box = previewRef.current?.getBoundingClientRect(); if (!box) return; pushHistory(historyRef, redoRef, params, setHistorySize, setRedoSize); dragAbortRef.current?.abort(); const controller = new AbortController(); dragAbortRef.current = controller; const move = (moveEvent: PointerEvent) => setLine(axis, index, axis === "horizontal" ? (moveEvent.clientY - box.top) / box.height : (moveEvent.clientX - box.left) / box.width); const stop = () => controller.abort(); window.addEventListener("pointermove", move, { signal: controller.signal }); window.addEventListener("pointerup", stop, { signal: controller.signal }); window.addEventListener("pointercancel", stop, { signal: controller.signal }); }; const setLine = (axis: "horizontal" | "vertical", index: number, value: number) => { setParams((current) => { const key = axis === "horizontal" ? "horizontalLines" : "verticalLines"; const lines = [...(current[key] || [])]; lines[index] = clampLine(value, lines[index - 1] ?? 0, lines[index + 1] ?? 1); return { ...current, [key]: lines }; }); }; const resetLines = () => { pushHistory(historyRef, redoRef, params, setHistorySize, setRedoSize); setActive(null); setParams((current) => ({ ...current, horizontalLines: buildGridLines(current.rows), verticalLines: buildGridLines(current.columns) })); }; const undoSplit = useCallback(() => { const previous = historyRef.current.pop(); if (!previous) return; redoRef.current.push(cloneSplitParams(params)); setParams(previous); setActive(null); setHistorySize(historyRef.current.length); setRedoSize(redoRef.current.length); }, [params]); const redoSplit = useCallback(() => { const next = redoRef.current.pop(); if (!next) return; historyRef.current.push(cloneSplitParams(params)); setParams(next); setActive(null); setHistorySize(historyRef.current.length); setRedoSize(redoRef.current.length); }, [params]); useEffect(() => { if (!open) return; const handleKeyDown = (event: KeyboardEvent) => { const target = event.target instanceof Element ? event.target : null; if (target?.closest("input,textarea,[contenteditable='true']")) return; const key = event.key.toLowerCase(); const isUndo = (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && key === "z"; const isRedo = (event.metaKey || event.ctrlKey) && !event.altKey && ((event.shiftKey && key === "z") || (!event.shiftKey && key === "y")); const isDelete = event.key === "Delete" || event.key === "Backspace"; if (!isUndo && !isRedo && !isDelete) return; event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); if (isDelete) deleteLine(); else if (isRedo) redoSplit(); else undoSplit(); }; window.addEventListener("keydown", handleKeyDown, true); return () => window.removeEventListener("keydown", handleKeyDown, true); }, [active, open, params, redoSplit, undoSplit]); const confirmParams = { ...params, horizontalLines, verticalLines, rows, columns }; return (

{t("canvas.editors.splitTitle")}

{t("canvas.editors.splitDescription", { count: total })}

{t("canvas.editors.splitHint")}

{image ? `${image.width} x ${image.height} px` : t("canvas.editors.loading")}
update("rows", value)} /> update("columns", value)} />
{t("canvas.editors.pieceCount")} {t("canvas.editors.pieces", { count: total })}
{t("canvas.editors.averageSize")} {pieceSize ? `${pieceSize.width} x ${pieceSize.height}` : t("canvas.editors.unknown")}
); } function NumberField({ label, value, onChange }: { label: string; value: number; onChange: (value: string | number | null) => void }) { return ( ); } function SplitGrid({ horizontalLines, verticalLines, active, onPointerDown }: { horizontalLines: number[]; verticalLines: number[]; active: ActiveLine; onPointerDown: (axis: "horizontal" | "vertical", index: number, event: ReactPointerEvent) => void }) { return (
{verticalLines.map((line, index) => (
onPointerDown("vertical", index, event)}>
))} {horizontalLines.map((line, index) => (
onPointerDown("horizontal", index, event)}>
))}
); } function buildGridLines(count: number) { return Array.from({ length: Math.max(1, count) - 1 }, (_, index) => (index + 1) / count); } function findLineSpot(lines: number[]) { const cuts = [0, ...lines, 1].sort((a, b) => a - b); let spot = 0.5; let max = 0; for (let index = 0; index < cuts.length - 1; index += 1) { const gap = cuts[index + 1] - cuts[index]; if (gap > max) { max = gap; spot = cuts[index] + gap / 2; } } return spot; } function clampLine(value: number, min: number, max: number) { return Math.min(max - 0.01, Math.max(min + 0.01, value)); } function clampGrid(value: string | number) { const numberValue = Number(value); return Math.min(maxGridSize, Math.max(1, Math.round(Number.isFinite(numberValue) ? numberValue : 1))); } function cloneSplitParams(params: CanvasImageSplitParams) { return { ...params, horizontalLines: [...(params.horizontalLines || [])], verticalLines: [...(params.verticalLines || [])], }; } function pushHistory(historyRef: { current: CanvasImageSplitParams[] }, redoRef: { current: CanvasImageSplitParams[] }, params: CanvasImageSplitParams, setHistorySize: (size: number) => void, setRedoSize: (size: number) => void) { historyRef.current.push(cloneSplitParams(params)); if (historyRef.current.length > 50) historyRef.current.shift(); redoRef.current = []; setHistorySize(historyRef.current.length); setRedoSize(0); }