@jgengine/navbake 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +95 -19
- package/package.json +34 -7
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
# @jgengine/navbake
|
|
2
2
|
|
|
3
|
-
Deterministic navigation-mesh baking for authored JGengine geometry.
|
|
3
|
+
Deterministic navigation-mesh baking for authored JGengine geometry, backed by [recast-navigation](https://github.com/isaac-mason/recast-navigation-js).
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { bakeNavMesh, initNavBake } from "@jgengine/navbake";
|
|
7
|
+
import { createNavMeshQuery } from "@jgengine/core/nav/navMesh";
|
|
8
|
+
|
|
9
|
+
await initNavBake();
|
|
10
|
+
const mesh = bakeNavMesh({ positions, indices, agentRadius: 0.4, agentHeight: 1.8, maxSlope: 45, maxClimb: 0.4 });
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Walkable triangles wind counter-clockwise seen from above (three.js front faces up). The result is plain `NavMeshData`: store it as an editor `nav` bake and route on it with `@jgengine/core/nav/navMesh`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import type { NavMeshData } from "@jgengine/core/nav/navMesh";
|
|
2
|
+
/** Walkable geometry and agent dimensions for {@link bakeNavMesh}. */
|
|
2
3
|
export interface BakeNavMeshOptions {
|
|
4
|
+
/** Flat xyz triples. */
|
|
3
5
|
positions: readonly number[];
|
|
6
|
+
/** Triangle vertex indices, counter-clockwise seen from above for walkable faces (three.js front faces up). */
|
|
4
7
|
indices: readonly number[];
|
|
5
8
|
agentRadius: number;
|
|
6
9
|
agentHeight: number;
|
|
10
|
+
/** Steepest walkable slope in degrees. */
|
|
7
11
|
maxSlope: number;
|
|
12
|
+
/** Tallest ledge the agent steps up. */
|
|
8
13
|
maxClimb: number;
|
|
14
|
+
/** Voxel size on XZ; defaults to half the agent radius. Smaller is more exact and slower. */
|
|
15
|
+
cellSize?: number;
|
|
16
|
+
/** Voxel size on Y; defaults to half the cell size. */
|
|
17
|
+
cellHeight?: number;
|
|
9
18
|
}
|
|
10
|
-
/**
|
|
19
|
+
/** Load the recast WebAssembly module once; await before the first {@link bakeNavMesh}. */
|
|
20
|
+
export declare function initNavBake(): Promise<void>;
|
|
21
|
+
/** Whether {@link initNavBake} has finished and {@link bakeNavMesh} can run synchronously. */
|
|
22
|
+
export declare function navBakeReady(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Voxelize indexed triangles with recast and return the walkable surface as convex polygons in the engine's
|
|
25
|
+
* serializable nav-mesh format: eroded by `agentRadius`, cut where clearance is under `agentHeight`, and split
|
|
26
|
+
* at slopes over `maxSlope` or steps over `maxClimb`. Requires {@link initNavBake}.
|
|
27
|
+
* @capability navmesh-bake voxelize scene triangles into an agent-sized polygon nav mesh
|
|
28
|
+
*/
|
|
11
29
|
export declare function bakeNavMesh(options: BakeNavMeshOptions): NavMeshData;
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,103 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { freeCompactHeightfield, freeContourSet, freeHeightfield, freePolyMesh, freePolyMeshDetail, init } from "recast-navigation";
|
|
2
|
+
import { generateSoloNavMesh } from "recast-navigation/generators";
|
|
3
|
+
let ready = false;
|
|
4
|
+
let loading = null;
|
|
5
|
+
/** Load the recast WebAssembly module once; await before the first {@link bakeNavMesh}. */
|
|
6
|
+
export function initNavBake() {
|
|
7
|
+
loading ??= init().then(() => {
|
|
8
|
+
ready = true;
|
|
9
|
+
}, (error) => {
|
|
10
|
+
loading = null;
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
13
|
+
return loading;
|
|
11
14
|
}
|
|
12
|
-
/**
|
|
15
|
+
/** Whether {@link initNavBake} has finished and {@link bakeNavMesh} can run synchronously. */
|
|
16
|
+
export function navBakeReady() {
|
|
17
|
+
return ready;
|
|
18
|
+
}
|
|
19
|
+
function freeIntermediates(intermediates) {
|
|
20
|
+
if (intermediates.heightfield)
|
|
21
|
+
freeHeightfield(intermediates.heightfield);
|
|
22
|
+
if (intermediates.compactHeightfield)
|
|
23
|
+
freeCompactHeightfield(intermediates.compactHeightfield);
|
|
24
|
+
if (intermediates.contourSet)
|
|
25
|
+
freeContourSet(intermediates.contourSet);
|
|
26
|
+
if (intermediates.polyMesh)
|
|
27
|
+
freePolyMesh(intermediates.polyMesh);
|
|
28
|
+
if (intermediates.polyMeshDetail)
|
|
29
|
+
freePolyMeshDetail(intermediates.polyMeshDetail);
|
|
30
|
+
}
|
|
31
|
+
function round(value) {
|
|
32
|
+
return Math.round(value * 1e4) / 1e4;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Voxelize indexed triangles with recast and return the walkable surface as convex polygons in the engine's
|
|
36
|
+
* serializable nav-mesh format: eroded by `agentRadius`, cut where clearance is under `agentHeight`, and split
|
|
37
|
+
* at slopes over `maxSlope` or steps over `maxClimb`. Requires {@link initNavBake}.
|
|
38
|
+
* @capability navmesh-bake voxelize scene triangles into an agent-sized polygon nav mesh
|
|
39
|
+
*/
|
|
13
40
|
export function bakeNavMesh(options) {
|
|
41
|
+
if (!ready)
|
|
42
|
+
throw new Error("bakeNavMesh: await initNavBake() before baking");
|
|
14
43
|
if (options.positions.length % 3 !== 0)
|
|
15
44
|
throw new Error("positions must contain xyz triples");
|
|
16
45
|
if (options.indices.length % 3 !== 0)
|
|
17
46
|
throw new Error("indices must contain triangle triples");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
47
|
+
if (!(options.agentRadius >= 0) || !(options.agentHeight > 0) || !(options.maxClimb >= 0)) {
|
|
48
|
+
throw new Error("agentRadius and maxClimb must be non-negative and agentHeight positive");
|
|
49
|
+
}
|
|
50
|
+
const vertexCount = options.positions.length / 3;
|
|
51
|
+
for (const index of options.indices) {
|
|
52
|
+
if (!Number.isInteger(index) || index < 0 || index >= vertexCount)
|
|
53
|
+
throw new Error(`index ${index} is out of range`);
|
|
54
|
+
}
|
|
55
|
+
if (options.indices.length === 0)
|
|
56
|
+
return { verts: [], polys: [], links: [] };
|
|
57
|
+
const cs = options.cellSize ?? Math.max(0.05, options.agentRadius / 2);
|
|
58
|
+
const ch = options.cellHeight ?? cs / 2;
|
|
59
|
+
const result = generateSoloNavMesh(options.positions, options.indices, {
|
|
60
|
+
cs,
|
|
61
|
+
ch,
|
|
62
|
+
walkableRadius: Math.ceil(options.agentRadius / cs),
|
|
63
|
+
walkableHeight: Math.ceil(options.agentHeight / ch),
|
|
64
|
+
walkableClimb: Math.floor(options.maxClimb / ch),
|
|
65
|
+
walkableSlopeAngle: Math.min(90, Math.max(0, options.maxSlope)),
|
|
66
|
+
maxVertsPerPoly: 6,
|
|
67
|
+
}, true);
|
|
68
|
+
// Detour refuses to build from zero polygons, so an all-unwalkable input is told apart from a real failure here.
|
|
69
|
+
const walkable = (result.intermediates.polyMesh?.npolys() ?? 0) > 0;
|
|
70
|
+
freeIntermediates(result.intermediates);
|
|
71
|
+
if (!result.success) {
|
|
72
|
+
if (!walkable)
|
|
73
|
+
return { verts: [], polys: [], links: [] };
|
|
74
|
+
throw new Error(`bakeNavMesh: ${result.error}`);
|
|
75
|
+
}
|
|
76
|
+
const navMesh = result.navMesh;
|
|
77
|
+
try {
|
|
78
|
+
const verts = [];
|
|
79
|
+
const polys = [];
|
|
80
|
+
for (let t = 0; t < navMesh.getMaxTiles(); t += 1) {
|
|
81
|
+
const tile = navMesh.getTile(t);
|
|
82
|
+
const header = tile.header();
|
|
83
|
+
if (header === null)
|
|
84
|
+
continue;
|
|
85
|
+
const base = verts.length / 3;
|
|
86
|
+
for (let i = 0; i < header.vertCount() * 3; i += 1)
|
|
87
|
+
verts.push(round(tile.verts(i)));
|
|
88
|
+
for (let p = 0; p < header.polyCount(); p += 1) {
|
|
89
|
+
const poly = tile.polys(p);
|
|
90
|
+
if (poly.getType() !== 0)
|
|
91
|
+
continue;
|
|
92
|
+
const ring = [];
|
|
93
|
+
for (let k = 0; k < poly.vertCount(); k += 1)
|
|
94
|
+
ring.push(base + poly.verts(k));
|
|
95
|
+
polys.push(ring);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { verts, polys, links: [] };
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
navMesh.destroy();
|
|
25
102
|
}
|
|
26
|
-
return { verts: Array.from(options.positions), polys, links: [] };
|
|
27
103
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/navbake",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Deterministic navigation-mesh baking for JGengine scenes.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
|
-
"repository": {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Noisemaker111/jgengine.git",
|
|
11
|
+
"directory": "packages/navbake"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"NOTICE"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./*": {
|
|
25
|
+
"types": "./dist/*.d.ts",
|
|
26
|
+
"default": "./dist/*.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
|
|
31
|
+
"check-types": "bun ../../scripts/check-types-preflight.ts && tsgo --noEmit -p tsconfig.json",
|
|
32
|
+
"test": "bun test src"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@jgengine/core": "^0.19.0",
|
|
36
|
+
"recast-navigation": "^0.34.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
14
41
|
}
|