├── 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 |
74 |
78 |
82 |
86 |
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 |
--------------------------------------------------------------------------------