import { useEffect, useRef, useState } from "react"; import { Stage, Layer, Rect } from "react-konva"; import Konva from "konva"; import { useEditorStore } from "../store/editorStore"; import { useDrawingHandlers } from "./canvas/useDrawingHandlers"; import { useStageSize } from "./canvas/useStageSize"; import { useKeyboardMovement } from "./canvas/useKeyboardMovement"; import { useSelectionHandlers } from "./canvas/useSelectionHandlers"; import { useTransformHandlers } from "./canvas/useTransformHandlers"; import { useObjectHandlers } from "./canvas/useObjectHandlers"; import { useMaskGroupState } from "./canvas/useMaskGroupState"; import ObjectsLayer from "./canvas/ObjectsLayer"; import GuidesLayer from "./canvas/GuidesLayer"; import { getSnappedPosition, type Guide } from "./canvas/useSnap"; import CellEditor from "@/components/CellEditor"; import ZoomControls from "./ZoomControls"; type EditorCanvasProps = { catalogSize?: string; }; const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => { const stageRef = useRef(null); const layerRef = useRef(null); const activeGuideIdsRef = useRef([]); const stageWrapRef = useRef(null); const { tool, objects, guides, selectedObjectId, selectedObjectIds, selectedCellId, editingCell, setStageRef, setLayerRef, setGuides, zoom, } = useEditorStore(); const { transformerRef, handleStageMouseDown, handleStageMouseMove, handleStageMouseUp } = useDrawingHandlers(); const stageSize = useStageSize(catalogSize); useKeyboardMovement(); const { handleSelect, handleCellClick, handleCellDblClick } = useSelectionHandlers(); const { handleObjectUpdate, handleGroupWithMask, handleUngroupFromMask, handleCellEditorSave, handleCellEditorCancel } = useObjectHandlers(); const { isGroupedWithMask, maskObject, canGroup, canUngroup, selectedObject } = useMaskGroupState(selectedObjectId); useTransformHandlers(transformerRef, layerRef); const [activeGuideIds, setActiveGuideIds] = useState([]); const [selectedGuideId, setSelectedGuideId] = useState(null); const [draftGuide, setDraftGuide] = useState<{ orientation: Guide["orientation"]; position: number; } | null>(null); useEffect(() => { if (stageRef.current) { setStageRef(stageRef); } if (layerRef.current) { setLayerRef(layerRef); } }, [setStageRef, setLayerRef]); const finalScale = stageSize.scale * zoom; const updateActiveGuides = (nextActiveGuideIds: string[]) => { const prev = activeGuideIdsRef.current; const changed = prev.length !== nextActiveGuideIds.length || prev.some((id, index) => id !== nextActiveGuideIds[index]); if (changed) { activeGuideIdsRef.current = nextActiveGuideIds; setActiveGuideIds(nextActiveGuideIds); } }; const handleGuidePositionChange = (id: string, position: number) => { setGuides( guides.map((guide) => guide.id === id ? { ...guide, position, } : guide, ), ); }; const addGuide = (orientation: Guide["orientation"], position: number) => { const id = `guide-${orientation}-${crypto.randomUUID()}`; setGuides([...guides, { id, orientation, position }]); setSelectedGuideId(id); }; const removeGuide = (id: string) => { setGuides(guides.filter((guide) => guide.id !== id)); setActiveGuideIds((prev) => prev.filter((activeId) => activeId !== id)); setSelectedGuideId((prev) => (prev === id ? null : prev)); }; const handleGuideDragEnd = (id: string, position: number, shouldRemove: boolean) => { if (shouldRemove) { removeGuide(id); return; } handleGuidePositionChange(id, position); }; useEffect(() => { const handleDeleteGuide = (e: KeyboardEvent) => { if (!selectedGuideId) return; const target = e.target as HTMLElement; if ( target?.tagName === "INPUT" || target?.tagName === "TEXTAREA" || target?.isContentEditable ) { return; } if (e.key === "Delete" || e.key === "Backspace") { e.preventDefault(); removeGuide(selectedGuideId); } }; window.addEventListener("keydown", handleDeleteGuide); return () => { window.removeEventListener("keydown", handleDeleteGuide); }; }, [selectedGuideId]); const startRulerGuideDrag = (orientation: Guide["orientation"]) => { const updateFromPointer = (event: MouseEvent) => { const rect = stageWrapRef.current?.getBoundingClientRect(); if (!rect) return null; const rawPos = orientation === "vertical" ? (event.clientX - rect.left) / finalScale : (event.clientY - rect.top) / finalScale; return rawPos; }; const updateDraft = (event: MouseEvent) => { const pointerPos = updateFromPointer(event); if (pointerPos === null) return; const boundedPos = orientation === "vertical" ? Math.max(0, Math.min(stageSize.width, pointerPos)) : Math.max(0, Math.min(stageSize.height, pointerPos)); setDraftGuide({ orientation, position: boundedPos }); }; const handleMouseMove = (event: MouseEvent) => { updateDraft(event); }; const handleMouseUp = (event: MouseEvent) => { const rect = stageWrapRef.current?.getBoundingClientRect(); if (!rect) { setDraftGuide(null); return; } const isInside = event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom; if (isInside) { const pointerPos = updateFromPointer(event); if (pointerPos !== null) { const boundedPos = orientation === "vertical" ? Math.max(0, Math.min(stageSize.width, pointerPos)) : Math.max(0, Math.min(stageSize.height, pointerPos)); addGuide(orientation, boundedPos); } } setDraftGuide(null); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp, { once: true }); }; const handleDragMove = (e: Konva.KonvaEventObject) => { const node = e.target; if (!node || node.name() === "guide-line") return; const snapped = getSnappedPosition(node, guides); if (node.x() !== snapped.position.x || node.y() !== snapped.position.y) { node.position(snapped.position); node.getLayer()?.batchDraw(); } updateActiveGuides(snapped.activeGuideIds); }; const handleDragEnd = () => { updateActiveGuides([]); }; const handleStageMouseDownWithGuideReset = (e: Konva.KonvaEventObject) => { if (e.target?.name() !== "guide-line") { setSelectedGuideId(null); } handleStageMouseDown(e); }; return (
{ e.preventDefault(); startRulerGuideDrag("vertical"); }} title="Drag to create vertical guide" />
{ e.preventDefault(); startRulerGuideDrag("horizontal"); }} title="Drag to create horizontal guide" />
{editingCell && ( )}
); }; export default EditorCanvas;