navmesh
Version:
A library for fast pathfinding using navigation meshes in JS
186 lines (161 loc) • 6.09 kB
text/typescript
import NavMesh from "./navmesh";
import Vector2 from "./math/vector-2";
const v2 = (x: number, y: number) => new Vector2(x, y);
describe("An empty NavMesh instance", () => {
let emptyNavMesh: NavMesh;
beforeAll(() => (emptyNavMesh = new NavMesh([])));
it("should not throw an error on construction", () => {
expect(() => emptyNavMesh).not.toThrow();
});
it("should always return null when queried for a path", () => {
const path = emptyNavMesh.findPath(v2(10, 20), v2(30, 50));
expect(path).toBeNull();
});
});
describe("A simple, fully connected NavMesh instance", () => {
let navMesh: NavMesh;
/*
- - - - -
- 1 - 2 -
- - - - -
*/
// prettier-ignore
const polygons = [
[v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1
[v2(10,0), v2(20,0), v2(20,10), v2(10,10)] // 2
];
beforeAll(() => (navMesh = new NavMesh(polygons)));
it("should return a direct path when points are in the same polygon", () => {
const path = navMesh.findPath(v2(0, 0), v2(5, 5));
expect(path).toEqual([v2(0, 0), v2(5, 5)]);
});
it("should return null when a point is outside all polygon", () => {
const path = navMesh.findPath(v2(-10, 0), v2(5, 5));
expect(path).toBeNull();
});
it("should return a path when points are in neighboring polygons", () => {
const path = navMesh.findPath(v2(5, 5), v2(15, 5));
expect(path).toEqual([v2(5, 5), v2(15, 5)]);
});
it("should return a path when points are on the edges of the polygons", () => {
const path = navMesh.findPath(v2(0, 0), v2(20, 10));
expect(path).toEqual([v2(0, 0), v2(20, 10)]);
});
});
describe("A NavMesh instance with two islands", () => {
let navMesh: NavMesh;
/*
- - - - - -
- 1 - - 2 -
- - - - - -
*/
// prettier-ignore
const polygons = [
[v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1
[v2(40,0), v2(50,0), v2(50,10), v2(40,10)], // 2
];
beforeAll(() => (navMesh = new NavMesh(polygons)));
it("should return null when queried for a path between islands", () => {
const path = navMesh.findPath(v2(0, 0), v2(40, 0));
expect(path).toBeNull();
});
});
describe("A NavMesh instance with a corner", () => {
let navMesh: NavMesh;
/*
- - - - -
- 1 - 2 -
- - - - -
- 3 -
- - -
*/
// prettier-ignore
const polygons = [
[v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1
[v2(10,0), v2(20,0), v2(20,10), v2(10,10)], // 2
[v2(10,10), v2(20,10), v2(20,20), v2(10,20)] // 3
];
beforeAll(() => (navMesh = new NavMesh(polygons)));
it("should return a path that hugs the corner", () => {
const path = navMesh.findPath(v2(0, 0), v2(10, 20));
expect(path).toEqual([v2(0, 0), v2(10, 10), v2(10, 20)]);
});
});
describe("isPointInMesh", () => {
let navMesh: NavMesh;
/*
- - - - - - - -
- 1 - 2 - - 4 -
- - - - - - - -
- 3 -
- - -
*/
// prettier-ignore
const polygons = [
[v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1
[v2(10,0), v2(20,0), v2(20,10), v2(10,10)], // 2
[v2(10,10), v2(20,10), v2(20,20), v2(10,20)], // 3
[v2(30,0), v2(40,0), v2(40,10), v2(30,10)] // 4
];
beforeAll(() => (navMesh = new NavMesh(polygons)));
it("should return true if point is on the edge of a polygon", () => {
expect(navMesh.isPointInMesh(v2(0, 0))).toEqual(true);
expect(navMesh.isPointInMesh(v2(10, 0))).toEqual(true);
expect(navMesh.isPointInMesh(v2(10, 10))).toEqual(true);
expect(navMesh.isPointInMesh(v2(40, 10))).toEqual(true);
});
it("should return true if point is in a polygon in the mesh", () => {
expect(navMesh.isPointInMesh(v2(5, 5))).toEqual(true);
expect(navMesh.isPointInMesh(v2(10, 5))).toEqual(true);
expect(navMesh.isPointInMesh(v2(32, 2))).toEqual(true);
});
it("should return false for a point outside the mesh", () => {
expect(navMesh.isPointInMesh(v2(-10, -20))).toEqual(false);
expect(navMesh.isPointInMesh(v2(25, 0))).toEqual(false);
expect(navMesh.isPointInMesh(v2(300, 100))).toEqual(false);
});
});
describe("findClosestMeshPoint", () => {
let navMesh: NavMesh;
/*
- - - - - - - -
- 1 - 2 - - 4 -
- - - - - - - -
- 3 -
- - -
*/
// prettier-ignore
const polygons = [
[v2(0,0), v2(10,0), v2(10,10), v2(0,10)], // 1
[v2(10,0), v2(20,0), v2(20,10), v2(10,10)], // 2
[v2(10,10), v2(20,10), v2(20,20), v2(10,20)], // 3
[v2(30,0), v2(40,0), v2(40,10), v2(30,10)] // 4
];
beforeAll(() => (navMesh = new NavMesh(polygons)));
it("should return null for points outside of the max distance", () => {
expect(navMesh.findClosestMeshPoint(v2(-100, 0), 10).polygon).toBeNull();
});
it("should return poly 1 for point inside poly 1", () => {
const result = navMesh.findClosestMeshPoint(v2(5, 5));
expect(result?.polygon?.id).toBe(0);
expect(result?.point).toEqual({ x: 5, y: 5 });
});
it("should return poly 1 or 2 for point on shared edge between poly 1 and 2", () => {
const result = navMesh.findClosestMeshPoint(v2(10, 5));
expect(result?.polygon?.id).toBeGreaterThanOrEqual(0);
expect(result?.polygon?.id).toBeLessThanOrEqual(1);
});
it("should return top left corner of poly 1 for a point just outside of top left corner", () => {
const result = navMesh.findClosestMeshPoint(v2(-10, -10));
expect(result?.point).toEqual({ x: 0, y: 0 });
});
it("should return top middle of poly 1 for a point just outside of top middle", () => {
const result = navMesh.findClosestMeshPoint(v2(-10, 5));
expect(result?.point).toEqual({ x: 0, y: 5 });
});
it("should return poly 2 or 4 for a point equidistant from them", () => {
const result = navMesh.findClosestMeshPoint(v2(25, 5));
const isPoly2or4 = result?.polygon?.id === 1 || result?.polygon?.id === 3;
expect(isPoly2or4).toBe(true);
});
});