@jgengine/navbake 0.18.0 → 0.21.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 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
- /** Bake indexed walkable triangles into the engine's serializable nav-mesh format. */
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
- function triangleSlope(positions, a, b, c) {
2
- const ax = positions[a * 3], ay = positions[a * 3 + 1], az = positions[a * 3 + 2];
3
- const bx = positions[b * 3], by = positions[b * 3 + 1], bz = positions[b * 3 + 2];
4
- const cx = positions[c * 3], cy = positions[c * 3 + 1], cz = positions[c * 3 + 2];
5
- const ux = bx - ax, uy = by - ay, uz = bz - az;
6
- const vx = cx - ax, vy = cy - ay, vz = cz - az;
7
- const ny = ux * vz - uz * vx;
8
- const nx = uy * vz - uz * vy;
9
- const nz = ux * vy - uy * vx;
10
- return Math.atan2(Math.hypot(nx, nz), Math.abs(ny)) * 180 / Math.PI;
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
- /** Bake indexed walkable triangles into the engine's serializable nav-mesh format. */
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
- const polys = [];
19
- for (let i = 0; i < options.indices.length; i += 3) {
20
- const a = options.indices[i], b = options.indices[i + 1], c = options.indices[i + 2];
21
- if (a < 0 || b < 0 || c < 0 || a * 3 + 2 >= options.positions.length || b * 3 + 2 >= options.positions.length || c * 3 + 2 >= options.positions.length)
22
- continue;
23
- if (triangleSlope(options.positions, a, b, c) <= Math.max(0, options.maxSlope))
24
- polys.push([a, b, c]);
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.18.0",
3
+ "version": "0.21.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": { "type": "git", "url": "git+https://github.com/Noisemaker111/jgengine.git", "directory": "packages/navbake" },
9
- "files": ["dist", "README.md", "LICENSE", "NOTICE"],
10
- "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./*": { "types": "./dist/*.d.ts", "default": "./dist/*.js" } },
11
- "scripts": { "build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist", "check-types": "bun ../../scripts/check-types-preflight.ts && tsgo --noEmit -p tsconfig.json", "test": "bun test src" },
12
- "dependencies": { "@jgengine/core": "^0.18.0", "recast-navigation": "^0.34.0" },
13
- "publishConfig": { "access": "public" }
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.21.0",
36
+ "recast-navigation": "^0.34.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
14
41
  }