Files
web-cad/frontend/src/components/CADCanvas.jsx
T

254 lines
9.2 KiB
React
Raw Normal View History

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();
}
}));
// 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 (
<div style={{ width: '100%', height: '100%' }}>
<canvas ref={canvasEl} />
<div style={{ position: 'absolute', bottom: 10, right: 10, background: 'rgba(255,255,255,0.8)', padding: '4px 8px', borderRadius: 4, fontSize: 12 }}>
Zoom: {Math.round(zoom * 100)}%
</div>
</div>
);
});
export default CADCanvas;