如何在React中创建可获取线段坐标的2D蓝图绘图功能?
React蓝图绘图:线段绘制与坐标获取方案
问题描述
我想在React中实现蓝图绘图功能,能够绘制线段并获取线段的起始和结束坐标,效果如图所示。尝试使用react-konva时无法获取正确坐标,求其他可行库或实现方式。
原尝试代码
const snapToGrid = (pos) => { // 將位置限制在最接近的網格交叉點上 return { x: Math.round(pos.x / gridSize) * gridSize, y: Math.round(pos.y / gridSize) * gridSize }; }; const handleMouseDown = (e) => { setIsDrawing(true); const { x, y } = snapToGrid(e.target.getStage().getPointerPosition()); setStartPoint({ x, y }); setEndPoint({ x, y }); // 起點和終點相同 }; const handleMouseMove = (e) => { if (!isDrawing) return; const { x, y } = snapToGrid(e.target.getStage().getPointerPosition()); setEndPoint({ x, y }); }; const handleMouseUp = () => { setIsDrawing(false); // 在滑鼠放開時,將當前繪製的線段添加到線段列表中 setLines([...lines, { points: [startPoint.x, startPoint.y, endPoint.x, endPoint.y] }]); };
解决方案
一、修复react-konva坐标问题
react-konva中getPointerPosition()返回的是全局文档坐标,若Stage存在缩放、偏移或父容器有定位,会导致坐标偏差。改用getRelativePointerPosition()获取相对于Stage的本地坐标即可解决:
const snapToGrid = (pos) => { // 将位置限制在最接近的网格交叉点上 return { x: Math.round(pos.x / gridSize) * gridSize, y: Math.round(pos.y / gridSize) * gridSize }; }; const handleMouseDown = (e) => { setIsDrawing(true); const stage = e.target.getStage(); // 获取相对于Stage的本地坐标 const pos = stage.getRelativePointerPosition(); const snappedPos = snapToGrid(pos); setStartPoint(snappedPos); setEndPoint(snappedPos); }; const handleMouseMove = (e) => { if (!isDrawing) return; const stage = e.target.getStage(); const pos = stage.getRelativePointerPosition(); const snappedPos = snapToGrid(pos); setEndPoint(snappedPos); }; const handleMouseUp = () => { setIsDrawing(false); // 过滤掉起止点相同的无效线段 if (startPoint.x !== endPoint.x || startPoint.y !== endPoint.y) { setLines(prev => [...prev, { points: [startPoint.x, startPoint.y, endPoint.x, endPoint.y] }]); } };
二、替代方案:原生Canvas实现
不依赖第三方库,直接操作Canvas元素,坐标控制更精准,同时支持网格绘制:
import { useRef, useState, useEffect } from 'react'; const BlueprintCanvas = ({ gridSize = 20 }) => { const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [startPoint, setStartPoint] = useState({ x: 0, y: 0 }); const [endPoint, setEndPoint] = useState({ x: 0, y: 0 }); const [lines, setLines] = useState([]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制网格背景 ctx.strokeStyle = '#eee'; ctx.lineWidth = 1; for (let x = 0; x < canvas.width; x += gridSize) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } for (let y = 0; y < canvas.height; y += gridSize) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } // 绘制已保存的线段 ctx.strokeStyle = '#2196F3'; ctx.lineWidth = 2; lines.forEach(line => { ctx.beginPath(); ctx.moveTo(line.points[0], line.points[1]); ctx.lineTo(line.points[2], line.points[3]); ctx.stroke(); }); // 绘制当前正在绘制的临时线段 if (isDrawing) { ctx.strokeStyle = '#FF5722'; ctx.beginPath(); ctx.moveTo(startPoint.x, startPoint.y); ctx.lineTo(endPoint.x, endPoint.y); ctx.stroke(); } }, [lines, isDrawing, startPoint, endPoint, gridSize]); const snapToGrid = (pos) => { return { x: Math.round(pos.x / gridSize) * gridSize, y: Math.round(pos.y / gridSize) * gridSize }; }; const getCanvasRelativePos = (e) => { const canvas = canvasRef.current; const rect = canvas.getBoundingClientRect(); // 计算鼠标相对于Canvas的坐标 return { x: e.clientX - rect.left, y: e.clientY - rect.top }; }; const handleMouseDown = (e) => { setIsDrawing(true); const pos = getCanvasRelativePos(e); const snappedPos = snapToGrid(pos); setStartPoint(snappedPos); setEndPoint(snappedPos); }; const handleMouseMove = (e) => { if (!isDrawing) return; const pos = getCanvasRelativePos(e); const snappedPos = snapToGrid(pos); setEndPoint(snappedPos); }; const handleMouseUp = () => { setIsDrawing(false); if (startPoint.x !== endPoint.x || startPoint.y !== endPoint.y) { setLines(prev => [...prev, { points: [startPoint.x, startPoint.y, endPoint.x, endPoint.y] }]); } }; return ( <canvas ref={canvasRef} width={800} height={600} style={{ border: '1px solid #ccc' }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} /> ); }; export default BlueprintCanvas;
三、替代方案:使用react-sketch-canvas
封装好的绘图库,支持导出路径数据,可轻松提取线段坐标:
- 安装依赖:
npm install react-sketch-canvas
- 示例代码:
import React, { useRef, useState } from 'react'; import { ReactSketchCanvas } from 'react-sketch-canvas'; const BlueprintDrawer = ({ gridSize = 20 }) => { const sketchRef = useRef(null); const [lines, setLines] = useState([]); const extractLineCoords = async () => { // 导出所有绘制路径 const paths = await sketchRef.current.exportPaths(); // 处理路径,提取线段起止坐标并对齐网格 const processedLines = paths.map(path => { const points = path.points; if (points.length >= 2) { const start = points[0]; const end = points[points.length - 1]; const snappedStart = { x: Math.round(start.x / gridSize) * gridSize, y: Math.round(start.y / gridSize) * gridSize }; const snappedEnd = { x: Math.round(end.x / gridSize) * gridSize, y: Math.round(end.y / gridSize) * gridSize }; return { points: [snappedStart.x, snappedStart.y, snappedEnd.x, snappedEnd.y] }; } return null; }).filter(Boolean); setLines(processedLines); console.log('线段坐标列表:', processedLines); }; return ( <div> <ReactSketchCanvas ref={sketchRef} width="800px" height="600px" strokeWidth={2} strokeColor="#2196F3" canvasColor="#fff" /> <button onClick={extractLineCoords} style={{ marginTop: '10px' }}>获取线段坐标</button> <div style={{ marginTop: '10px' }}> <h4>已提取的线段:</h4> <ul> {lines.map((line, idx) => ( <li key={idx}> 起止坐标:({line.points[0]}, {line.points[1]}) → ({line.points[2]}, {line.points[3]}) </li> ))} </ul> </div> </div> ); }; export default BlueprintDrawer;
内容的提问来源于stack exchange,提问作者Julio
相关产品推荐
相关产品推荐

