├── index.html ├── lib └── delaunator.js └── src └── main.js /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 2d navmesh 9 | 65 | 66 | 67 |
68 | 69 |
70 |
71 |
72 |
起终点
73 |
74 |
75 |
76 |
网格路径
77 |
78 |
79 |
80 |
边中点路径
81 |
82 |
83 |
84 |
公共边
85 |
86 |
87 |
88 |
平滑路径
89 |
90 |
91 |
92 |
点击设置起点
93 |
滑动设置终点
94 |
95 |
96 | 97 | 98 | -------------------------------------------------------------------------------- /lib/delaunator.js: -------------------------------------------------------------------------------- 1 | /** delaunator 2 | * Github:https://github.com/mapbox/delaunator 3 | * */ 4 | (function (global, factory) { 5 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 6 | typeof define === 'function' && define.amd ? define(factory) : 7 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Delaunator = factory()); 8 | }(this, (function () { 'use strict'; 9 | 10 | const epsilon = 1.1102230246251565e-16; 11 | const splitter = 134217729; 12 | const resulterrbound = (3 + 8 * epsilon) * epsilon; 13 | 14 | // fast_expansion_sum_zeroelim routine from oritinal code 15 | function sum(elen, e, flen, f, h) { 16 | let Q, Qnew, hh, bvirt; 17 | let enow = e[0]; 18 | let fnow = f[0]; 19 | let eindex = 0; 20 | let findex = 0; 21 | if ((fnow > enow) === (fnow > -enow)) { 22 | Q = enow; 23 | enow = e[++eindex]; 24 | } else { 25 | Q = fnow; 26 | fnow = f[++findex]; 27 | } 28 | let hindex = 0; 29 | if (eindex < elen && findex < flen) { 30 | if ((fnow > enow) === (fnow > -enow)) { 31 | Qnew = enow + Q; 32 | hh = Q - (Qnew - enow); 33 | enow = e[++eindex]; 34 | } else { 35 | Qnew = fnow + Q; 36 | hh = Q - (Qnew - fnow); 37 | fnow = f[++findex]; 38 | } 39 | Q = Qnew; 40 | if (hh !== 0) { 41 | h[hindex++] = hh; 42 | } 43 | while (eindex < elen && findex < flen) { 44 | if ((fnow > enow) === (fnow > -enow)) { 45 | Qnew = Q + enow; 46 | bvirt = Qnew - Q; 47 | hh = Q - (Qnew - bvirt) + (enow - bvirt); 48 | enow = e[++eindex]; 49 | } else { 50 | Qnew = Q + fnow; 51 | bvirt = Qnew - Q; 52 | hh = Q - (Qnew - bvirt) + (fnow - bvirt); 53 | fnow = f[++findex]; 54 | } 55 | Q = Qnew; 56 | if (hh !== 0) { 57 | h[hindex++] = hh; 58 | } 59 | } 60 | } 61 | while (eindex < elen) { 62 | Qnew = Q + enow; 63 | bvirt = Qnew - Q; 64 | hh = Q - (Qnew - bvirt) + (enow - bvirt); 65 | enow = e[++eindex]; 66 | Q = Qnew; 67 | if (hh !== 0) { 68 | h[hindex++] = hh; 69 | } 70 | } 71 | while (findex < flen) { 72 | Qnew = Q + fnow; 73 | bvirt = Qnew - Q; 74 | hh = Q - (Qnew - bvirt) + (fnow - bvirt); 75 | fnow = f[++findex]; 76 | Q = Qnew; 77 | if (hh !== 0) { 78 | h[hindex++] = hh; 79 | } 80 | } 81 | if (Q !== 0 || hindex === 0) { 82 | h[hindex++] = Q; 83 | } 84 | return hindex; 85 | } 86 | 87 | function estimate(elen, e) { 88 | let Q = e[0]; 89 | for (let i = 1; i < elen; i++) Q += e[i]; 90 | return Q; 91 | } 92 | 93 | function vec(n) { 94 | return new Float64Array(n); 95 | } 96 | 97 | const ccwerrboundA = (3 + 16 * epsilon) * epsilon; 98 | const ccwerrboundB = (2 + 12 * epsilon) * epsilon; 99 | const ccwerrboundC = (9 + 64 * epsilon) * epsilon * epsilon; 100 | 101 | const B = vec(4); 102 | const C1 = vec(8); 103 | const C2 = vec(12); 104 | const D = vec(16); 105 | const u = vec(4); 106 | 107 | function orient2dadapt(ax, ay, bx, by, cx, cy, detsum) { 108 | let acxtail, acytail, bcxtail, bcytail; 109 | let bvirt, c, ahi, alo, bhi, blo, _i, _j, _0, s1, s0, t1, t0, u3; 110 | 111 | const acx = ax - cx; 112 | const bcx = bx - cx; 113 | const acy = ay - cy; 114 | const bcy = by - cy; 115 | 116 | s1 = acx * bcy; 117 | c = splitter * acx; 118 | ahi = c - (c - acx); 119 | alo = acx - ahi; 120 | c = splitter * bcy; 121 | bhi = c - (c - bcy); 122 | blo = bcy - bhi; 123 | s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo); 124 | t1 = acy * bcx; 125 | c = splitter * acy; 126 | ahi = c - (c - acy); 127 | alo = acy - ahi; 128 | c = splitter * bcx; 129 | bhi = c - (c - bcx); 130 | blo = bcx - bhi; 131 | t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo); 132 | _i = s0 - t0; 133 | bvirt = s0 - _i; 134 | B[0] = s0 - (_i + bvirt) + (bvirt - t0); 135 | _j = s1 + _i; 136 | bvirt = _j - s1; 137 | _0 = s1 - (_j - bvirt) + (_i - bvirt); 138 | _i = _0 - t1; 139 | bvirt = _0 - _i; 140 | B[1] = _0 - (_i + bvirt) + (bvirt - t1); 141 | u3 = _j + _i; 142 | bvirt = u3 - _j; 143 | B[2] = _j - (u3 - bvirt) + (_i - bvirt); 144 | B[3] = u3; 145 | 146 | let det = estimate(4, B); 147 | let errbound = ccwerrboundB * detsum; 148 | if (det >= errbound || -det >= errbound) { 149 | return det; 150 | } 151 | 152 | bvirt = ax - acx; 153 | acxtail = ax - (acx + bvirt) + (bvirt - cx); 154 | bvirt = bx - bcx; 155 | bcxtail = bx - (bcx + bvirt) + (bvirt - cx); 156 | bvirt = ay - acy; 157 | acytail = ay - (acy + bvirt) + (bvirt - cy); 158 | bvirt = by - bcy; 159 | bcytail = by - (bcy + bvirt) + (bvirt - cy); 160 | 161 | if (acxtail === 0 && acytail === 0 && bcxtail === 0 && bcytail === 0) { 162 | return det; 163 | } 164 | 165 | errbound = ccwerrboundC * detsum + resulterrbound * Math.abs(det); 166 | det += (acx * bcytail + bcy * acxtail) - (acy * bcxtail + bcx * acytail); 167 | if (det >= errbound || -det >= errbound) return det; 168 | 169 | s1 = acxtail * bcy; 170 | c = splitter * acxtail; 171 | ahi = c - (c - acxtail); 172 | alo = acxtail - ahi; 173 | c = splitter * bcy; 174 | bhi = c - (c - bcy); 175 | blo = bcy - bhi; 176 | s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo); 177 | t1 = acytail * bcx; 178 | c = splitter * acytail; 179 | ahi = c - (c - acytail); 180 | alo = acytail - ahi; 181 | c = splitter * bcx; 182 | bhi = c - (c - bcx); 183 | blo = bcx - bhi; 184 | t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo); 185 | _i = s0 - t0; 186 | bvirt = s0 - _i; 187 | u[0] = s0 - (_i + bvirt) + (bvirt - t0); 188 | _j = s1 + _i; 189 | bvirt = _j - s1; 190 | _0 = s1 - (_j - bvirt) + (_i - bvirt); 191 | _i = _0 - t1; 192 | bvirt = _0 - _i; 193 | u[1] = _0 - (_i + bvirt) + (bvirt - t1); 194 | u3 = _j + _i; 195 | bvirt = u3 - _j; 196 | u[2] = _j - (u3 - bvirt) + (_i - bvirt); 197 | u[3] = u3; 198 | const C1len = sum(4, B, 4, u, C1); 199 | 200 | s1 = acx * bcytail; 201 | c = splitter * acx; 202 | ahi = c - (c - acx); 203 | alo = acx - ahi; 204 | c = splitter * bcytail; 205 | bhi = c - (c - bcytail); 206 | blo = bcytail - bhi; 207 | s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo); 208 | t1 = acy * bcxtail; 209 | c = splitter * acy; 210 | ahi = c - (c - acy); 211 | alo = acy - ahi; 212 | c = splitter * bcxtail; 213 | bhi = c - (c - bcxtail); 214 | blo = bcxtail - bhi; 215 | t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo); 216 | _i = s0 - t0; 217 | bvirt = s0 - _i; 218 | u[0] = s0 - (_i + bvirt) + (bvirt - t0); 219 | _j = s1 + _i; 220 | bvirt = _j - s1; 221 | _0 = s1 - (_j - bvirt) + (_i - bvirt); 222 | _i = _0 - t1; 223 | bvirt = _0 - _i; 224 | u[1] = _0 - (_i + bvirt) + (bvirt - t1); 225 | u3 = _j + _i; 226 | bvirt = u3 - _j; 227 | u[2] = _j - (u3 - bvirt) + (_i - bvirt); 228 | u[3] = u3; 229 | const C2len = sum(C1len, C1, 4, u, C2); 230 | 231 | s1 = acxtail * bcytail; 232 | c = splitter * acxtail; 233 | ahi = c - (c - acxtail); 234 | alo = acxtail - ahi; 235 | c = splitter * bcytail; 236 | bhi = c - (c - bcytail); 237 | blo = bcytail - bhi; 238 | s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo); 239 | t1 = acytail * bcxtail; 240 | c = splitter * acytail; 241 | ahi = c - (c - acytail); 242 | alo = acytail - ahi; 243 | c = splitter * bcxtail; 244 | bhi = c - (c - bcxtail); 245 | blo = bcxtail - bhi; 246 | t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo); 247 | _i = s0 - t0; 248 | bvirt = s0 - _i; 249 | u[0] = s0 - (_i + bvirt) + (bvirt - t0); 250 | _j = s1 + _i; 251 | bvirt = _j - s1; 252 | _0 = s1 - (_j - bvirt) + (_i - bvirt); 253 | _i = _0 - t1; 254 | bvirt = _0 - _i; 255 | u[1] = _0 - (_i + bvirt) + (bvirt - t1); 256 | u3 = _j + _i; 257 | bvirt = u3 - _j; 258 | u[2] = _j - (u3 - bvirt) + (_i - bvirt); 259 | u[3] = u3; 260 | const Dlen = sum(C2len, C2, 4, u, D); 261 | 262 | return D[Dlen - 1]; 263 | } 264 | 265 | function orient2d(ax, ay, bx, by, cx, cy) { 266 | const detleft = (ay - cy) * (bx - cx); 267 | const detright = (ax - cx) * (by - cy); 268 | const det = detleft - detright; 269 | 270 | if (detleft === 0 || detright === 0 || (detleft > 0) !== (detright > 0)) return det; 271 | 272 | const detsum = Math.abs(detleft + detright); 273 | if (Math.abs(det) >= ccwerrboundA * detsum) return det; 274 | 275 | return -orient2dadapt(ax, ay, bx, by, cx, cy, detsum); 276 | } 277 | 278 | const EPSILON = Math.pow(2, -52); 279 | const EDGE_STACK = new Uint32Array(512); 280 | 281 | class Delaunator { 282 | 283 | static from(points, getX = defaultGetX, getY = defaultGetY) { 284 | const n = points.length; 285 | const coords = new Float64Array(n * 2); 286 | 287 | for (let i = 0; i < n; i++) { 288 | const p = points[i]; 289 | coords[2 * i] = getX(p); 290 | coords[2 * i + 1] = getY(p); 291 | } 292 | 293 | return new Delaunator(coords); 294 | } 295 | 296 | constructor(coords) { 297 | const n = coords.length >> 1; 298 | if (n > 0 && typeof coords[0] !== 'number') throw new Error('Expected coords to contain numbers.'); 299 | 300 | this.coords = coords; 301 | 302 | // arrays that will store the triangulation graph 303 | const maxTriangles = Math.max(2 * n - 5, 0); 304 | this._triangles = new Uint32Array(maxTriangles * 3); 305 | this._halfedges = new Int32Array(maxTriangles * 3); 306 | 307 | // temporary arrays for tracking the edges of the advancing convex hull 308 | this._hashSize = Math.ceil(Math.sqrt(n)); 309 | this._hullPrev = new Uint32Array(n); // edge to prev edge 310 | this._hullNext = new Uint32Array(n); // edge to next edge 311 | this._hullTri = new Uint32Array(n); // edge to adjacent triangle 312 | this._hullHash = new Int32Array(this._hashSize).fill(-1); // angular edge hash 313 | 314 | // temporary arrays for sorting points 315 | this._ids = new Uint32Array(n); 316 | this._dists = new Float64Array(n); 317 | 318 | this.update(); 319 | } 320 | 321 | update() { 322 | const {coords, _hullPrev: hullPrev, _hullNext: hullNext, _hullTri: hullTri, _hullHash: hullHash} = this; 323 | const n = coords.length >> 1; 324 | 325 | // populate an array of point indices; calculate input data bbox 326 | let minX = Infinity; 327 | let minY = Infinity; 328 | let maxX = -Infinity; 329 | let maxY = -Infinity; 330 | 331 | for (let i = 0; i < n; i++) { 332 | const x = coords[2 * i]; 333 | const y = coords[2 * i + 1]; 334 | if (x < minX) minX = x; 335 | if (y < minY) minY = y; 336 | if (x > maxX) maxX = x; 337 | if (y > maxY) maxY = y; 338 | this._ids[i] = i; 339 | } 340 | const cx = (minX + maxX) / 2; 341 | const cy = (minY + maxY) / 2; 342 | 343 | let minDist = Infinity; 344 | let i0, i1, i2; 345 | 346 | // pick a seed point close to the center 347 | for (let i = 0; i < n; i++) { 348 | const d = dist(cx, cy, coords[2 * i], coords[2 * i + 1]); 349 | if (d < minDist) { 350 | i0 = i; 351 | minDist = d; 352 | } 353 | } 354 | const i0x = coords[2 * i0]; 355 | const i0y = coords[2 * i0 + 1]; 356 | 357 | minDist = Infinity; 358 | 359 | // find the point closest to the seed 360 | for (let i = 0; i < n; i++) { 361 | if (i === i0) continue; 362 | const d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1]); 363 | if (d < minDist && d > 0) { 364 | i1 = i; 365 | minDist = d; 366 | } 367 | } 368 | let i1x = coords[2 * i1]; 369 | let i1y = coords[2 * i1 + 1]; 370 | 371 | let minRadius = Infinity; 372 | 373 | // find the third point which forms the smallest circumcircle with the first two 374 | for (let i = 0; i < n; i++) { 375 | if (i === i0 || i === i1) continue; 376 | const r = circumradius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1]); 377 | if (r < minRadius) { 378 | i2 = i; 379 | minRadius = r; 380 | } 381 | } 382 | let i2x = coords[2 * i2]; 383 | let i2y = coords[2 * i2 + 1]; 384 | 385 | if (minRadius === Infinity) { 386 | // order collinear points by dx (or dy if all x are identical) 387 | // and return the list as a hull 388 | for (let i = 0; i < n; i++) { 389 | this._dists[i] = (coords[2 * i] - coords[0]) || (coords[2 * i + 1] - coords[1]); 390 | } 391 | quicksort(this._ids, this._dists, 0, n - 1); 392 | const hull = new Uint32Array(n); 393 | let j = 0; 394 | for (let i = 0, d0 = -Infinity; i < n; i++) { 395 | const id = this._ids[i]; 396 | if (this._dists[id] > d0) { 397 | hull[j++] = id; 398 | d0 = this._dists[id]; 399 | } 400 | } 401 | this.hull = hull.subarray(0, j); 402 | this.triangles = new Uint32Array(0); 403 | this.halfedges = new Uint32Array(0); 404 | return; 405 | } 406 | 407 | // swap the order of the seed points for counter-clockwise orientation 408 | if (orient2d(i0x, i0y, i1x, i1y, i2x, i2y) < 0) { 409 | const i = i1; 410 | const x = i1x; 411 | const y = i1y; 412 | i1 = i2; 413 | i1x = i2x; 414 | i1y = i2y; 415 | i2 = i; 416 | i2x = x; 417 | i2y = y; 418 | } 419 | 420 | const center = circumcenter(i0x, i0y, i1x, i1y, i2x, i2y); 421 | this._cx = center.x; 422 | this._cy = center.y; 423 | 424 | for (let i = 0; i < n; i++) { 425 | this._dists[i] = dist(coords[2 * i], coords[2 * i + 1], center.x, center.y); 426 | } 427 | 428 | // sort the points by distance from the seed triangle circumcenter 429 | quicksort(this._ids, this._dists, 0, n - 1); 430 | 431 | // set up the seed triangle as the starting hull 432 | this._hullStart = i0; 433 | let hullSize = 3; 434 | 435 | hullNext[i0] = hullPrev[i2] = i1; 436 | hullNext[i1] = hullPrev[i0] = i2; 437 | hullNext[i2] = hullPrev[i1] = i0; 438 | 439 | hullTri[i0] = 0; 440 | hullTri[i1] = 1; 441 | hullTri[i2] = 2; 442 | 443 | hullHash.fill(-1); 444 | hullHash[this._hashKey(i0x, i0y)] = i0; 445 | hullHash[this._hashKey(i1x, i1y)] = i1; 446 | hullHash[this._hashKey(i2x, i2y)] = i2; 447 | 448 | this.trianglesLen = 0; 449 | this._addTriangle(i0, i1, i2, -1, -1, -1); 450 | 451 | for (let k = 0, xp, yp; k < this._ids.length; k++) { 452 | const i = this._ids[k]; 453 | const x = coords[2 * i]; 454 | const y = coords[2 * i + 1]; 455 | 456 | // skip near-duplicate points 457 | if (k > 0 && Math.abs(x - xp) <= EPSILON && Math.abs(y - yp) <= EPSILON) continue; 458 | xp = x; 459 | yp = y; 460 | 461 | // skip seed triangle points 462 | if (i === i0 || i === i1 || i === i2) continue; 463 | 464 | // find a visible edge on the convex hull using edge hash 465 | let start = 0; 466 | for (let j = 0, key = this._hashKey(x, y); j < this._hashSize; j++) { 467 | start = hullHash[(key + j) % this._hashSize]; 468 | if (start !== -1 && start !== hullNext[start]) break; 469 | } 470 | 471 | start = hullPrev[start]; 472 | let e = start, q; 473 | while (q = hullNext[e], orient2d(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1]) >= 0) { 474 | e = q; 475 | if (e === start) { 476 | e = -1; 477 | break; 478 | } 479 | } 480 | if (e === -1) continue; // likely a near-duplicate point; skip it 481 | 482 | // add the first triangle from the point 483 | let t = this._addTriangle(e, i, hullNext[e], -1, -1, hullTri[e]); 484 | 485 | // recursively flip triangles from the point until they satisfy the Delaunay condition 486 | hullTri[i] = this._legalize(t + 2); 487 | hullTri[e] = t; // keep track of boundary triangles on the hull 488 | hullSize++; 489 | 490 | // walk forward through the hull, adding more triangles and flipping recursively 491 | let n = hullNext[e]; 492 | while (q = hullNext[n], orient2d(x, y, coords[2 * n], coords[2 * n + 1], coords[2 * q], coords[2 * q + 1]) < 0) { 493 | t = this._addTriangle(n, i, q, hullTri[i], -1, hullTri[n]); 494 | hullTri[i] = this._legalize(t + 2); 495 | hullNext[n] = n; // mark as removed 496 | hullSize--; 497 | n = q; 498 | } 499 | 500 | // walk backward from the other side, adding more triangles and flipping 501 | if (e === start) { 502 | while (q = hullPrev[e], orient2d(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1]) < 0) { 503 | t = this._addTriangle(q, i, e, -1, hullTri[e], hullTri[q]); 504 | this._legalize(t + 2); 505 | hullTri[q] = t; 506 | hullNext[e] = e; // mark as removed 507 | hullSize--; 508 | e = q; 509 | } 510 | } 511 | 512 | // update the hull indices 513 | this._hullStart = hullPrev[i] = e; 514 | hullNext[e] = hullPrev[n] = i; 515 | hullNext[i] = n; 516 | 517 | // save the two new edges in the hash table 518 | hullHash[this._hashKey(x, y)] = i; 519 | hullHash[this._hashKey(coords[2 * e], coords[2 * e + 1])] = e; 520 | } 521 | 522 | this.hull = new Uint32Array(hullSize); 523 | for (let i = 0, e = this._hullStart; i < hullSize; i++) { 524 | this.hull[i] = e; 525 | e = hullNext[e]; 526 | } 527 | 528 | // trim typed triangle mesh arrays 529 | this.triangles = this._triangles.subarray(0, this.trianglesLen); 530 | this.halfedges = this._halfedges.subarray(0, this.trianglesLen); 531 | } 532 | 533 | _hashKey(x, y) { 534 | return Math.floor(pseudoAngle(x - this._cx, y - this._cy) * this._hashSize) % this._hashSize; 535 | } 536 | 537 | _legalize(a) { 538 | const {_triangles: triangles, _halfedges: halfedges, coords} = this; 539 | 540 | let i = 0; 541 | let ar = 0; 542 | 543 | // recursion eliminated with a fixed-size stack 544 | while (true) { 545 | const b = halfedges[a]; 546 | 547 | /* if the pair of triangles doesn't satisfy the Delaunay condition 548 | * (p1 is inside the circumcircle of [p0, pl, pr]), flip them, 549 | * then do the same check/flip recursively for the new pair of triangles 550 | * 551 | * pl pl 552 | * /||\ / \ 553 | * al/ || \bl al/ \a 554 | * / || \ / \ 555 | * / a||b \ flip /___ar___\ 556 | * p0\ || /p1 => p0\---bl---/p1 557 | * \ || / \ / 558 | * ar\ || /br b\ /br 559 | * \||/ \ / 560 | * pr pr 561 | */ 562 | const a0 = a - a % 3; 563 | ar = a0 + (a + 2) % 3; 564 | 565 | if (b === -1) { // convex hull edge 566 | if (i === 0) break; 567 | a = EDGE_STACK[--i]; 568 | continue; 569 | } 570 | 571 | const b0 = b - b % 3; 572 | const al = a0 + (a + 1) % 3; 573 | const bl = b0 + (b + 2) % 3; 574 | 575 | const p0 = triangles[ar]; 576 | const pr = triangles[a]; 577 | const pl = triangles[al]; 578 | const p1 = triangles[bl]; 579 | 580 | const illegal = inCircle( 581 | coords[2 * p0], coords[2 * p0 + 1], 582 | coords[2 * pr], coords[2 * pr + 1], 583 | coords[2 * pl], coords[2 * pl + 1], 584 | coords[2 * p1], coords[2 * p1 + 1]); 585 | 586 | if (illegal) { 587 | triangles[a] = p1; 588 | triangles[b] = p0; 589 | 590 | const hbl = halfedges[bl]; 591 | 592 | // edge swapped on the other side of the hull (rare); fix the halfedge reference 593 | if (hbl === -1) { 594 | let e = this._hullStart; 595 | do { 596 | if (this._hullTri[e] === bl) { 597 | this._hullTri[e] = a; 598 | break; 599 | } 600 | e = this._hullPrev[e]; 601 | } while (e !== this._hullStart); 602 | } 603 | this._link(a, hbl); 604 | this._link(b, halfedges[ar]); 605 | this._link(ar, bl); 606 | 607 | const br = b0 + (b + 1) % 3; 608 | 609 | // don't worry about hitting the cap: it can only happen on extremely degenerate input 610 | if (i < EDGE_STACK.length) { 611 | EDGE_STACK[i++] = br; 612 | } 613 | } else { 614 | if (i === 0) break; 615 | a = EDGE_STACK[--i]; 616 | } 617 | } 618 | 619 | return ar; 620 | } 621 | 622 | _link(a, b) { 623 | this._halfedges[a] = b; 624 | if (b !== -1) this._halfedges[b] = a; 625 | } 626 | 627 | // add a new triangle given vertex indices and adjacent half-edge ids 628 | _addTriangle(i0, i1, i2, a, b, c) { 629 | const t = this.trianglesLen; 630 | 631 | this._triangles[t] = i0; 632 | this._triangles[t + 1] = i1; 633 | this._triangles[t + 2] = i2; 634 | 635 | this._link(t, a); 636 | this._link(t + 1, b); 637 | this._link(t + 2, c); 638 | 639 | this.trianglesLen += 3; 640 | 641 | return t; 642 | } 643 | } 644 | 645 | // monotonically increases with real angle, but doesn't need expensive trigonometry 646 | function pseudoAngle(dx, dy) { 647 | const p = dx / (Math.abs(dx) + Math.abs(dy)); 648 | return (dy > 0 ? 3 - p : 1 + p) / 4; // [0..1] 649 | } 650 | 651 | function dist(ax, ay, bx, by) { 652 | const dx = ax - bx; 653 | const dy = ay - by; 654 | return dx * dx + dy * dy; 655 | } 656 | 657 | function inCircle(ax, ay, bx, by, cx, cy, px, py) { 658 | const dx = ax - px; 659 | const dy = ay - py; 660 | const ex = bx - px; 661 | const ey = by - py; 662 | const fx = cx - px; 663 | const fy = cy - py; 664 | 665 | const ap = dx * dx + dy * dy; 666 | const bp = ex * ex + ey * ey; 667 | const cp = fx * fx + fy * fy; 668 | 669 | return dx * (ey * cp - bp * fy) - 670 | dy * (ex * cp - bp * fx) + 671 | ap * (ex * fy - ey * fx) < 0; 672 | } 673 | 674 | function circumradius(ax, ay, bx, by, cx, cy) { 675 | const dx = bx - ax; 676 | const dy = by - ay; 677 | const ex = cx - ax; 678 | const ey = cy - ay; 679 | 680 | const bl = dx * dx + dy * dy; 681 | const cl = ex * ex + ey * ey; 682 | const d = 0.5 / (dx * ey - dy * ex); 683 | 684 | const x = (ey * bl - dy * cl) * d; 685 | const y = (dx * cl - ex * bl) * d; 686 | 687 | return x * x + y * y; 688 | } 689 | 690 | function circumcenter(ax, ay, bx, by, cx, cy) { 691 | const dx = bx - ax; 692 | const dy = by - ay; 693 | const ex = cx - ax; 694 | const ey = cy - ay; 695 | 696 | const bl = dx * dx + dy * dy; 697 | const cl = ex * ex + ey * ey; 698 | const d = 0.5 / (dx * ey - dy * ex); 699 | 700 | const x = ax + (ey * bl - dy * cl) * d; 701 | const y = ay + (dx * cl - ex * bl) * d; 702 | 703 | return {x, y}; 704 | } 705 | 706 | function quicksort(ids, dists, left, right) { 707 | if (right - left <= 20) { 708 | for (let i = left + 1; i <= right; i++) { 709 | const temp = ids[i]; 710 | const tempDist = dists[temp]; 711 | let j = i - 1; 712 | while (j >= left && dists[ids[j]] > tempDist) ids[j + 1] = ids[j--]; 713 | ids[j + 1] = temp; 714 | } 715 | } else { 716 | const median = (left + right) >> 1; 717 | let i = left + 1; 718 | let j = right; 719 | swap(ids, median, i); 720 | if (dists[ids[left]] > dists[ids[right]]) swap(ids, left, right); 721 | if (dists[ids[i]] > dists[ids[right]]) swap(ids, i, right); 722 | if (dists[ids[left]] > dists[ids[i]]) swap(ids, left, i); 723 | 724 | const temp = ids[i]; 725 | const tempDist = dists[temp]; 726 | while (true) { 727 | do i++; while (dists[ids[i]] < tempDist); 728 | do j--; while (dists[ids[j]] > tempDist); 729 | if (j < i) break; 730 | swap(ids, i, j); 731 | } 732 | ids[left + 1] = ids[j]; 733 | ids[j] = temp; 734 | 735 | if (right - i + 1 >= j - left) { 736 | quicksort(ids, dists, i, right); 737 | quicksort(ids, dists, left, j - 1); 738 | } else { 739 | quicksort(ids, dists, left, j - 1); 740 | quicksort(ids, dists, i, right); 741 | } 742 | } 743 | } 744 | 745 | function swap(arr, i, j) { 746 | const tmp = arr[i]; 747 | arr[i] = arr[j]; 748 | arr[j] = tmp; 749 | } 750 | 751 | function defaultGetX(p) { 752 | return p[0]; 753 | } 754 | function defaultGetY(p) { 755 | return p[1]; 756 | } 757 | 758 | return Delaunator; 759 | 760 | }))); 761 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 初始化画布 */ 2 | const canvas = document.getElementById("canvas"); 3 | const ctx = canvas.getContext("2d"); 4 | const width = 400; 5 | const height = 280; 6 | ctx.lineJoin = "round"; 7 | ctx.lineCap = "round"; 8 | canvas.style.width = width * 3 + "px"; 9 | canvas.style.height = height * 3 + "px"; 10 | const scale = 10; 11 | canvas.width = width * scale; 12 | canvas.height = height * scale; 13 | ctx.scale(scale, scale); 14 | const padding = 2; 15 | const totalWidth = parseInt(canvas.style.width); 16 | const totalHeight = parseInt(canvas.style.height); 17 | 18 | /** 点集 */ 19 | // 工具函数,生成八边形障碍 20 | const generateObstacle = (x, y, radius) => 21 | new Array(8).fill(null).map((_, index) => { 22 | const angle = (Math.PI / 4) * index; // 45度 23 | const pointX = x + radius * Math.cos(angle); 24 | const pointY = y + ((radius * Math.sin(angle)) / height) * width; // 因为getRenderPosition会修改渲染比例 25 | return { x : pointX, y : pointY }; 26 | }); 27 | 28 | /** 德劳内三角剖分 */ 29 | const generateTriangles = (points) => { 30 | const triangles = Delaunator.from(points.map((p) => [p.x, p.y])).triangles; 31 | return triangles.reduce((acc, cur, i) => { 32 | if (i % 3 !== 0) return acc; 33 | return [...acc, [triangles[i], triangles[i + 1], triangles[i + 2]].map((index) => points[index])]; 34 | }, []); 35 | }; 36 | 37 | /** A星算法 */ 38 | // 工具函数,检查某个三角网格是否属于某一个障碍 39 | const checkIsObstacle = (triangle, obstacles) => 40 | obstacles.some((obstacle) => triangle.every((p1) => obstacle.some((p2) => p1.x === p2.x && p1.y === p2.y))); 41 | // 工具函数,计算三角网格的重心 42 | const getNodeMid = (node) => { 43 | let x = (node.triangle[0].x + node.triangle[1].x + node.triangle[2].x) / 3; 44 | let y = (node.triangle[0].y + node.triangle[1].y + node.triangle[2].y) / 3; 45 | return { x, y }; 46 | }; 47 | // 工具函数,获取某个node的三条边 48 | const getEdges = (node) => { 49 | const triangle = node.triangle; 50 | return [ 51 | [triangle[0], triangle[1]], 52 | [triangle[1], triangle[2]], 53 | [triangle[2], triangle[0]], 54 | ]; 55 | }; 56 | // 工具函数,判断两条边是否相同 57 | const isSameEdge = (edge1, edge2) => { 58 | const [p1, p2] = edge1; 59 | const [q1, q2] = edge2; 60 | return ( 61 | (p1.x === q1.x && p1.y === q1.y && p2.x === q2.x && p2.y === q2.y) || 62 | (p1.x === q2.x && p1.y === q2.y && p2.x === q1.x && p2.y === q1.y) 63 | ); 64 | }; 65 | // 工具函数,获取某个网格的三个邻居网格(通过判断是否跟某个三角形有公共边) 66 | const getNeighborNodes = (nodes, curNode) => 67 | nodes.filter((otherNode) => { 68 | if (otherNode === curNode || otherNode.isObstacle) return false; 69 | for (const curEdge of getEdges(curNode)) { 70 | for (const otherEdge of getEdges(otherNode)) { 71 | if (isSameEdge(curEdge, otherEdge)) return true; 72 | } 73 | } 74 | return false; 75 | }); 76 | // 工具函数-计算方向(使用叉乘) 77 | const crossProduct = (a, b, c) => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); 78 | // 工具函数-计算夹角(使用点乘) 79 | const dotProduct = (a, b, c) => { 80 | const ab = { x : b.x - a.x, y : b.y - a.y }; 81 | const ac = { x : c.x - a.x, y : c.y - a.y }; 82 | const dotProduct = ab.x * ac.x + ab.y * ac.y; 83 | const lenAB = Math.sqrt(ab.x ** 2 + ab.y ** 2); 84 | const lenAC = Math.sqrt(ac.x ** 2 + ac.y ** 2); 85 | const cosTheta = dotProduct / (lenAB * lenAC); 86 | const angleRadians = Math.acos(cosTheta); 87 | const angleDegrees = angleRadians * (180 / Math.PI); 88 | return angleDegrees; 89 | }; 90 | // 工具函数,判断点是否在三角形内 91 | const isPointInNode = (node, point) => { 92 | let [p1, p2, p3] = node.triangle; 93 | let d1 = crossProduct(p1, p2, point); 94 | let d2 = crossProduct(p2, p3, point); 95 | let d3 = crossProduct(p3, p1, point); 96 | return (d1 >= 0 && d2 >= 0 && d3 >= 0) || (d1 <= 0 && d2 <= 0 && d3 <= 0); 97 | }; 98 | // 工具函数,判断point所在node 99 | const getNodeByPoint = (nodes, point) => nodes.find((node) => isPointInNode(node, point)); 100 | // 工具函数,计算两个node的公共边 101 | const getCommonEdge = (node1, node2) => { 102 | for (const curEdge of getEdges(node1)) { 103 | for (const nextEdge of getEdges(node2)) { 104 | if (isSameEdge(curEdge, nextEdge)) { 105 | return curEdge; 106 | } 107 | } 108 | } 109 | }; 110 | // 工具函数,获取边的中点 111 | const getEdgeMid = (edge) => { 112 | const [p1, p2] = edge; 113 | return { x : (p1.x + p2.x) / 2, y : (p1.y + p2.y) / 2 }; 114 | }; 115 | // 工具函数,两条边的中点距离 116 | const getEdgeDistance = (edge1, edge2) => { 117 | const p1 = getEdgeMid(edge1); 118 | const p2 = getEdgeMid(edge2); 119 | const distX = Math.abs(p1.x - p2.x); 120 | const distY = Math.abs(p1.y - p2.y); 121 | return Math.sqrt(distX ** 2 + distY ** 2); 122 | }; 123 | // 工具函数,启发式函数 124 | const heuristic = (edge1, edge2) => getEdgeDistance(edge1, edge2); 125 | 126 | // a星寻路 127 | const aStar = ({ startNode, endNode, nodes, startPoint, endPoint }) => { 128 | startNode.g = 0; 129 | startNode.h = 0; // 起点随便设置,用不到 130 | startNode.f = startNode.g + startNode.h; 131 | const openList = [startNode]; 132 | const closeList = []; 133 | while (openList.length) { 134 | // 择优 135 | openList.sort((a, b) => a.f - b.f); 136 | const currentNode = openList.shift(); 137 | if (currentNode === endNode) { 138 | // 到达终点,回溯路径 139 | const nodePath = []; 140 | let node = currentNode; 141 | while (node) { 142 | nodePath.unshift(node); 143 | node = node.parent; 144 | } 145 | return nodePath; 146 | } 147 | // 扩展 148 | closeList.push(currentNode); 149 | const neighborNodes = getNeighborNodes(nodes, currentNode); 150 | for (let neighborNode of neighborNodes) { 151 | if (closeList.includes(neighborNode)) continue; 152 | const curCommonEdge = getCommonEdge(currentNode, neighborNode); 153 | const preCommonEdge = currentNode.parent 154 | ? getCommonEdge(currentNode, currentNode.parent) 155 | : new Array(2).fill(startPoint); 156 | const tempG = currentNode.g + heuristic(curCommonEdge, preCommonEdge); 157 | if (tempG < neighborNode.g) { 158 | neighborNode.parent = currentNode; 159 | neighborNode.g = tempG; 160 | neighborNode.h = heuristic(curCommonEdge, new Array(2).fill(endPoint)); 161 | neighborNode.f = neighborNode.g + neighborNode.h; 162 | if (!openList.includes(neighborNode)) openList.push(neighborNode); 163 | } 164 | } 165 | } 166 | }; 167 | 168 | /** 漏斗算法 */ 169 | // 工具函数,两个点是否相同 170 | const isSamePoint = (p1, p2) => p1.x === p2.x && p1.y === p2.y; 171 | // 工具函数,获取边上的另一个点 172 | const getOtherPoint = (edge, p1) => { 173 | const index = edge.findIndex((p2) => isSamePoint(p1, p2)); 174 | return edge[index === 0 ? 1 : 0]; 175 | }; 176 | // 漏斗平滑 177 | const funnel = ({ startPoint, endPoint, commonEdge, leftPoints, rightPoints }) => { 178 | // 漏斗边开始遍历起点 179 | let nextLeftIndex = 0; 180 | let nextRightIndex = 0; 181 | const smoothedPath = [startPoint]; 182 | while (!isSamePoint(smoothedPath[smoothedPath.length - 1], endPoint)) { 183 | // 漏斗中点 184 | const mid = smoothedPath[smoothedPath.length - 1]; 185 | let preAngle = 360; 186 | let preSign = NaN; 187 | let leftIndex = nextLeftIndex; 188 | let rightIndex = nextRightIndex; 189 | const leftTotal = leftPoints.length - 1; 190 | const rightTotal = rightPoints.length - 1; 191 | let leftMoved = false; 192 | const lefts = leftPoints.map((point) => ({ point, disable : false })); 193 | const rights = rightPoints.map((point) => ({ point, disable : false })); 194 | // 前进直到非disabled节点 195 | const addLeftIndex = () => { 196 | if (leftIndex >= leftTotal) return false; 197 | const start = leftIndex + 1; 198 | const index = lefts.slice(start).findIndex(e => !e.disable); 199 | if (index === -1) return false; 200 | leftIndex = start + index; 201 | return true; 202 | }; 203 | // 后退直到非disabled节点 204 | const reduceLeftIndex = () => { 205 | leftIndex = (() => { 206 | let tempIndex = leftIndex; 207 | while (tempIndex > 0 && lefts[tempIndex].disable) tempIndex--; 208 | return tempIndex; 209 | })(); 210 | }; 211 | const addRightIndex = () => { 212 | if (rightIndex >= rightTotal) return false; 213 | const start = rightIndex + 1; 214 | const index = rights.slice(start).findIndex(e => !e.disable); 215 | if (index === -1) return false; 216 | rightIndex = start + index; 217 | return true; 218 | 219 | }; 220 | const reduceRightIndex = () => { 221 | rightIndex = (() => { 222 | let tempIndex = rightIndex; 223 | while (tempIndex > 0 && rights[tempIndex].disable) tempIndex--; 224 | return tempIndex; 225 | })(); 226 | }; 227 | const addIndex = (_leftIndex = leftIndex, _rightIndex = rightIndex) => { 228 | const leftIndexAtCommonEdge = commonEdge.findLastIndex((edge) => 229 | edge.some((p) => isSamePoint(p, lefts[_leftIndex].point)) 230 | ); 231 | const rightIndexAtCommonEdge = commonEdge.findLastIndex((edge) => 232 | edge.some((p) => isSamePoint(p, rights[_rightIndex].point)) 233 | ); 234 | // 比较左右index在公共边上的位置,靠前的优先移动 235 | if (leftIndexAtCommonEdge < rightIndexAtCommonEdge) { 236 | const success = addLeftIndex(); 237 | leftMoved = success; 238 | !success && addRightIndex(); 239 | } else { 240 | const success = addRightIndex(); 241 | leftMoved = !success; 242 | if (!success) addLeftIndex(); 243 | } 244 | }; 245 | while (true) { 246 | const left = lefts[leftIndex].point; 247 | const right = rights[rightIndex].point; 248 | const angle = dotProduct(mid, left, right); 249 | const sign = crossProduct(mid, left, right); 250 | // debugger 251 | 252 | // 1.符号相反,代表两条漏斗边发生跨越 253 | // 2.叉积为0,代表两条漏斗边平行,到点终点或者跟终点平行 254 | if ((preSign > 0 && sign < 0) || (preSign < 0 && sign > 0) || sign === 0) { 255 | const target = leftMoved ? right : left; 256 | // 保存路径点 257 | smoothedPath.push(target); 258 | const edge = commonEdge.findLast((edge) => edge.some((p) => isSamePoint(p, target))); // 拐点所在最后的公共边 259 | const p1 = getOtherPoint(edge, target); 260 | // 决定下次迭代的两个点 261 | if (leftMoved) { 262 | nextLeftIndex = lefts.findIndex(({ point : p2 }) => isSamePoint(p1, p2)); 263 | nextRightIndex = rightIndex + 1; 264 | } else { 265 | nextRightIndex = rights.findIndex(({ point : p2 }) => isSamePoint(p1, p2)); 266 | nextLeftIndex = leftIndex + 1; 267 | } 268 | break; 269 | } 270 | 271 | if (angle <= preAngle) { 272 | (leftMoved ? lefts : rights).map((v) => (v.disable = false)); 273 | addIndex(); 274 | preAngle = angle; // 记录temp 275 | preSign = sign; 276 | } else { 277 | if (leftMoved) { 278 | lefts[leftIndex].disable = true; 279 | let tempIndex = leftIndex; 280 | reduceLeftIndex(); 281 | addIndex(tempIndex, rightIndex); 282 | } else { 283 | rights[rightIndex].disable = true; 284 | let tempIndex = rightIndex; 285 | reduceRightIndex(); 286 | addIndex(leftIndex, tempIndex); 287 | } 288 | } 289 | } 290 | } 291 | return smoothedPath; 292 | }; 293 | 294 | // 工具函数,逻辑坐标转成渲染坐标,用padding防止溢出 295 | const getRenderPosition = ({ x, y }) => ({ 296 | x : (x / 100) * (width - padding * 2) + padding, 297 | y : (y / 100) * (height - padding * 2) + padding, 298 | }); 299 | // 工具函数,保留几位 300 | const toFixed = (num, digits) => parseFloat(num.toFixed(digits)); 301 | /** 画点 */ 302 | const drawPoint = ({points}) => { 303 | for (const p of points) { 304 | const point = getRenderPosition({ x : p.x, y : p.y }); 305 | const radius = 2; 306 | ctx.fillStyle = "#ff6398"; 307 | ctx.beginPath(); 308 | ctx.arc(point.x, point.y, radius, 0, Math.PI * 2); 309 | ctx.closePath(); 310 | ctx.fill(); 311 | } 312 | for (const p of [startPoint, endPoint]) { 313 | const point = getRenderPosition({ x : p.x, y : p.y }); 314 | const radius = 3; 315 | ctx.fillStyle = "#ff6398"; 316 | ctx.beginPath(); 317 | ctx.arc(point.x, point.y, radius, 0, Math.PI * 2); 318 | ctx.closePath(); 319 | ctx.fill(); 320 | } 321 | }; 322 | /** 画线 */ 323 | const drawEdge = ({ nodes,nodePath }) => { 324 | for (const node of nodes) { 325 | const [a, b, c] = node.triangle; 326 | const p1 = getRenderPosition({ x : a.x, y : a.y }); 327 | const p2 = getRenderPosition({ x : b.x, y : b.y }); 328 | const p3 = getRenderPosition({ x : c.x, y : c.y }); 329 | // 图形 330 | ctx.beginPath(); 331 | ctx.moveTo(p1.x, p1.y); 332 | ctx.lineTo(p2.x, p2.y); 333 | ctx.lineTo(p3.x, p3.y); 334 | ctx.closePath(); 335 | // 填充 336 | ctx.fillStyle =(()=>{ 337 | if(node.isObstacle){ 338 | return "#404040" 339 | }else if(nodePath.some(n=>n === node)){ 340 | return "rgba(255,99,152,0.5)" 341 | }else{ 342 | return "transparent"; 343 | } 344 | })(); 345 | ctx.fill(); 346 | // 边框 347 | ctx.lineWidth = 0.75; 348 | ctx.strokeStyle = "#65ddfd"; 349 | ctx.stroke(); 350 | // 索引文字 351 | // ctx.font = "5px SimHei"; 352 | // ctx.fillStyle = "#888"; 353 | // ctx.textAlign = "center"; 354 | // ctx.textBaseline = "middle"; 355 | // const mid = getNodeMid(node); 356 | // const textPos = getRenderPosition({ x : mid.x, y : mid.y }); 357 | // ctx.fillText(`${ node.index }`, textPos.x, textPos.y); 358 | } 359 | }; 360 | /** 画公共线 */ 361 | const drawCommonEdge = ({ commonEdge }) => { 362 | for (let i = 0; i < commonEdge.length; i++) { 363 | for (const edge of commonEdge) { 364 | const [p1, p2] = edge; 365 | const cur = getRenderPosition({ x : p1.x, y : p1.y }); 366 | const next = getRenderPosition({ x : p2.x, y : p2.y }); 367 | // 画图形 368 | ctx.beginPath(); 369 | ctx.moveTo(cur.x, cur.y); 370 | ctx.lineTo(next.x, next.y); 371 | ctx.closePath(); 372 | // 边框 373 | ctx.lineWidth = 1; 374 | ctx.strokeStyle = "#feb94a"; 375 | ctx.stroke(); 376 | } 377 | } 378 | }; 379 | /** 画A星网格路径 */ 380 | const drawNodePath = ({ nodePath }) => { 381 | const path = [startPoint]; 382 | for (let i = 0; i < nodePath.length - 1; i++) { 383 | path.push(getEdgeMid(getCommonEdge(nodePath[i], nodePath[i + 1]))); 384 | } 385 | path.push(endPoint); 386 | for (let i = 0; i < path.length - 1; i++) { 387 | const p1 = path[i]; 388 | const p2 = path[i + 1]; 389 | const cur = getRenderPosition({ x : p1.x, y : p1.y }); 390 | const next = getRenderPosition({ x : p2.x, y : p2.y }); 391 | ctx.beginPath(); 392 | ctx.moveTo(cur.x, cur.y); 393 | ctx.lineTo(next.x, next.y); 394 | ctx.closePath(); 395 | ctx.lineWidth = 1; 396 | ctx.strokeStyle = "#ee938f"; 397 | ctx.stroke(); 398 | } 399 | }; 400 | /** 画漏斗平滑路径 */ 401 | const drawSmoothedPath = ({ smoothedPath }) => { 402 | for (let i = 0; i < smoothedPath.length - 1; i++) { 403 | const p1 = smoothedPath[i]; 404 | const p2 = smoothedPath[i + 1]; 405 | const cur = getRenderPosition({ x : p1.x, y : p1.y }); 406 | const next = getRenderPosition({ x : p2.x, y : p2.y }); 407 | ctx.beginPath(); 408 | ctx.moveTo(cur.x, cur.y); 409 | ctx.lineTo(next.x, next.y); 410 | ctx.closePath(); 411 | ctx.lineWidth = 1.25; 412 | ctx.strokeStyle = "#aae062"; 413 | ctx.stroke(); 414 | } 415 | }; 416 | /** 寻路核心流程 */ 417 | const main = ({ startPoint, endPoint, canvas }) => { 418 | // 地图边界 419 | const map = [ 420 | { x : 0, y : 0 }, 421 | { x : 0, y : 100 }, 422 | { x : 100, y : 0 }, 423 | { x : 100, y : 100 }, 424 | ]; 425 | // 障碍 426 | const obstacles = (() => { 427 | const obstacles = [ 428 | // generateObstacle(30, 46, 10), 429 | // generateObstacle(55, 40, 10), 430 | generateObstacle(40, 25, 10), 431 | generateObstacle(80, 21, 15), 432 | generateObstacle(15, 14, 10), 433 | generateObstacle(25, 75, 14), 434 | generateObstacle(60, 58, 16), 435 | generateObstacle(85, 80, 10), 436 | generateObstacle(20, 45, 6), 437 | generateObstacle(90, 52, 8), 438 | generateObstacle(50, 89, 8), 439 | ]; 440 | return obstacles.map((ob) => 441 | ob.map((p) => { 442 | return { x : toFixed(p.x, 8), y : toFixed(p.y, 8) }; 443 | }) 444 | ); 445 | })(); 446 | // 点集 447 | const points = [...map, ...obstacles.flat()]; 448 | // console.log("points",points) 449 | // 三角剖分 450 | const triangles = generateTriangles(points); 451 | // 三角网格封装成node 452 | const nodes = triangles.map((triangle, index) => ({ 453 | index, // 方便标识 454 | triangle, // 对应的三角网格 455 | isObstacle : checkIsObstacle(triangle, obstacles), // 是否是障碍node 456 | g : Infinity, // 到该节点的代价 457 | h : 0, // 到达目标点点代价 458 | f : 0, // 启发式评估值 459 | parent : null, // 父节点,用于回溯 460 | })); 461 | // 防止点击障碍 462 | if (getNodeByPoint(nodes, startPoint)?.isObstacle || getNodeByPoint(nodes, endPoint)?.isObstacle) return; 463 | 464 | // 网格路径 465 | const startNode = getNodeByPoint(nodes, startPoint); 466 | const endNode = getNodeByPoint(nodes, endPoint); 467 | // a星网格寻路 468 | const nodePath = aStar({ startNode, endNode, nodes, startPoint, endPoint }); 469 | // console.log("nodePath", nodePath); 470 | // 网格路径公共边 471 | const commonEdge = (() => { 472 | const result = []; 473 | for (let i = 0; i < nodePath.length - 1; i++) { 474 | result.push(getCommonEdge(nodePath[i], nodePath[i + 1])); 475 | } 476 | return [...result, [endPoint, endPoint]]; // 最后一项特殊处理,把终点看作一条端点相同的边 477 | })(); 478 | // console.log("commonEdge",commonEdge) 479 | // 网格路径区分左右两条路径点 480 | const [leftPoints, rightPoints] = (() => { 481 | const leftPoints = [commonEdge[0][1]]; // 随便放就行,只要能把端点分成两组 482 | const rightPoints = []; 483 | for (let i = 0; i < commonEdge.length - 1; i++) { 484 | const [one, two] = commonEdge[i]; 485 | const oneInLeft = leftPoints.some((point) => isSamePoint(point, one)); 486 | const oneInRight = rightPoints.some((point) => isSamePoint(point, one)); 487 | const twoInLeft = leftPoints.some((point) => isSamePoint(point, two)); 488 | const twoInRight = rightPoints.some((point) => isSamePoint(point, two)); 489 | if (oneInLeft) { 490 | rightPoints.push(two); 491 | } else if (oneInRight) { 492 | leftPoints.push(two); 493 | } else if (twoInLeft) { 494 | rightPoints.push(one); 495 | } else if (twoInRight) { 496 | leftPoints.push(one); 497 | } 498 | } 499 | leftPoints.push(endPoint); 500 | rightPoints.push(endPoint); 501 | return [leftPoints, rightPoints]; 502 | })(); 503 | // console.log("leftPoints", leftPoints) 504 | // console.log("rightPoints", rightPoints) 505 | // 漏斗优化路径 506 | const smoothedPath = funnel({ 507 | startPoint, 508 | endPoint, 509 | commonEdge, 510 | leftPoints, 511 | rightPoints, 512 | }); 513 | // console.log("smoothedPath", smoothedPath); 514 | 515 | // 渲染 516 | ctx.clearRect(0, 0, width, height); 517 | drawEdge({ nodes,nodePath }); 518 | drawCommonEdge({ commonEdge }); 519 | drawNodePath({ nodePath }); 520 | drawSmoothedPath({ smoothedPath }); 521 | drawPoint({ points }); 522 | }; 523 | 524 | let startPoint = { x : 5, y : 5 }; 525 | let endPoint = { x : 75, y : 50 }; 526 | 527 | /** 交互 */ 528 | // 点击设置起点 529 | canvas.addEventListener("click", (e) => { 530 | const rect = canvas.getBoundingClientRect(); 531 | const x = e.clientX - rect.left; 532 | const y = e.clientY - rect.top - padding; 533 | if (!(x < totalWidth && x > 0 && y < totalHeight && y > 0)) return false; 534 | startPoint = { x : (x / totalWidth) * 100, y : (y / totalHeight) * 100 }; 535 | return false; 536 | }); 537 | // 滑动设置终点 538 | canvas.addEventListener("mousemove", (e) => { 539 | e.stopPropagation(); 540 | e.preventDefault(); 541 | const rect = canvas.getBoundingClientRect(); 542 | const x = e.clientX - rect.left; 543 | const y = e.clientY - rect.top - padding; 544 | if (!(x < totalWidth && x > 0 && y < totalHeight && y > 0)) return false; 545 | endPoint = { x : (x / totalWidth) * 100, y : (y / totalHeight) * 100 }; 546 | return false; 547 | }); 548 | 549 | /** 循环 */ 550 | const tick = () => { 551 | main({ startPoint, endPoint, canvas }); 552 | requestAnimationFrame(tick); 553 | }; 554 | 555 | tick(); 556 | --------------------------------------------------------------------------------