import React, { useEffect, useRef, forwardRef, useImperativeHandle, useState } from 'react'; import { fabric } from 'fabric'; const CADCanvas = forwardRef(({ activeTool, setActiveTool, onCanvasReady }, ref) => { const canvasEl = useRef(null); const fabricRef = useRef(null); const [zoom, setZoom] = useState(1); const isDrawing = useRef(false); const startPoint = useRef(null); const tempShape = useRef(null); useImperativeHandle(ref, () => ({ getCanvas: () => fabricRef.current, toJSON: () => fabricRef.current?.toJSON(), loadFromJSON: (json) => { if (fabricRef.current) { fabricRef.current.loadFromJSON(json, fabricRef.current.renderAll.bind(fabricRef.current)); } }, addSvgObject: (svgStr) => { if (!fabricRef.current) return; fabric.loadSVGFromString(svgStr, (objects, options) => { const group = fabric.util.groupSVGElements(objects, options); group.set({ left: 100, top: 100, scaleX: 0.5, scaleY: 0.5 }); fabricRef.current.add(group); fabricRef.current.setActiveObject(group); fabricRef.current.renderAll(); }); }, addObjects: (objects) => { if (!fabricRef.current) return; objects.forEach(obj => { let shape; if (obj.type === 'line') { shape = new fabric.Line([obj.x1, obj.y1, obj.x2, obj.y2], { stroke: obj.stroke || '#000', strokeWidth: obj.strokeWidth || 1 }); } else if (obj.type === 'circle') { shape = new fabric.Circle({ left: obj.left, top: obj.top, radius: obj.radius, fill: obj.fill || 'transparent', stroke: obj.stroke || '#000', strokeWidth: obj.strokeWidth || 1 }); } else if (obj.type === 'polygon') { shape = new fabric.Polygon(obj.points.map(p => ({ x: p.x, y: p.y })), { fill: obj.fill || 'transparent', stroke: obj.stroke || '#000', strokeWidth: obj.strokeWidth || 1 }); } else if (obj.type === 'rect') { shape = new fabric.Rect({ left: obj.left, top: obj.top, width: obj.width, height: obj.height, fill: obj.fill || 'transparent', stroke: obj.stroke || '#000', strokeWidth: obj.strokeWidth || 1 }); } if (shape) fabricRef.current.add(shape); }); fabricRef.current.renderAll(); }, // Get selected elements for block creation getSelectedElements: () => { if (!fabricRef.current) return []; const activeObject = fabricRef.current.getActiveObject(); if (!activeObject) return []; // If it's a group, return all objects in the group if (activeObject.type === 'group') { return activeObject.getObjects().map(obj => ({ id: obj.id || Date.now() + Math.random(), type: obj.type, ...obj.toObject(['id', 'type', 'left', 'top', 'width', 'height', 'radius', 'x1', 'y1', 'x2', 'y2', 'points', 'fill', 'stroke', 'strokeWidth']) })); } // If it's a single object, return it return [{ id: activeObject.id || Date.now() + Math.random(), type: activeObject.type, ...activeObject.toObject(['id', 'type', 'left', 'top', 'width', 'height', 'radius', 'x1', 'y1', 'x2', 'y2', 'points', 'fill', 'stroke', 'strokeWidth']) }]; }, // Generate SVG data for selected elements generateSvgData: () => { if (!fabricRef.current) return ''; const activeObject = fabricRef.current.getActiveObject(); if (!activeObject) return ''; // Clone the object to avoid modifying the original return activeObject.toSVG(); } })); // Initialize fabric canvas useEffect(() => { if (canvasEl.current && !fabricRef.current) { fabricRef.current = new fabric.Canvas(canvasEl.current, { width: canvasEl.current.parentElement.clientWidth, height: canvasEl.current.parentElement.clientHeight, backgroundColor: '#fff', selection: true, }); if (onCanvasReady) onCanvasReady(fabricRef.current); const handleResize = () => { if (canvasEl.current && fabricRef.current) { fabricRef.current.setWidth(canvasEl.current.parentElement.clientWidth); fabricRef.current.setHeight(canvasEl.current.parentElement.clientHeight); fabricRef.current.renderAll(); } }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); if (fabricRef.current) { fabricRef.current.dispose(); fabricRef.current = null; } }; } }, []); // Zoom with mouse wheel useEffect(() => { if (!fabricRef.current) return; const canvas = fabricRef.current; const handleWheel = (opt) => { const delta = opt.e.deltaY; let zoom = canvas.getZoom(); zoom *= 0.999 ** delta; if (zoom > 20) zoom = 20; if (zoom < 0.01) zoom = 0.01; canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom); opt.e.preventDefault(); opt.e.stopPropagation(); setZoom(Math.round(zoom * 100) / 100); }; canvas.on('mouse:wheel', handleWheel); return () => canvas.off('mouse:wheel', handleWheel); }, []); // Drawing tool behavior useEffect(() => { if (!fabricRef.current) return; const canvas = fabricRef.current; canvas.off('mouse:down'); canvas.off('mouse:move'); canvas.off('mouse:up'); if (activeTool === 'select') { canvas.isDrawingMode = false; canvas.selection = true; canvas.forEachObject(obj => obj.selectable = true); return; } canvas.selection = false; canvas.forEachObject(obj => obj.selectable = false); if (activeTool === 'freehand') { canvas.isDrawingMode = true; canvas.freeDrawingBrush.width = 2; canvas.freeDrawingBrush.color = '#000'; return; } canvas.isDrawingMode = false; canvas.on('mouse:down', (opt) => { isDrawing.current = true; const pointer = canvas.getPointer(opt.e); startPoint.current = { x: pointer.x, y: pointer.y }; if (activeTool === 'line') { tempShape.current = new fabric.Line( [pointer.x, pointer.y, pointer.x, pointer.y], { stroke: '#000', strokeWidth: 2 } ); } else if (activeTool === 'rect') { tempShape.current = new fabric.Rect({ left: pointer.x, top: pointer.y, width: 0, height: 0, fill: 'transparent', stroke: '#000', strokeWidth: 2, }); } else if (activeTool === 'circle') { tempShape.current = new fabric.Circle({ left: pointer.x, top: pointer.y, radius: 0, fill: 'transparent', stroke: '#000', strokeWidth: 2, }); } else if (activeTool === 'text') { const text = new fabric.IText('Text', { left: pointer.x, top: pointer.y, fontSize: 20, fill: '#000', }); canvas.add(text); canvas.setActiveObject(text); isDrawing.current = false; return; } else if (activeTool === 'dimension') { const line = new fabric.Line([pointer.x, pointer.y, pointer.x, pointer.y], { stroke: '#e74c3c', strokeWidth: 1, strokeDashArray: [5, 5] }); const tick1 = new fabric.Line([pointer.x, pointer.y, pointer.x+10, pointer.y], { stroke: '#e74c3c', strokeWidth: 1 }); const tick2 = new fabric.Line([pointer.x+10, pointer.y, pointer.x+10, pointer.y+10], { stroke: '#e74c3c', strokeWidth: 1 }); const text = new fabric.Text('0', { left: pointer.x, top: pointer.y - 20, fontSize: 12, fill: '#e74c3c', }); const group = new fabric.Group([line, tick1, tick2, text], { selectable: false, hasControls: false, hasBorders: false, }); tempShape.current = { group, line, text, tick1, tick2, start: {x: pointer.x, y: pointer.y} }; canvas.add(group); } if (tempShape.current && !(activeTool === 'dimension')) { canvas.add(tempShape.current); } }); canvas.on('mouse:move', (opt) => { if (!isDrawing.current || !tempShape.current) return; const pointer = canvas.getPointer(opt.e); if (activeTool === 'line') { tempShape.current.set({ x2: pointer.x, y2: pointer.y }); } else if (activeTool === 'rect') { const start = startPoint.current; let left = Math.min(start.x, pointer.x); let top = Math.min(start.y, pointer.y); let width = Math.abs(start.x - pointer.x); let height = Math.abs(start.y - pointer.y); tempShape.current.set({ left, top, width, height }); } else if (activeTool === 'circle') { const start = startPoint.current; const radius = Math.sqrt( (pointer.x - start.x) ** 2 + (pointer.y - start.y) ** 2 ) / 2; tempShape.current.set({ left: start.x - radius, top: start.y - radius, radius: radius, }); } else if (activeTool === 'dimension') { const { group, line, text, tick1, tick2, start } = tempShape.current; // Update line line.set({ x2: pointer.x, y2: pointer.y }); // Compute distance const dx = pointer.x - start.x; const dy = pointer.y - start.y; const dist = Math.sqrt(dx*dx + dy*dy).toFixed(1); text.set({ text: dist }); // Update text position to middle of line const midX = (start.x + pointer.x) / 2; const midY = (start.y + pointer.y) / 2; text.set({ left: midX - 15, top: midY - 20 }); // Update tick positions (simplified: put at start and end) tick1.set({ x1: start.x, y1: start.y, x2: start.x+10, y2: start.y }); tick2.set({ x1: pointer.x, y1: pointer.y, x2: pointer.x-10, y2: pointer.y }); // Add corner tick lines (just simple markers) // Refresh group group.addWithUpdate(); } canvas.renderAll(); }); canvas.on('mouse:up', () => { if (isDrawing.current && activeTool === 'dimension' && tempShape.current) { // Make dimension group selectable after placement tempShape.current.group.set({ selectable: true, hasControls: false, hasBorders: true }); } isDrawing.current = false; tempShape.current = null; }); }, [activeTool]); return (
Zoom: {Math.round(zoom * 100)}%
); }); export default CADCanvas;