import { useEffect, useRef } from "react"; import { Group, Rect, Circle, RegularPolygon, Star } from "react-konva"; import Konva from "konva"; import type { EditorObject } from "../../store/editorStore"; import { RectangleShape, CircleShape, TriangleShape, AbstractShape, TextShape, LineShape, ArrowShape, ImageObject, LinkShape, VideoShape, } from "../tools"; import Table from "@/components/Table"; type ObjectRendererProps = { obj: EditorObject; isSelected: boolean; transformerRef: React.RefObject; layerRef: React.RefObject; onSelect: (id: string, node: Konva.Node) => void; onUpdate: (id: string, updates: Partial) => void; draggable: boolean; onCellClick?: (tableId: string, cellId: string) => void; onCellDblClick?: (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => void; selectedCellId?: string | null; allObjects?: EditorObject[]; }; const ObjectRenderer = ({ obj, isSelected, transformerRef, layerRef, onSelect, onUpdate, draggable, onCellClick, onCellDblClick, selectedCellId, allObjects = [], }: ObjectRendererProps) => { const groupRef = useRef(null); // Find mask for this object const maskShape = obj.maskId ? allObjects.find((m) => m.id === obj.maskId) : null; const shouldApplyMask = maskShape && !obj.isMask; // Apply caching for masked objects - REQUIRED for destination-in to work useEffect(() => { if (!groupRef.current || !shouldApplyMask) return; // Use requestAnimationFrame to ensure shapes are rendered before caching const rafId = requestAnimationFrame(() => { if (!groupRef.current) return; // Clear cache first groupRef.current.clearCache(); // Apply cache - Konva will automatically calculate bounds groupRef.current.cache(); groupRef.current.getLayer()?.batchDraw(); }); return () => { cancelAnimationFrame(rafId); groupRef.current?.clearCache(); }; }, [shouldApplyMask, maskShape?.x, maskShape?.y, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.x, obj.y, obj.width, obj.height, obj.rotation]); if (obj.visible === false) { return null; } // برای objectهای با mask، یک obj جدید با موقعیت (0, 0) ایجاد کن const shapeObj = shouldApplyMask ? { ...obj, x: 0, y: 0 } : obj; const commonProps = { obj: shapeObj, isSelected, transformerRef, onSelect, onUpdate, // اگر mask اعمال شده، shape را غیر draggable کن (Group drag می‌شود) draggable: shouldApplyMask ? false : draggable, }; const renderMaskShape = (mask: EditorObject) => { const maskProps = { listening: false, perfectDrawEnabled: false, fill: "black", }; // موقعیت mask نسبت به object - برای overlap صحیح // mask در موقعیت مطلق است، پس باید آن را relative به object کنیم // چون shape در موقعیت (0, 0) است، باید موقعیت mask را نسبت به shape محاسبه کنم const relativeX = (mask.x || 0) - (obj.x || 0); const relativeY = (mask.y || 0) - (obj.y || 0); if (mask.type === "rectangle" && mask.shapeType === "circle") { return ( ); } if (mask.type === "rectangle" && mask.shapeType === "triangle") { const radius = Math.min((mask.width || 100) / 2, (mask.height || 100) / 2); return ( ); } if (mask.type === "rectangle" && mask.shapeType === "abstract") { const baseRadius = Math.min((mask.width || 100) / 2, (mask.height || 100) / 2); const abstractScale = 0.85; const radius = baseRadius * abstractScale; const innerRadius = radius * 0.5; return ( ); } return ( ); }; let shapeElement: React.ReactNode = null; switch (obj.type) { case "rectangle": if (obj.shapeType === "circle") { shapeElement = ; } else if (obj.shapeType === "triangle") { shapeElement = ; } else if (obj.shapeType === "abstract") { shapeElement = ; } else { shapeElement = ; } break; case "text": shapeElement = ; break; case "line": shapeElement = ; break; case "arrow": shapeElement = ; break; case "image": shapeElement = ; break; case "link": shapeElement = ; break; case "video": shapeElement = ; break; case "document": shapeElement = ; break; case "grid": shapeElement = ( { onSelect(tableId, node); onCellClick?.(tableId, cellId); }} onCellDblClick={(tableId, cellId, x, y, width, height, text) => { onCellDblClick?.(tableId, cellId, x, y, width, height, text); }} onDragEnd={(tableId, x, y) => { onUpdate(tableId, { x, y }); }} draggable={draggable} /> ); break; default: return null; } // If no mask, render normally if (!shouldApplyMask || !maskShape) { return shapeElement; } // Apply masking with Group + destination-in or destination-out // IMPORTANT: The shape inside keeps its normal event handlers // The mask layer has listening={false} to not interfere with selection // Default: destination-out (overlap مخفی می‌شود) const compositeOp = obj.maskInvert === false ? "destination-in" : "destination-out"; const handleGroupDragEnd = (e: Konva.KonvaEventObject) => { const node = e.target; onUpdate(obj.id, { x: node.x(), y: node.y(), }); }; const handleGroupDragMove = (e: Konva.KonvaEventObject) => { const node = e.target; onUpdate(obj.id, { x: node.x(), y: node.y(), }); // به‌روزرسانی transformer در حین drag if (transformerRef.current && isSelected) { transformerRef.current.nodes([node]); transformerRef.current.getLayer()?.batchDraw(); } }; const handleGroupTransformEnd = () => { if (!groupRef.current) return; const node = groupRef.current; const scaleX = node.scaleX(); const scaleY = node.scaleY(); node.scaleX(1); node.scaleY(1); onUpdate(obj.id, { x: node.x(), y: node.y(), rotation: node.rotation(), width: Math.max(5, (obj.width || 100) * scaleX), height: Math.max(5, (obj.height || 100) * scaleY), }); }; return ( { if (groupRef.current) { onSelect(obj.id, groupRef.current); } }} onDragEnd={handleGroupDragEnd} onDragMove={handleGroupDragMove} onTransformEnd={handleGroupTransformEnd} > {shapeElement} {renderMaskShape(maskShape)} ); }; export default ObjectRenderer;