| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907 |
- <!DOCTYPE html>
- <html lang="zh">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>六边形拓扑 A* 寻路演示 (Hexagonal Topology A* Pathfinding)</title>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- height: 100vh;
- overflow: hidden;
- padding: 10px;
- }
- .container {
- max-width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- }
- h1 {
- color: white;
- text-align: center;
- margin-bottom: 2px;
- font-size: 1.3em;
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
- }
- .subtitle {
- color: rgba(255, 255, 255, 0.9);
- text-align: center;
- margin-bottom: 3px;
- font-size: 0.65em;
- }
- .info-panel {
- background: white;
- border-radius: 8px;
- padding: 4px 8px;
- margin-bottom: 6px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- width: 100%;
- display: flex;
- flex-direction: column;
- }
- .info-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 4px;
- margin-bottom: 4px;
- }
- .info-card {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- color: white;
- padding: 3px 6px;
- border-radius: 5px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- }
- .info-card h3 {
- margin-bottom: 1px;
- font-size: 0.65em;
- }
- .info-card p {
- font-size: 0.55em;
- line-height: 1.1;
- }
- .controls {
- display: flex;
- justify-content: center;
- gap: 10px;
- align-items: center;
- background: rgba(255, 255, 255, 0.95);
- padding: 8px 10px;
- border-radius: 6px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
- width: 100%;
- flex-wrap: wrap;
- }
- .status {
- font-weight: 500;
- color: #fa0202;
- font-size: 0.75em;
- }
- .status-bar {
- background: rgba(255, 255, 255, 0.95);
- padding: 4px 10px;
- border-radius: 6px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
- width: 100%;
- margin-bottom: 4px;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- button {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- padding: 8px 20px;
- border-radius: 20px;
- font-size: 0.9em;
- cursor: pointer;
- transition: all 0.3s;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- font-weight: 500;
- }
- button:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- }
- button:active {
- transform: translateY(0);
- }
- button:disabled {
- background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%);
- cursor: not-allowed;
- transform: none;
- }
- button.active {
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
- box-shadow: 0 4px 12px rgba(245, 158, 11, 0.5);
- }
- .canvas-wrapper {
- background: white;
- border-radius: 10px;
- padding: 10px;
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
- }
- .canvas-container {
- overflow: auto;
- border: 2px solid #e2e8f0;
- border-radius: 8px;
- background: #f8fafc;
- flex: 1;
- }
- canvas {
- display: block;
- cursor: crosshair;
- }
- .legend {
- display: flex;
- justify-content: center;
- gap: 12px;
- margin-top: 8px;
- flex-wrap: wrap;
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 6px;
- background: rgba(255, 255, 255, 0.9);
- padding: 4px 10px;
- border-radius: 15px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
- font-size: 0.75em;
- }
- .legend-color {
- width: 18px;
- height: 18px;
- border-radius: 3px;
- border: 1.5px solid #333;
- }
- .instructions {
- background: rgba(102, 126, 234, 0.15);
- padding: 3px 6px;
- border-radius: 5px;
- }
- .instructions h4 {
- color: #1e293b;
- margin-bottom: 1px;
- font-size: 0.65em;
- }
- .instructions ul {
- list-style: none;
- padding-left: 0;
- margin: 0;
- }
- .instructions li {
- color: #1e293b;
- font-size: 0.55em;
- padding-left: 8px;
- position: relative;
- line-height: 1.2;
- }
- .instructions li:before {
- content: "▸";
- position: absolute;
- left: 0;
- color: #667eea;
- font-weight: bold;
- }
- /* Toast 提示框样式 */
- .toast {
- position: fixed;
- top: 25%;
- left: 50%;
- transform: translate(-50%, -50%) scale(0.8);
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 20px 40px;
- border-radius: 12px;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
- font-size: 1.1em;
- font-weight: 500;
- z-index: 10000;
- opacity: 0;
- transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
- min-width: 250px;
- text-align: center;
- pointer-events: none;
- }
- .toast.show {
- opacity: 1;
- transform: translate(-50%, -50%) scale(1);
- }
- .toast.success {
- background: linear-gradient(135deg, #10b981 0%, #059669 100%);
- }
- .toast.error {
- background: linear-gradient(135deg, #f43f5e 0%, #dc2626 100%);
- }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>🎮 六边形拓扑 A* 寻路演示</h1>
- <p class="subtitle">Hexagonal Topology A* Pathfinding - 菱形格子 + 六方向移动</p>
- <div class="info-panel">
- <div class="info-grid">
- <div class="info-card">
- <h3>📐 网格类型</h3>
- <p><strong>列交错六边形</strong><br>
- 偶数列偏移形成六边形拓扑</p>
- </div>
- <div class="info-card">
- <h3>🎯 6方向移动</h3>
- <p><strong>6个邻居</strong>上下、左上左下、右上右下</p>
- </div>
- <div class="info-card">
- <h3>💎 菱形渲染</h3>
- <p><strong>等距投影</strong>45度旋转的美观展示</p>
- </div>
- <div class="instructions">
- <h4>📖 使用说明</h4>
- <ul>
- <li>左键:起点 | 右键:终点 | 按钮:寻路 | 规格:100x100网格</li>
- </ul>
- </div>
- </div>
- <div class="controls">
- <button id="findPathBtn">(老寻路)</button>
- <button id="findNewPathBtn">(新寻路)</button>
- <button id="copyPathBtn" disabled>复制路径</button>
- <button id="randomObstaclesBtn">随机生成障碍点</button>
- <button id="editObstaclesBtn">手动设置障碍点</button>
- <button id="toggleCoordsBtn">显示坐标</button>
- </div>
- <div class="status-bar">
- <div class="status" id="statusText">Ready - 左键设置起点,右键设置终点</div>
- </div>
- </div>
- <div class="canvas-wrapper">
- <div class="canvas-container">
- <canvas id="gridCanvas"></canvas>
- </div>
- <div class="legend">
- <div class="legend-item">
- <div class="legend-color" style="background: #10b981;"></div>
- <span>起点 (Start)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background: #f43f5e;"></div>
- <span>终点 (End)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background: #3b82f6;"></div>
- <span>路径 (Path)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background: #334155;"></div>
- <span>障碍点 (Obstacle)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background: rgba(245, 158, 11, 0.3);"></div>
- <span>偶数列 (Even Columns)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background: rgba(139, 92, 246, 0.3);"></div>
- <span>奇数列 (Odd Columns)</span>
- </div>
- </div>
- </div>
- </div>
- <script>
- const canvas = document.getElementById('gridCanvas');
- const ctx = canvas.getContext('2d', { alpha: false });
- const findPathBtn = document.getElementById('findPathBtn');
- const findNewPathBtn = document.getElementById('findNewPathBtn');
- const copyPathBtn = document.getElementById('copyPathBtn');
- const statusText = document.getElementById('statusText');
- // Config
- const GRID_SIZE = 100;
- // Tight-packed hexagonal grid dimensions
- // For proper hexagonal packing: HEIGHT = WIDTH * sqrt(3)/2
- const TILE_WIDTH = 30;
- const TILE_HEIGHT = TILE_WIDTH * Math.sqrt(3) / 2; // 精确值 ≈ 25.98076
- // Canvas size: need to accommodate full grid range
- // Max X range: 0 to GRID_SIZE-1
- // Max Y range: 0 to GRID_SIZE-1
- // Screen X range: (0+0) to (99+99) = 0 to 198 half-widths
- // Screen Y range: (0-99) to (99-0) = -99 to 99 half-heights + colOffset
- const CANVAS_WIDTH = (GRID_SIZE * 2) * (TILE_WIDTH / 2) + TILE_WIDTH * 2;
- const CANVAS_HEIGHT = (GRID_SIZE * 2) * (TILE_HEIGHT / 2) + TILE_HEIGHT * 4;
- const OFFSET_X = TILE_WIDTH * 2;
- const OFFSET_Y = CANVAS_HEIGHT / 2;
- // Styles
- const COLORS = {
- bg: '#f8fafc',
- grid: '#94a3b8',
- start: '#10b981',
- end: '#f43f5e',
- path: '#3b82f6',
- obstacle: '#334155' // 障碍点颜色
- };
- // State
- let startPoint = null;
- let endPoint = null;
- let path = [];
- let obstacles = []; // 障碍点列表
- let showCoordinates = false;
- let isEditingObstacles = false; // 是否处于编辑障碍点模式
- // Toast 提示框功能
- // 功能:显示一个从底部向上漂浮的5秒后消失的提示框
- // 参数:message - 提示消息,type - 类型('success', 'error', 'info')
- function showToast(message, type = 'info') {
- // 创建 toast 元素
- const toast = document.createElement('div');
- toast.className = `toast ${type}`;
- toast.textContent = message;
- document.body.appendChild(toast);
- // 延迟显示,触发动画
- setTimeout(() => {
- toast.classList.add('show');
- }, 50);
- // 5秒后隐藏并移除
- setTimeout(() => {
- toast.classList.remove('show');
- setTimeout(() => {
- document.body.removeChild(toast);
- }, 200); // 等待动画完成后移除
- }, 2000);
- }
- // Setup Canvas
- canvas.width = CANVAS_WIDTH;
- canvas.height = CANVAS_HEIGHT;
- // Isometric Grid to Screen
- function gridToScreen(x, y) {
- // Offset so (1,1) is at the westernmost point
- const offsetX = x - 1;
- const offsetY = y - 1;
- // Column stagger: EVEN X columns shift
- if (x % 2 === 0) {
- // 向东北45度移动1.5格 = 在等距投影中 X方向+1.5, Y方向不变
- // 等距投影: screenX = (gridX + gridY) * W/2
- // 向东北移动1.5格: gridY += 1.5
- const neOffset = 1.5;
- // 垂直偏移(向下)
- const colOffsetY = TILE_HEIGHT / 2;
- // 水平偏移(向左,增加偏移量)
- const colOffsetX = -TILE_WIDTH / 2;
- // 东北偏移(向右上)
- const neOffsetX = neOffset * (TILE_WIDTH / 2);
- const neOffsetY = -neOffset * (TILE_HEIGHT / 2);
- const screenX = (offsetX + offsetY) * (TILE_WIDTH / 2) + OFFSET_X + colOffsetX + neOffsetX;
- const screenY = (offsetX - offsetY) * (TILE_HEIGHT / 2) + OFFSET_Y + colOffsetY + neOffsetY;
- return { x: screenX, y: screenY };
- } else {
- // 奇数列保持原位
- const screenX = (offsetX + offsetY) * (TILE_WIDTH / 2) + OFFSET_X;
- const screenY = (offsetX - offsetY) * (TILE_HEIGHT / 2) + OFFSET_Y;
- return { x: screenX, y: screenY };
- }
- }
- // Screen to Grid (approximate)
- function screenToGrid(sx, sy) {
- // Inverse isometric transformation (Y-inverted)
- const tempX = (sx - OFFSET_X) / (TILE_WIDTH / 2);
- const tempY = (sy - OFFSET_Y) / (TILE_HEIGHT / 2);
- let approxX = Math.round((tempX + tempY) / 2);
- let approxY = Math.round((tempX - tempY) / 2);
- // Distance-based refinement
- let minDist = Infinity;
- let closest = null;
- for (let dx = -2; dx <= 2; dx++) {
- for (let dy = -2; dy <= 2; dy++) {
- const cx = approxX + dx;
- const cy = approxY + dy;
- if (cx < 0 || cx >= GRID_SIZE || cy < 0 || cy >= GRID_SIZE) continue;
- const pos = gridToScreen(cx, cy);
- const dist = (sx - pos.x) ** 2 + (sy - pos.y) ** 2;
- if (dist < minDist) {
- minDist = dist;
- closest = { x: cx, y: cy };
- }
- }
- }
- return closest || { x: 0, y: 0 };
- }
- function drawDiamond(x, y, color) {
- const pos = gridToScreen(x, y);
- const HW = TILE_WIDTH / 2;
- const HH = TILE_HEIGHT / 2;
- ctx.beginPath();
- ctx.moveTo(pos.x, pos.y - HH); // Top
- ctx.lineTo(pos.x + HW, pos.y); // Right
- ctx.lineTo(pos.x, pos.y + HH); // Bottom
- ctx.lineTo(pos.x - HW, pos.y); // Left
- ctx.closePath();
- ctx.fillStyle = color;
- ctx.fill();
- ctx.strokeStyle = COLORS.grid;
- ctx.lineWidth = 0.5;
- ctx.stroke();
- }
- function clearCanvas() {
- ctx.fillStyle = COLORS.bg;
- ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
- // Draw all grid cells with column highlighting
- for (let y = 1; y <= GRID_SIZE; y++) {
- for (let x = 1; x <= GRID_SIZE; x++) {
- const pos = gridToScreen(x, y);
- const HW = TILE_WIDTH / 2;
- const HH = TILE_HEIGHT / 2;
- // Column highlighting: even columns (orange tint), odd columns (purple tint)
- let fillColor = (x % 2 === 0) ? 'rgba(245, 158, 11, 0.1)' : 'rgba(139, 92, 246, 0.1)';
- ctx.beginPath();
- ctx.moveTo(pos.x, pos.y - HH);
- ctx.lineTo(pos.x + HW, pos.y);
- ctx.lineTo(pos.x, pos.y + HH);
- ctx.lineTo(pos.x - HW, pos.y);
- ctx.closePath();
- ctx.fillStyle = fillColor;
- ctx.fill();
- ctx.strokeStyle = COLORS.grid;
- ctx.lineWidth = 0.5;
- ctx.stroke();
- }
- }
- // Draw coordinate labels based on showCoordinates flag
- if (showCoordinates) {
- ctx.fillStyle = '#1e293b';
- ctx.font = '6px monospace';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- for (let y = 1; y <= GRID_SIZE; y++) {
- for (let x = 1; x <= GRID_SIZE; x++) {
- const pos = gridToScreen(x, y);
- ctx.fillText(`${x},${y}`, pos.x, pos.y);
- }
- }
- }
- }
- function getNeighbors(x, y) {
- const isEvenCol = (x % 2 === 0);
- let dirs;
- if (isEvenCol) {
- // Even columns (shifted down)
- dirs = [
- { dx: 0, dy: -1, label: '4' }, // Up
- { dx: 0, dy: 1, label: '1' }, // Down
- { dx: 1, dy: 0, label: '3' }, // Right-Up
- { dx: 1, dy: 1, label: '2' }, // Right-Down
- { dx: -1, dy: 0, label: '5' }, // Left-Up
- { dx: -1, dy: 1, label: '6' } // Left-Down
- ];
- } else {
- // Odd columns (base position)
- dirs = [
- { dx: 0, dy: -1, label: '4' }, // Up
- { dx: 0, dy: 1, label: '1' }, // Down
- { dx: 1, dy: -1, label: '3' }, // Right-Up
- { dx: 1, dy: 0, label: '2' }, // Right-Down
- { dx: -1, dy: -1, label: '5' }, // Left-Up
- { dx: -1, dy: 0, label: '6' } // Left-Down
- ];
- }
- return dirs.map(d => ({
- x: x + d.dx,
- y: y + d.dy,
- label: d.label
- })).filter(n => n.x >= 0 && n.x < GRID_SIZE && n.y >= 0 && n.y < GRID_SIZE);
- }
- function redraw() {
- clearCanvas();
- // Draw Obstacles - 绘制障碍点
- if (obstacles.length > 0) {
- for (const obs of obstacles) {
- drawDiamond(obs.x, obs.y, COLORS.obstacle);
- }
- }
- // Draw Path as connected lines between cell centers
- if (path.length > 0) {
- ctx.strokeStyle = COLORS.path;
- ctx.lineWidth = 2.5;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.beginPath();
- const firstPos = gridToScreen(path[0].x, path[0].y);
- ctx.moveTo(firstPos.x, firstPos.y);
- for (let i = 1; i < path.length; i++) {
- const pos = gridToScreen(path[i].x, path[i].y);
- ctx.lineTo(pos.x, pos.y);
- }
- ctx.stroke();
- }
- // Draw Start
- if (startPoint) {
- drawDiamond(startPoint.x, startPoint.y, COLORS.start);
- }
- // Draw End
- if (endPoint) {
- drawDiamond(endPoint.x, endPoint.y, COLORS.end);
- }
- }
- function drawLabel(x, y, text) {
- const pos = gridToScreen(x, y);
- ctx.fillStyle = '#1e293b';
- ctx.font = 'bold 12px Inter, sans-serif';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(text, pos.x, pos.y);
- }
- // Interaction
- function getGridPos(evt) {
- const rect = canvas.getBoundingClientRect();
- const scaleX = canvas.width / rect.width;
- const scaleY = canvas.height / rect.height;
- const clickX = (evt.clientX - rect.left) * scaleX;
- const clickY = (evt.clientY - rect.top) * scaleY;
- return screenToGrid(clickX, clickY);
- }
- // 鼠标点击事件处理
- // 功能:根据当前模式设置起点、终点或编辑障碍点
- // 左键:设置起点,右键:设置终点,点击时坐标立即更新
- canvas.addEventListener('mousedown', (e) => {
- const pos = getGridPos(e);
- if (pos.x < 0 || pos.x >= GRID_SIZE || pos.y < 0 || pos.y >= GRID_SIZE) return;
- // 如果处于编辑障碍点模式
- if (isEditingObstacles) {
- const key = `${pos.x},${pos.y}`;
- const obstacleIndex = obstacles.findIndex(obs => obs.x === pos.x && obs.y === pos.y);
- if (obstacleIndex !== -1) {
- // 已存在障碍点,删除它
- obstacles.splice(obstacleIndex, 1);
- statusText.textContent = `删除障碍点 (${pos.x}, ${pos.y})`;
- } else {
- // 不是起点和终点,添加障碍点
- const isStartOrEnd = (startPoint && startPoint.x === pos.x && startPoint.y === pos.y) ||
- (endPoint && endPoint.x === pos.x && endPoint.y === pos.y);
- if (!isStartOrEnd) {
- obstacles.push({ x: pos.x, y: pos.y });
- statusText.textContent = `添加障碍点 (${pos.x}, ${pos.y})`;
- } else {
- showToast('不能在起点或终点设置障碍点', 'error');
- }
- }
- // 清空当前路径
- path = [];
- copyPathBtn.disabled = true;
- redraw();
- } else {
- // 普通模式:设置起点和终点
- if (e.button === 0) {
- startPoint = pos;
- statusText.textContent = `起点: (${pos.x}, ${pos.y})`;
- } else if (e.button === 2) {
- e.preventDefault();
- endPoint = pos;
- statusText.textContent = `终点: (${pos.x}, ${pos.y})`;
- }
- path = [];
- copyPathBtn.disabled = true;
- redraw();
- }
- });
- canvas.addEventListener('contextmenu', (e) => e.preventDefault());
- // 通用寻路函数
- async function performPathfinding(endpoint, buttonElement) {
- if (!startPoint || !endPoint) {
- showToast('请先设置起点(左键)和终点(右键)', 'error');
- return;
- }
- buttonElement.disabled = true;
- statusText.textContent = "Finding path...";
- try {
- const response = await fetch(endpoint, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- startX: startPoint.x,
- startY: startPoint.y,
- endX: endPoint.x,
- endY: endPoint.y,
- obstacles: obstacles // 传递障碍点列表
- })
- });
- if (!response.ok) {
- const err = await response.json();
- throw new Error(err.error || 'Unknown error');
- }
- const data = await response.json();
- if (data.error) {
- statusText.textContent = `Error: ${data.error}`;
- showToast(`错误: ${data.error}`, 'error');
- } else if (data.path && data.path.length > 0) {
- path = data.path;
- redraw();
- // 显示寻路成功提示
- showToast(`寻路成功!耗时:${data.costTime}ms, 路径长度: ${path.length}`, 'success');
- statusText.textContent = 'Ready - 左键设置起点,右键设置终点';
- copyPathBtn.disabled = false;
- } else {
- statusText.textContent = "No path found.";
- showToast('未找到路径!', 'error');
- copyPathBtn.disabled = true;
- }
- } catch (err) {
- console.error(err);
- statusText.textContent = "Error finding path.";
- showToast(`寻路错误: ${err.message}`, 'error');
- copyPathBtn.disabled = true; // 出错时禁用复制按钮
- } finally {
- buttonElement.disabled = false;
- // 注意:不要在这里无条件禁用copyPathBtn,因为它应该在成功时保持启用
- }
- }
- findPathBtn.addEventListener('click', async () => {
- await performPathfinding('/find-path', findPathBtn);
- });
- // 新寻路按钮事件处理 - 使用 astar6 算法
- findNewPathBtn.addEventListener('click', async () => {
- await performPathfinding('/find-path-astar6', findNewPathBtn);
- });
- // Toggle coordinates button
- const toggleCoordsBtn = document.getElementById('toggleCoordsBtn');
- toggleCoordsBtn.addEventListener('click', () => {
- showCoordinates = !showCoordinates;
- toggleCoordsBtn.textContent = showCoordinates ? '隐藏坐标' : '显示坐标';
- redraw();
- });
- // Copy path button - 复制路径按钮点击事件处理
- // 功能:将路径点列表转换为 {x,y} 格式并复制到剪贴板
- copyPathBtn.addEventListener('click', () => {
- if (path.length > 0) {
- // Convert path to string format: "{x1,y1} {x2,y2} {x3,y3} ..."
- const pathString = path.map(point => `{${point.x},${point.y}}`).join(' ');
- navigator.clipboard.writeText(pathString).then(() => {
- // 显示成功提示
- showToast('路径已复制到剪贴板!', 'success');
- }).catch(err => {
- console.error('Failed to copy path: ', err);
- showToast('复制失败,请手动选择复制', 'error');
- // Fallback: select text in alert
- alert(`复制失败,请手动复制以下内容:\n${pathString}`);
- });
- }
- });
- // Random obstacles button - 随机生成障碍点按钮点击事件处理
- // 功能:弹窗输入障碍点数量,生成随机障碍点并绘制
- const randomObstaclesBtn = document.getElementById('randomObstaclesBtn');
- randomObstaclesBtn.addEventListener('click', () => {
- const countStr = prompt('请输入要生成的障碍点数量(1-1000):', '50');
- if (countStr === null) return; // 用户取消
- const count = parseInt(countStr, 10);
- if (isNaN(count) || count < 1 || count > 1000) {
- showToast('请输入有效的数量(1-1000)', 'error');
- return;
- }
- // 生成随机障碍点
- obstacles = [];
- const existingPoints = new Set();
- // 添加起点和终点到已存在点集合,避免在这些位置生成障碍
- if (startPoint) existingPoints.add(`${startPoint.x},${startPoint.y}`);
- if (endPoint) existingPoints.add(`${endPoint.x},${endPoint.y}`);
- let attempts = 0;
- const maxAttempts = count * 10; // 最大尝试次数,避免死循环
- while (obstacles.length < count && attempts < maxAttempts) {
- const x = Math.floor(Math.random() * GRID_SIZE) + 1;
- const y = Math.floor(Math.random() * GRID_SIZE) + 1;
- const key = `${x},${y}`;
- if (!existingPoints.has(key)) {
- obstacles.push({ x, y });
- existingPoints.add(key);
- }
- attempts++;
- }
- // 清空当前路径
- path = [];
- copyPathBtn.disabled = true;
- redraw();
- showToast(`已生成 ${obstacles.length} 个障碍点`, 'success');
- statusText.textContent = 'Ready - 左键设置起点,右键设置终点';
- });
- // Edit obstacles button - 手动设置障碍点按钮点击事件处理
- // 功能:切换编辑障碍点模式,允许用户点击地图添加或删除障碍点
- const editObstaclesBtn = document.getElementById('editObstaclesBtn');
- editObstaclesBtn.addEventListener('click', () => {
- isEditingObstacles = !isEditingObstacles;
- if (isEditingObstacles) {
- editObstaclesBtn.classList.add('active');
- statusText.textContent = '编辑模式:点击地图添加/删除障碍点';
- showToast('已进入编辑障碍点模式', 'success');
- } else {
- editObstaclesBtn.classList.remove('active');
- statusText.textContent = 'Ready - 左键设置起点,右键设置终点';
- showToast('已退出编辑障碍点模式', 'info');
- }
- });
- // Initial draw
- redraw();
- // Mouse hover tooltip for coordinates
- let tooltip = null;
- function createTooltip() {
- if (!tooltip) {
- tooltip = document.createElement('div');
- tooltip.style.cssText = `
- position: absolute;
- background: rgba(30, 41, 59, 0.95);
- color: white;
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 12px;
- pointer-events: none;
- z-index: 1000;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- `;
- document.body.appendChild(tooltip);
- }
- }
- function showTooltip(x, y, text) {
- createTooltip();
- tooltip.textContent = text;
- tooltip.style.left = x + 'px';
- tooltip.style.top = y + 'px';
- tooltip.style.display = 'block';
- }
- function hideTooltip() {
- if (tooltip) {
- tooltip.style.display = 'none';
- }
- }
- canvas.addEventListener('mousemove', (e) => {
- const rect = canvas.getBoundingClientRect();
- const scaleX = canvas.width / rect.width;
- const scaleY = canvas.height / rect.height;
- const mouseX = (e.clientX - rect.left) * scaleX;
- const mouseY = (e.clientY - rect.top) * scaleY;
- const gridPos = screenToGrid(mouseX, mouseY);
- if (gridPos.x >= 0 && gridPos.x < GRID_SIZE && gridPos.y >= 0 && gridPos.y < GRID_SIZE) {
- showTooltip(e.clientX + 10, e.clientY + 10, `(${gridPos.x}, ${gridPos.y})`);
- } else {
- hideTooltip();
- }
- });
- canvas.addEventListener('mouseleave', hideTooltip);
- // Auto-scroll to (1,1) position on load
- window.addEventListener('load', () => {
- const container = document.querySelector('.canvas-container');
- const pos = gridToScreen(1, 1);
- container.scrollLeft = pos.x - container.clientWidth / 2;
- container.scrollTop = pos.y - container.clientHeight / 2;
- });
- </script>
- </body>
- </html>
|