基于canvas的路网编辑交互

效果

扩展

结合cesium 加上节点状态控制和路径损失设计,可以做多层场景路网规划。

代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        * {
            box-sizing: border-box;
        }

        body {
            margin: 0;
            padding: 0;
            background-color: #f5f5f5;
        }

        .container {
            position: absolute;
            right: 0;
            top: 0;
            padding: 10px;
            font-size: 14px;
        }

        canvas {
            display: block;
            width: 100vw;
            height: 100vh;
        }

    </style>
</head>
<body>
<div class="container">
    <div><strong>Ctrl+左键点击:</strong> 创建点或选中点/线</div>
    <div><strong>Ctrl+右键点击:</strong>
        <div>当选中点时:创建与另一个点的连线</div>
        <div>当选中线时:在线段上插入新点并分割线段</div>
    </div>
    <div><strong>Alt+右键点击:</strong> 当选中点时,创建与另一个点的连线并分割所有相交的线段</div>
    <div><strong>直接右键点击:</strong> 清空当前选中状态</div>
    <div><strong>鼠标拖动:</strong> 拖动点调整位置</div>
    <div><strong>Backspace键:</strong> 删除当前选中的点或线</div>
    <div class="status-bar">
        状态: <span id="status">就绪 - 按住Ctrl点击左键创建点或选择点/线</span>
    </div>
</div>
<canvas id="canvas"></canvas>
<script>
// 获取Canvas元素和上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const statusText = document.getElementById('status');
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight

// 定义全局变量
let pointArr = []; // 点数组 {id:'', position:[x,y]}
let linkArr = []; // 线数组 {id:'', data:[pointA, pointB]}
let focus = null; // 焦点对象 {m: pointId/linkId, t: 'point'/'link'}

// 常量定义
const R = 10; // 点检测半径
const H = 5; // 线检测宽度

// 生成唯一ID
const generateId = () => crypto.randomUUID()
// 计算两点间距离
const distance = (x1, y1, x2, y2) => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)

// 判断点是否在线段附近
function isPointNearLine(px, py, x1, y1, x2, y2) {
    // 计算点到线段的最短距离
    const A = px - x1, B = py - y1, C = x2 - x1, D = y2 - y1;

    const dot = A * C + B * D;
    const lenSq = C * C + D * D;
    let param = -1;

    if (lenSq !== 0) {
        param = dot / lenSq;
    }

    let xx, yy;

    if (param < 0) {
        xx = x1;
        yy = y1;
    } else if (param > 1) {
        xx = x2;
        yy = y2;
    } else {
        xx = x1 + param * C;
        yy = y1 + param * D;
    }

    const dx = px - xx, dy = py - yy;

    return Math.sqrt(dx * dx + dy * dy) <= H;
}

// 判断两线段是否相交,并返回交点
function getLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
    const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
    // 平行或共线
    if (Math.abs(denominator) < 0.0001) {
        return null;
    }
    const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
    const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
    // 检查交点是否在两线段上
    if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
        const intersectionX = x1 + t * (x2 - x1);
        const intersectionY = y1 + t * (y2 - y1);
        // 排除端点相交的情况
        const isEndpoint =
            (Math.abs(intersectionX - x1) < 0.1 && Math.abs(intersectionY - y1) < 0.1) ||
            (Math.abs(intersectionX - x2) < 0.1 && Math.abs(intersectionY - y2) < 0.1) ||
            (Math.abs(intersectionX - x3) < 0.1 && Math.abs(intersectionY - y3) < 0.1) ||
            (Math.abs(intersectionX - x4) < 0.1 && Math.abs(intersectionY - y4) < 0.1);

        if (isEndpoint) {
            return null;
        }
        return {x: intersectionX, y: intersectionY};
    }
    return null;
}

// 找到距离某个点最近的点
function findNearestPoint(x, y, excludeId = null) {
    let minDist = Infinity;
    let nearestPoint = null;
    for (const point of pointArr) {
        if (excludeId && point.id === excludeId) continue;

        const dist = distance(x, y, point.position[0], point.position[1]);
        if (dist < R && dist < minDist) {
            minDist = dist;
            nearestPoint = point;
        }
    }
    return nearestPoint;
}

// 找到包含某个点的线
function findLineAtPoint(x, y) {
    for (const link of linkArr) {
        const pointA = pointArr.find(p => p.id === link.data[0]);
        const pointB = pointArr.find(p => p.id === link.data[1]);

        if (pointA && pointB) {
            if (isPointNearLine(x, y, pointA.position[0], pointA.position[1], pointB.position[0], pointB.position[1])) {
                return link;
            }
        }
    }
    return null;
}

// 创建点
function createPoint(x, y) {
    const newPoint = {id: generateId(), position: [x, y]};
    pointArr.push(newPoint);
    updateStatus(`创建点 (${Math.round(x)}, ${Math.round(y)})`);
    return newPoint;
}

// 创建线
function createLink(pointAId, pointBId) {
    // 检查是否已存在相同的连线
    const existingLink = linkArr.find(link =>
        (link.data[0] === pointAId && link.data[1] === pointBId) ||
        (link.data[0] === pointBId && link.data[1] === pointAId)
    );

    if (existingLink) {
        updateStatus("连线已存在");
        return existingLink;
    }

    const newLink = {id: generateId(), data: [pointAId, pointBId]};

    linkArr.push(newLink);

    const pointA = pointArr.find(p => p.id === pointAId);
    const pointB = pointArr.find(p => p.id === pointBId);
    updateStatus(`创建连线: 点${pointArr.indexOf(pointA) + 1} ↔ 点${pointArr.indexOf(pointB) + 1}`);
    return newLink;
}

// 删除点
function deletePoint(pointId) {
    // 删除与该点相关的所有连线
    linkArr = linkArr.filter(link => !link.data.includes(pointId));

    // 删除点
    const index = pointArr.findIndex(p => p.id === pointId);
    if (index !== -1) {
        pointArr.splice(index, 1);
    }

    // 如果焦点在该点上,清空焦点
    if (focus && focus.t === 'point' && focus.m === pointId) {
        focus = null;
    }

    updateStatus(`删除点${index + 1}`);
}

// 删除线
function deleteLink(linkId) {
    const index = linkArr.findIndex(l => l.id === linkId);
    if (index !== -1) {
        linkArr.splice(index, 1);
    }

    // 如果焦点在该线上,清空焦点
    if (focus && focus.t === 'link' && focus.m === linkId) {
        focus = null;
    }

    updateStatus(`删除连线${index + 1}`);
}

// 更新状态文本
function updateStatus(text) {
    statusText.textContent = "状态: " + text;
}

// 绘制所有点和线
function draw() {
    // 清空画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制所有线
    for (const link of linkArr) {
        const pointA = pointArr.find(p => p.id === link.data[0]);
        const pointB = pointArr.find(p => p.id === link.data[1]);

        if (!pointA || !pointB) continue;

        // 设置线条颜色
        if (focus && focus.t === 'link' && focus.m === link.id) {
            ctx.strokeStyle = '#ff0000'; // 红色
            ctx.lineWidth = 3;
        } else {
            ctx.strokeStyle = '#888888'; // 灰色
            ctx.lineWidth = 2;
        }

        ctx.beginPath();
        ctx.moveTo(pointA.position[0], pointA.position[1]);
        ctx.lineTo(pointB.position[0], pointB.position[1]);
        ctx.stroke();
    }

    // 绘制所有点
    for (let i = 0; i < pointArr.length; i++) {
        const point = pointArr[i];
        // 设置点颜色
        if (focus && focus.t === 'point' && focus.m === point.id) {
            ctx.fillStyle = '#ff0000'; // 红色
        } else {
            ctx.fillStyle = '#000000'; // 黑色
        }
        ctx.beginPath();
        ctx.arc(point.position[0], point.position[1], 5, 0, Math.PI * 2);
        ctx.fill();
    }
    let focusT = focus?.t;
    if (focusT === 'point') {
        const point = pointArr.find(p => p.id === focus.m);
        if (point) {
            const index = pointArr.indexOf(point);
            ctx.fillText(`当前选中: 点${index + 1} (${Math.round(point.position[0])}, ${Math.round(point.position[1])})`, 10, 40);
        }
    } else if (focusT === 'link') {
        const link = linkArr.find(l => l.id === focus.m);
        if (link) {
            const pointA = pointArr.find(p => p.id === link.data[0]);
            const pointB = pointArr.find(p => p.id === link.data[1]);
            if (pointA && pointB) {
                const indexA = pointArr.indexOf(pointA);
                const indexB = pointArr.indexOf(pointB);
                ctx.fillText(`当前选中: 连线${indexA + 1}↔${indexB + 1}`, 10, 40);
            }
        }
    }
}

let isDragging = false;
let dragPoint = null;
let mouseX = 0;
let mouseY = 0;

canvas.addEventListener('click', (e) => {
    const ctrlPressed = e.ctrlKey;
    if (ctrlPressed) {
        e.preventDefault();

        mouseX = e.offsetX;
        mouseY = e.offsetY;

        // 1) 根据R判断是否有选中点
        const point = findNearestPoint(mouseX, mouseY);

        if (point) {
            // 有点则设置focus
            focus = {m: point.id, t: 'point'};
            updateStatus(`选中点${pointArr.indexOf(point) + 1}`);
        } else {
            // 2) 没有点就根据H判断是否选中线
            const link = findLineAtPoint(mouseX, mouseY);

            if (link) {
                // 有线则设置focus
                focus = {m: link.id, t: 'link'};
                updateStatus("选中连线");
            } else {
                // 3) 如果也没有线,则创建一个点P,并设置focus
                const newPoint = createPoint(mouseX, mouseY);
                focus = {m: newPoint.id, t: 'point'};
            }
        }
    }
    draw();
});

canvas.addEventListener('contextmenu', (e) => {
    e.preventDefault();

    mouseX = e.offsetX;
    mouseY = e.offsetY;

    const ctrlPressed = e.ctrlKey;
    const altPressed = e.altKey;

    if (!ctrlPressed && !altPressed) {
        // 直接鼠标右键会清空focus
        focus = null;
        updateStatus("已清空选中状态");
    } else if (ctrlPressed) {
        if (focus !== null && focus.t === 'point') {
            // 根据R判断右键是否有选中点
            const rightClickPoint = findNearestPoint(mouseX, mouseY, focus.m);

            if (rightClickPoint) {
                // 如果有则创建两点的link
                createLink(focus.m, rightClickPoint.id);
                // 将focus设置为右键选中的点
                focus = {m: rightClickPoint.id, t: 'point'};
            } else {
                // 如果右键没有选中点,则在右键处创建一个点
                const newPoint = createPoint(mouseX, mouseY);
                // 创建和focus的link
                createLink(focus.m, newPoint.id);
                // 将focus设置为新创建的点
                focus = {m: newPoint.id, t: 'point'};
            }
        } else if (focus !== null && focus.t === 'link') {
            // 根据H判断右键是否在focus指代的link上
            const link = linkArr.find(l => l.id === focus.m);
            if (link) {
                const pointA = pointArr.find(p => p.id === link.data[0]);
                const pointB = pointArr.find(p => p.id === link.data[1]);

                if (pointA && pointB && isPointNearLine(mouseX, mouseY, pointA.position[0], pointA.position[1], pointB.position[0], pointB.position[1])) {
                    // 在线段上创建新点
                    const newPoint = createPoint(mouseX, mouseY);

                    // 删除原来的link
                    deleteLink(link.id);

                    // 创建两个新link
                    createLink(pointA.id, newPoint.id);
                    createLink(newPoint.id, pointB.id);

                    // 设置焦点为新点
                    focus = {m: newPoint.id, t: 'point'};
                }
            }
        }
    } else if (altPressed) {
        if (focus !== null && focus.t === 'point') {
            const focusPoint = pointArr.find(p => p.id === focus.m);
            if (!focusPoint) return;

            // 1. 根据R判断右键是否有选中点N
            const rightClickPoint = findNearestPoint(mouseX, mouseY, focus.m);
            let pointN = null;

            if (rightClickPoint) {
                // 1) 如果有则记录M = focusPoint, 然后设置focus为N
                pointN = rightClickPoint;
                focus = {m: pointN.id, t: 'point'};
            } else {
                // 2) 如果右键没有选中点,则在右键处创建一个点N
                pointN = createPoint(mouseX, mouseY);
                // 记录M = focusPoint, 然后设置focus为N
                focus = {m: pointN.id, t: 'point'};
            }

            // 2. 遍历linkArr中所有link,检查与线段[M,N]是否有交叉点
            const M = focusPoint;
            const N = pointN;
            const crossPoints = [];

            // 记录需要删除和添加的link
            const linksToRemove = [];
            const linksToAdd = [];

            for (const link of linkArr) {
                const pointA = pointArr.find(p => p.id === link.data[0]);
                const pointB = pointArr.find(p => p.id === link.data[1]);

                if (!pointA || !pointB) continue;

                // 检查当前link是否与线段M-N相交
                const intersection = getLineIntersection(
                    M.position[0], M.position[1],
                    N.position[0], N.position[1],
                    pointA.position[0], pointA.position[1],
                    pointB.position[0], pointB.position[1]
                );

                if (intersection) {
                    // 添加交点
                    const crossPoint = {
                        id: generateId(),
                        position: [intersection.x, intersection.y]
                    };
                    pointArr.push(crossPoint);
                    crossPoints.push({
                        point: crossPoint,
                        distance: distance(M.position[0], M.position[1], intersection.x, intersection.y)
                    });

                    // 标记原link需要删除
                    linksToRemove.push(link.id);

                    // 添加两个新link
                    linksToAdd.push({data: [pointA.id, crossPoint.id]});
                    linksToAdd.push({data: [crossPoint.id, pointB.id]});
                }
            }

            // 执行删除操作
            for (const linkId of linksToRemove) {
                const index = linkArr.findIndex(l => l.id === linkId);
                if (index !== -1) {
                    linkArr.splice(index, 1);
                }
            }

            // 执行添加操作
            for (const linkData of linksToAdd) {
                createLink(linkData.data[0], linkData.data[1]);
            }

            // 3. 收集所有的crossPoint
            const allPoints = [
                {point: M, distance: 0},
                ...crossPoints,
                {point: N, distance: distance(M.position[0], M.position[1], N.position[0], N.position[1])}
            ];

            // 按距离排序
            allPoints.sort((a, b) => a.distance - b.distance);

            // 从M开始每相邻两个点创建一个新的link
            for (let i = 0; i < allPoints.length - 1; i++) {
                createLink(allPoints[i].point.id, allPoints[i + 1].point.id);
            }
            updateStatus(`创建连线并分割了${crossPoints.length}条相交线段`);
        }
    }
    draw();
})

// 鼠标移动事件
canvas.addEventListener('mousedown', (e) => {
    e.preventDefault();

    mouseX = e.offsetX;
    mouseY = e.offsetY;

    // 检查是否按住了Ctrl键
    const ctrlPressed = e.ctrlKey;
    const altPressed = e.altKey;
    if (!ctrlPressed && !altPressed) {
        // 普通左键:检查是否在点上,开始拖动
        const point = findNearestPoint(mouseX, mouseY);
        if (point) {
            isDragging = true;
            dragPoint = point;
            focus = {m: point.id, t: 'point'};
            updateStatus(`开始拖动点${pointArr.indexOf(point) + 1}`);
        }
    }
});
// 鼠标移动事件
canvas.addEventListener('mousemove', (e) => {
    mouseX = e.offsetX;
    mouseY = e.offsetY;

    if (isDragging && dragPoint) {
        dragPoint.position[0] = mouseX;
        dragPoint.position[1] = mouseY;
        draw();
    }
});

// 鼠标释放事件
canvas.addEventListener('mouseup', () => {
    if (isDragging) {
        isDragging = false;
        dragPoint = null;
        updateStatus("拖动结束");
    }
});

// 键盘事件
document.addEventListener('keydown', (e) => {
    if (e.key === 'Backspace' && focus) {
        if (focus.t === 'point') {
            deletePoint(focus.m);
        } else if (focus.t === 'link') {
            deleteLink(focus.m);
        }
        draw();
    }
});


// 初始绘制
draw();
updateStatus("就绪 - 按住Ctrl点击左键创建点或选择点/线");

function addExamplePoints() {
    // 示例点
    const p1 = createPoint(200, 200);
    const p2 = createPoint(400, 200);
    const p3 = createPoint(300, 350);
    const p4 = createPoint(500, 300);

    // 示例连线
    createLink(p1.id, p2.id);
    createLink(p2.id, p3.id);

    draw();
}

addExamplePoints();

</script>
</body>
</html>

思路

已知canvas

定义全局变量

let pointArr = []; // 点数组 {id:'', position:[x,y]}

let linkArr = []; // 线数组 {id:'', data:[pointA, pointB]}

let focus = null; // 焦点对象 {m: pointId/linkId, t: 'point'/'link'}

// 常量定义

const R = 10; // 点检测半径

const H = 5; // 线检测宽度

按住ctrl 点击鼠标左键,

1)根据R判断是否有选中点P,有点则设置focus={m:P,t:'point'}

2)没有点就根据H判断是否选中线D,有线则设置focus={m:D,t:'link'}

3)如果也没有线,则创建一个点P,并设置focus={m:P,t:'point'}

也就是说按住ctrl的鼠标左键用来创建点和选中点或link

按住ctrl 点击鼠标右键,如果focus不为空且t === 'point',

根据R判断右键否有选中点,如果有则创建两点的link,并将focus设置为右键选中的点

如果右键没有选中点,则在右键处创建一个点,并创建和focus的link,并将focus设置为新创建的点

按住ctrl 点击鼠标右键,如果focus不为空且t === 'link',循根据H判断右键是否在focus指代的link上,找到第一个符合的link,

假如link的两端点为M,N,创建一个右键位置的点P,删除M,N的link,创建MP,PN的link

按住alt 点击鼠标右键,如果focus不为空且t === 'point',

1.根据R判断右键否有选中点N,

1)如果有则记录M = focusPoint,然后设置focus为N

2)如果右键没有选中点,则在右键处创建一个点N,记录M = focusPoint,然后设置focus为N

2.然后需要依次判断linkArr中所有link:[a,b]和线段[M,N]是否有交叉点 crossPoint,注意是判断所有的link;

如果有crossPoint

则添加crossPoint到pointArr中,并且

linkArr中对应的link:{id,data:[a,b]}删除,并创建两个新的link加入到linkArr中,分别为:{id,data:[a,crossPoint]}、{id,data:[crossPoint,b]}

3.遍历完link并收集所有的 crossPoint

可以根据到M的距离由近到远排序得到[ M, crossPoint0,crossPoint1,crossPoint2,... N],即使一个crossPoint也没有也得到[M,N]。

从M开始每相邻两个点创建一个新的link并加入linkArr中

直接鼠标右键会 清空focus

按下鼠标拖动可以调整点的位置

如果focus不为空,按下backspace 可以删除对应的link 或者 point

默认用黑色绘制点,用灰色绘制线

如果focus.t ==='link' 对应的线和点用红色绘制

如果focus.t ==='point' 对应的点用红色绘制

相关推荐
m0_502724952 小时前
Arco design vue 阻止弹窗关闭
javascript·vue.js·arco design
xifangge20252 小时前
Python 爬虫实战:爬取豆瓣电影 Top250 数据并进行可视化分析
开发语言·爬虫·python
蜡台2 小时前
Uniapp 实现 二手车价格评估 功能
前端·javascript·uni-app·估值·汽车抵押·二手车评估
SunnyDays10112 小时前
C# 实战:快速查找并高亮 Word 文档中的文字(普通查找 + 正则表达式)
开发语言·c#
Yan-英杰2 小时前
TypeScript+React 全栈生态实战:从架构选型到工程落地,告别开发踩坑
javascript·学习·typescript
kaoshi100app2 小时前
本周,河南二建报名公布!
开发语言·人工智能·职场和发展·学习方法
海天鹰2 小时前
JSZip库读取ePub电子书目录
javascript
421!2 小时前
ESP32学习笔记之GPIO
开发语言·笔记·单片机·嵌入式硬件·学习·算法·fpga开发
比特森林探险记2 小时前
Element Plus 实战指南
前端·javascript