import { useEffect, useRef, type RefObject } from "react"; import Konva from "konva"; import { useEditorStore } from "../../store/editorStore"; import { getNodeTransformDimensions, nodeHasPendingTransform, resolveObjectFromNodeId, } from "../../utils/transformCommit"; export const useTransformHandlers = ( transformerRef: RefObject, layerRef: RefObject ) => { const { objects, selectedObjectIds, updateObject, applyTableResize, } = useEditorStore(); const commitTransformRef = useRef<(() => void) | null>(null); // Attach transformer to selected objects when they change useEffect(() => { if ( !transformerRef.current || selectedObjectIds.length === 0 || !layerRef.current ) { if (transformerRef.current) { transformerRef.current.nodes([]); transformerRef.current.getLayer()?.batchDraw(); } return; } // Use a small delay to ensure the nodes are mounted (especially for images that need to load) const timeoutId = setTimeout(() => { if (!layerRef.current || !transformerRef.current) return; const nodesToTransform: Konva.Node[] = []; selectedObjectIds.forEach((objId) => { const selectedObject = objects.find((obj) => obj.id === objId); if (!selectedObject) return; // For all objects, find by id (group, grid, or regular) if (selectedObject.type === "group" || selectedObject.type === "grid") { const node = layerRef.current?.findOne(`#${objId}`); if (node) { nodesToTransform.push(node); } } else { // اگر object با mask گروه شده، transformer را به Group متصل کن const maskedGroup = layerRef.current?.findOne(`#masked-${objId}`); const regularNode = layerRef.current?.findOne(`#${objId}`); const targetNode = maskedGroup || regularNode; if (targetNode) { nodesToTransform.push(targetNode); } } }); if (nodesToTransform.length > 0) { transformerRef.current.nodes(nodesToTransform); transformerRef.current.getLayer()?.batchDraw(); } }, 100); return () => { clearTimeout(timeoutId); const transformer = transformerRef.current; if (!transformer) return; const nodes = transformer.nodes(); if (nodes.some(nodeHasPendingTransform)) { commitTransformRef.current?.(); } }; }, [selectedObjectIds, objects, transformerRef, layerRef]); useEffect(() => { if (!transformerRef.current || selectedObjectIds.length === 0) return; const transformer = transformerRef.current; const handleTransform = () => { const nodes = transformer.nodes(); if (nodes.length === 0 || !layerRef.current) return; nodes.forEach((node) => { const nodeId = node.id(); const selectedObject = objects.find((obj) => obj.id === nodeId); if (!selectedObject || selectedObject.type !== "group") return; const scaleX = node.scaleX(); const scaleY = node.scaleY(); const rotation = node.rotation(); const newX = node.x(); const newY = node.y(); const groupMembers = objects.filter( (o) => o.groupId === selectedObject.id ); const oldGroupX = selectedObject.x || 0; const oldGroupY = selectedObject.y || 0; // Update Konva nodes directly for live preview groupMembers.forEach((member) => { const relativeX = (member.x || 0) - oldGroupX; const relativeY = (member.y || 0) - oldGroupY; // Apply rotation const angle = (rotation * Math.PI) / 180; const rotatedX = relativeX * Math.cos(angle) - relativeY * Math.sin(angle); const rotatedY = relativeX * Math.sin(angle) + relativeY * Math.cos(angle); // Apply scale const scaledX = rotatedX * scaleX; const scaledY = rotatedY * scaleY; const finalX = newX + scaledX; const finalY = newY + scaledY; // Find the Konva node for this member const memberNode = layerRef.current?.findOne(`#${member.id}`) || layerRef.current?.findOne(`#masked-${member.id}`); if (memberNode) { // برای AbstractShape و TriangleShape که از مرکز استفاده می‌کنند، باید موقعیت را تنظیم کنیم if ( member.type === "rectangle" && (member.shapeType === "triangle" || member.shapeType === "abstract") ) { // موقعیت در store گوشه بالا-چپ است، اما در render از مرکز استفاده می‌شود const w = (member.width || 100) * scaleX; const h = (member.height || 100) * scaleY; memberNode.x(finalX + w / 2); memberNode.y(finalY + h / 2); } else { memberNode.x(finalX); memberNode.y(finalY); } memberNode.width((member.width || 100) * scaleX); memberNode.height((member.height || 100) * scaleY); memberNode.rotation((member.rotation || 0) + rotation); memberNode.scaleX(1); memberNode.scaleY(1); } }); layerRef.current?.batchDraw(); }); }; const handleTransformEnd = () => { useEditorStore.getState().commitObjectHistoryBeforeChange(); const nodes = transformer.nodes(); if (nodes.length === 0) return; nodes.forEach((node) => { const nodeId = node.id(); const selectedObject = resolveObjectFromNodeId(nodeId, objects); if (!selectedObject) return; // Handle group objects if (selectedObject.type === "group") { const scaleX = node.scaleX(); const scaleY = node.scaleY(); const rotation = node.rotation(); const newX = node.x(); const newY = node.y(); node.scaleX(1); node.scaleY(1); node.rotation(0); const groupMembers = objects.filter( (o) => o.groupId === selectedObject.id ); const oldGroupX = selectedObject.x || 0; const oldGroupY = selectedObject.y || 0; // محاسبه bounds جدید بر اساا member های update شده const updatedMembers = groupMembers.map((member) => { const relativeX = (member.x || 0) - oldGroupX; const relativeY = (member.y || 0) - oldGroupY; // Apply rotation const angle = (rotation * Math.PI) / 180; const rotatedX = relativeX * Math.cos(angle) - relativeY * Math.sin(angle); const rotatedY = relativeX * Math.sin(angle) + relativeY * Math.cos(angle); // Apply scale const scaledX = rotatedX * scaleX; const scaledY = rotatedY * scaleY; return { id: member.id, x: newX + scaledX, y: newY + scaledY, rotation: (member.rotation || 0) + rotation, width: (member.width || 100) * scaleX, height: (member.height || 100) * scaleY, }; }); // Update all members updatedMembers.forEach((member) => { updateObject(member.id, { x: member.x, y: member.y, rotation: member.rotation, width: member.width, height: member.height, }); }); // محاسبه bounds گروه بر اساس member های update شده با در نظر گرفتن rotation let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; updatedMembers.forEach((m) => { const originalMember = groupMembers.find((gm) => gm.id === m.id); if (!originalMember) return; const w = m.width || 0; const h = m.height || 0; // برای دایره، x و y مرکز دایره است و bounding box همیشه یک مربع است if ( originalMember.type === "rectangle" && originalMember.shapeType === "circle" ) { const radius = w / 2; const cx = m.x || 0; const cy = m.y || 0; // برای دایره، bounding box همیشه یک مربع با ضلع برابر با قطر است minX = Math.min(minX, cx - radius); minY = Math.min(minY, cy - radius); maxX = Math.max(maxX, cx + radius); maxY = Math.max(maxY, cy + radius); return; } // برای مثلث و abstract، x و y در store گوشه بالا-چپ است اما در render مرکز استفاده می‌شود if ( originalMember.type === "rectangle" && (originalMember.shapeType === "triangle" || originalMember.shapeType === "abstract") ) { // محاسبه مرکز از گوشه بالا-چپ const cx = (m.x || 0) + w / 2; const cy = (m.y || 0) + h / 2; const rot = ((m.rotation || 0) * Math.PI) / 180; // برای abstract، از radius واقعی استفاده کن (0.85 * baseRadius) // برای triangle، از radius واقعی استفاده کن (min(width, height) / 2) let actualRadius: number; if (originalMember.shapeType === "abstract") { const baseRadius = Math.min(w / 2, h / 2); actualRadius = baseRadius * 0.85; } else { // triangle actualRadius = Math.min(w / 2, h / 2); } // محاسبه چهار گوشه با استفاده از radius واقعی const corners = [ { x: -actualRadius, y: -actualRadius }, { x: actualRadius, y: -actualRadius }, { x: actualRadius, y: actualRadius }, { x: -actualRadius, y: actualRadius }, ]; corners.forEach((corner) => { const rotatedX = corner.x * Math.cos(rot) - corner.y * Math.sin(rot); const rotatedY = corner.x * Math.sin(rot) + corner.y * Math.cos(rot); const finalX = cx + rotatedX; const finalY = cy + rotatedY; minX = Math.min(minX, finalX); minY = Math.min(minY, finalY); maxX = Math.max(maxX, finalX); maxY = Math.max(maxY, finalY); }); return; } // برای arrow و line، x و y نقطه شروع است و width/height نقطه پایان است if ( originalMember.type === "arrow" || originalMember.type === "line" ) { const startX = m.x || 0; const startY = m.y || 0; const endX = m.width || 0; const endY = m.height || 0; // برای arrow و line، bounding box از نقطه شروع تا نقطه پایان است minX = Math.min(minX, startX, endX); minY = Math.min(minY, startY, endY); maxX = Math.max(maxX, startX, endX); maxY = Math.max(maxY, startY, endY); return; } // برای سایر اشکال، x و y گوشه بالا-چپ است const cx = (m.x || 0) + w / 2; const cy = (m.y || 0) + h / 2; const rot = ((m.rotation || 0) * Math.PI) / 180; // محاسبه چهار گوشه rectangle با rotation const corners = [ { x: -w / 2, y: -h / 2 }, { x: w / 2, y: -h / 2 }, { x: w / 2, y: h / 2 }, { x: -w / 2, y: h / 2 }, ]; corners.forEach((corner) => { const rotatedX = corner.x * Math.cos(rot) - corner.y * Math.sin(rot); const rotatedY = corner.x * Math.sin(rot) + corner.y * Math.cos(rot); const finalX = cx + rotatedX; const finalY = cy + rotatedY; minX = Math.min(minX, finalX); minY = Math.min(minY, finalY); maxX = Math.max(maxX, finalX); maxY = Math.max(maxY, finalY); }); }); const groupWidth = maxX - minX; const groupHeight = maxY - minY; // Update group node directly in Konva first to prevent transformer flicker if (layerRef.current) { const groupNode = layerRef.current.findOne(`#${selectedObject.id}`); if (groupNode) { groupNode.x(minX); groupNode.y(minY); groupNode.width(groupWidth); groupNode.height(groupHeight); groupNode.rotation(0); } } // Update group object with new bounds (rotation should remain 0) updateObject(selectedObject.id, { x: minX, y: minY, width: groupWidth, height: groupHeight, rotation: 0, }); // Force transformer to update immediately if (transformerRef.current && layerRef.current) { const groupNode = layerRef.current.findOne(`#${selectedObject.id}`); if (groupNode) { transformerRef.current.nodes([groupNode]); transformerRef.current.getLayer()?.batchDraw(); } } } else if (selectedObject.type === "grid") { // Handle grid objects separately const scaleX = node.scaleX(); const scaleY = node.scaleY(); node.scaleX(1); node.scaleY(1); const newWidth = (selectedObject.width || 300) * scaleX; const newHeight = (selectedObject.height || 200) * scaleY; applyTableResize(selectedObject.id, newWidth, newHeight); updateObject(selectedObject.id, { x: node.x(), y: node.y(), }); } else { // Handle other objects const scaleX = node.scaleX(); const scaleY = node.scaleY(); node.scaleX(1); node.scaleY(1); // Handle special shapes that use radius (Circle, Triangle, Abstract) if (selectedObject.type === "rectangle") { if (selectedObject.shapeType === "circle") { // Circle: maintain aspect ratio and use average scale const avgScale = (scaleX + scaleY) / 2; const newRadius = Math.max( 5, ((selectedObject.width || 50) / 2) * avgScale ); const newSize = newRadius * 2; updateObject(selectedObject.id, { x: node.x(), y: node.y(), rotation: node.rotation(), width: newSize, height: newSize, }); } else if ( selectedObject.shapeType === "triangle" || selectedObject.shapeType === "abstract" ) { // Triangle and Abstract: maintain aspect ratio and use average scale const avgScale = (scaleX + scaleY) / 2; const baseRadius = Math.min( (selectedObject.width || 100) / 2, (selectedObject.height || 100) / 2 ); const newRadius = Math.max(5, baseRadius * avgScale); const newSize = newRadius * 2; updateObject(selectedObject.id, { x: node.x() - newSize / 2, y: node.y() - newSize / 2, rotation: node.rotation(), width: newSize, height: newSize, }); } else { // Regular rectangle updateObject(selectedObject.id, { x: node.x(), y: node.y(), rotation: node.rotation(), width: Math.max(5, (selectedObject.width || 100) * scaleX), height: Math.max(5, (selectedObject.height || 100) * scaleY), }); } } else if ( selectedObject.type === "line" || selectedObject.type === "arrow" ) { // Line and Arrow: scale the end point const startX = selectedObject.x || 0; const startY = selectedObject.y || 0; const endX = selectedObject.width || 0; const endY = selectedObject.height || 0; const dx = (endX - startX) * scaleX; const dy = (endY - startY) * scaleY; updateObject(selectedObject.id, { x: node.x(), y: node.y(), rotation: node.rotation(), width: node.x() + dx, height: node.y() + dy, }); } else if ( selectedObject.type === "text" || selectedObject.type === "link" ) { // Text and Link: use stored width/height from store, not getClientRect // because TextShape auto-calculates dimensions and we need to scale the stored values const storedWidth = selectedObject.width || 100; const storedHeight = selectedObject.height || 100; // Use average scale to maintain aspect ratio for text const avgScale = (scaleX + scaleY) / 2; const newWidth = Math.max(5, storedWidth * scaleX); const newHeight = Math.max(5, storedHeight * scaleY); if (selectedObject.type === "link") { // For link, also update fontSize updateObject(selectedObject.id, { x: node.x(), y: node.y(), rotation: node.rotation(), fontSize: selectedObject.fontSize ? Math.max(5, selectedObject.fontSize * scaleX) : undefined, }); } else { // For text, update width, height, and fontSize updateObject(selectedObject.id, { x: node.x(), y: node.y(), rotation: node.rotation(), width: newWidth, height: newHeight, fontSize: selectedObject.fontSize ? Math.max(5, selectedObject.fontSize * avgScale) : undefined, }); } } else if (selectedObject.type === "custom-shape") { // Scale each vertex in-place; no width/height anchor needed const oldPoints = selectedObject.points ?? []; const newPoints = oldPoints.map((val, i) => i % 2 === 0 ? val * scaleX : val * scaleY, ); // Recompute axis-aligned bounding box from the scaled points const xs = newPoints.filter((_, i) => i % 2 === 0); const ys = newPoints.filter((_, i) => i % 2 !== 0); const newWidth = xs.length > 0 ? Math.max(...xs) - Math.min(...xs) : 0; const newHeight = ys.length > 0 ? Math.max(...ys) - Math.min(...ys) : 0; updateObject(selectedObject.id, { x: node.x(), y: node.y(), rotation: node.rotation(), points: newPoints, width: Math.max(1, newWidth), height: Math.max(1, newHeight), }); } else { // Default for raster objects (image, sticker, document, video, …) const { width: baseWidth, height: baseHeight } = getNodeTransformDimensions(node); updateObject(selectedObject.id, { x: node.x(), y: node.y(), rotation: node.rotation(), width: Math.max(5, baseWidth * scaleX), height: Math.max(5, baseHeight * scaleY), }); } } }); }; commitTransformRef.current = handleTransformEnd; transformer.on("transform", handleTransform); transformer.on("transformend", handleTransformEnd); return () => { commitTransformRef.current = null; transformer.off("transform", handleTransform); transformer.off("transformend", handleTransformEnd); }; }, [ selectedObjectIds, objects, transformerRef, applyTableResize, updateObject, layerRef, ]); };