You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

封装好的绘图库,支持导出路径数据,可轻松提取线段坐标:

  1. 安装依赖:
npm install react-sketch-canvas
  1. 示例代码:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 02:57:15