@woosh/meep-engine 2.163.5 → 2.163.6
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/package.json +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts +16 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +144 -0
- package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
- package/src/engine/navigation/mesh/build/navmesh_build_topology.js +13 -0
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"description": "Pure JavaScript game engine. Fully featured and production ready.",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"author": "Alexander Goldring",
|
|
9
|
-
"version": "2.163.
|
|
9
|
+
"version": "2.163.6",
|
|
10
10
|
"main": "build/meep.module.js",
|
|
11
11
|
"module": "build/meep.module.js",
|
|
12
12
|
"exports": {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fill boundary holes that are too thin for the agent to occupy.
|
|
3
|
+
*
|
|
4
|
+
* Erosion/clearance can leave a hair-thin sliver hole hugging an obstacle's offset edge (a strip of
|
|
5
|
+
* cells that are actually clear but get pinched off into their own tiny loop). Such a hole is narrower
|
|
6
|
+
* than the agent, so it can never be a real navigation obstacle - it is numerical noise that inflates
|
|
7
|
+
* the boundary, the face count, and the loop count. This fills any inner loop whose mean width
|
|
8
|
+
* (2*area/perimeter, the inradius of an equivalent disc) is below the agent radius. The single
|
|
9
|
+
* largest-area loop (the outer boundary) is never filled.
|
|
10
|
+
*
|
|
11
|
+
* @param {BinaryTopology} mesh
|
|
12
|
+
* @param {number} agent_radius
|
|
13
|
+
* @returns {number} number of holes filled
|
|
14
|
+
*/
|
|
15
|
+
export function bt_mesh_fill_small_holes(mesh: BinaryTopology, agent_radius: number): number;
|
|
16
|
+
//# sourceMappingURL=bt_mesh_fill_small_holes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bt_mesh_fill_small_holes.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js"],"names":[],"mappings":"AA+FA;;;;;;;;;;;;;GAaG;AACH,6EAHW,MAAM,GACJ,MAAM,CAoClB"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { bt_mesh_walk_boundary_loops } from "../query/bt_mesh_walk_boundary_loops.js";
|
|
2
|
+
import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
|
|
3
|
+
import { bt_radial_loop_add } from "./bt_radial_loop_add.js";
|
|
4
|
+
|
|
5
|
+
const _c = [0, 0, 0];
|
|
6
|
+
const _p = [0, 0, 0];
|
|
7
|
+
const _q = [0, 0, 0];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Order a boundary cycle's edges into a vertex/edge walk starting at `start_vertex`.
|
|
11
|
+
* (Same traversal as bt_mesh_close_boundary_holes.)
|
|
12
|
+
*/
|
|
13
|
+
function order_cycle_from(mesh, cycle, start_vertex, out_vertices, out_edges) {
|
|
14
|
+
const n = cycle.length;
|
|
15
|
+
const v_to_edges = new Map();
|
|
16
|
+
for (let i = 0; i < n; i++) {
|
|
17
|
+
const e = cycle[i];
|
|
18
|
+
const v1 = mesh.edge_read_vertex1(e);
|
|
19
|
+
const v2 = mesh.edge_read_vertex2(e);
|
|
20
|
+
let bucket = v_to_edges.get(v1);
|
|
21
|
+
if (bucket === undefined) { bucket = []; v_to_edges.set(v1, bucket); }
|
|
22
|
+
bucket.push(e);
|
|
23
|
+
bucket = v_to_edges.get(v2);
|
|
24
|
+
if (bucket === undefined) { bucket = []; v_to_edges.set(v2, bucket); }
|
|
25
|
+
bucket.push(e);
|
|
26
|
+
}
|
|
27
|
+
let v_curr = start_vertex;
|
|
28
|
+
let e_prev = -1;
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
out_vertices[i] = v_curr;
|
|
31
|
+
const edges_at_v = v_to_edges.get(v_curr);
|
|
32
|
+
const e_next = (edges_at_v[0] === e_prev) ? edges_at_v[1] : edges_at_v[0];
|
|
33
|
+
out_edges[i] = e_next;
|
|
34
|
+
const e_v1 = mesh.edge_read_vertex1(e_next);
|
|
35
|
+
const e_v2 = mesh.edge_read_vertex2(e_next);
|
|
36
|
+
v_curr = (e_v1 === v_curr) ? e_v2 : e_v1;
|
|
37
|
+
e_prev = e_next;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Perimeter and (Newell) 3D area of an ordered loop of vertices. */
|
|
42
|
+
function loop_perimeter_and_area(mesh, verts) {
|
|
43
|
+
const n = verts.length;
|
|
44
|
+
let perimeter = 0;
|
|
45
|
+
let nx = 0, ny = 0, nz = 0;
|
|
46
|
+
for (let i = 0; i < n; i++) {
|
|
47
|
+
mesh.vertex_read_coordinate(_p, 0, verts[i]);
|
|
48
|
+
mesh.vertex_read_coordinate(_q, 0, verts[(i + 1) % n]);
|
|
49
|
+
perimeter += Math.hypot(_q[0] - _p[0], _q[1] - _p[1], _q[2] - _p[2]);
|
|
50
|
+
// Newell's method for polygon area normal
|
|
51
|
+
nx += (_p[1] - _q[1]) * (_p[2] + _q[2]);
|
|
52
|
+
ny += (_p[2] - _q[2]) * (_p[0] + _q[0]);
|
|
53
|
+
nz += (_p[0] - _q[0]) * (_p[1] + _q[1]);
|
|
54
|
+
}
|
|
55
|
+
const area = 0.5 * Math.hypot(nx, ny, nz);
|
|
56
|
+
return { perimeter, area };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Fan-fill a single boundary loop (n-gon face + centroid poke), as close_boundary_holes does. */
|
|
60
|
+
function fill_loop(mesh, cycle) {
|
|
61
|
+
const n = cycle.length;
|
|
62
|
+
const seed_edge = cycle[0];
|
|
63
|
+
const seed_loop = mesh.edge_read_loop(seed_edge);
|
|
64
|
+
const existing_loop_vertex = mesh.loop_read_vertex(seed_loop);
|
|
65
|
+
const seed_v1 = mesh.edge_read_vertex1(seed_edge);
|
|
66
|
+
const seed_v2 = mesh.edge_read_vertex2(seed_edge);
|
|
67
|
+
const start_vertex = (existing_loop_vertex === seed_v1) ? seed_v2 : seed_v1;
|
|
68
|
+
|
|
69
|
+
const ordered_vertices = new Array(n);
|
|
70
|
+
const ordered_edges = new Array(n);
|
|
71
|
+
order_cycle_from(mesh, cycle, start_vertex, ordered_vertices, ordered_edges);
|
|
72
|
+
|
|
73
|
+
let cx = 0, cy = 0, cz = 0;
|
|
74
|
+
for (let i = 0; i < n; i++) {
|
|
75
|
+
mesh.vertex_read_coordinate(_c, 0, ordered_vertices[i]);
|
|
76
|
+
cx += _c[0]; cy += _c[1]; cz += _c[2];
|
|
77
|
+
}
|
|
78
|
+
cx /= n; cy /= n; cz /= n;
|
|
79
|
+
|
|
80
|
+
const new_face = mesh.faces.allocate();
|
|
81
|
+
const loops = new Array(n);
|
|
82
|
+
for (let i = 0; i < n; i++) {
|
|
83
|
+
loops[i] = mesh.loop_create();
|
|
84
|
+
mesh.loop_write_vertex(loops[i], ordered_vertices[i]);
|
|
85
|
+
mesh.loop_write_face(loops[i], new_face);
|
|
86
|
+
bt_radial_loop_add(mesh, loops[i], ordered_edges[i]);
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < n; i++) {
|
|
89
|
+
mesh.loop_write_next(loops[i], loops[(i + 1) % n]);
|
|
90
|
+
mesh.loop_write_prev(loops[i], loops[(i - 1 + n) % n]);
|
|
91
|
+
}
|
|
92
|
+
mesh.face_write_loop(new_face, loops[0]);
|
|
93
|
+
bt_mesh_face_poke(mesh, new_face, cx, cy, cz);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fill boundary holes that are too thin for the agent to occupy.
|
|
98
|
+
*
|
|
99
|
+
* Erosion/clearance can leave a hair-thin sliver hole hugging an obstacle's offset edge (a strip of
|
|
100
|
+
* cells that are actually clear but get pinched off into their own tiny loop). Such a hole is narrower
|
|
101
|
+
* than the agent, so it can never be a real navigation obstacle - it is numerical noise that inflates
|
|
102
|
+
* the boundary, the face count, and the loop count. This fills any inner loop whose mean width
|
|
103
|
+
* (2*area/perimeter, the inradius of an equivalent disc) is below the agent radius. The single
|
|
104
|
+
* largest-area loop (the outer boundary) is never filled.
|
|
105
|
+
*
|
|
106
|
+
* @param {BinaryTopology} mesh
|
|
107
|
+
* @param {number} agent_radius
|
|
108
|
+
* @returns {number} number of holes filled
|
|
109
|
+
*/
|
|
110
|
+
export function bt_mesh_fill_small_holes(mesh, agent_radius) {
|
|
111
|
+
if (agent_radius <= 0) return 0;
|
|
112
|
+
|
|
113
|
+
const cycles = bt_mesh_walk_boundary_loops(mesh);
|
|
114
|
+
if (cycles.length <= 1) return 0;
|
|
115
|
+
|
|
116
|
+
// measure every loop; the largest-area one is the outer boundary and is never filled
|
|
117
|
+
const measures = cycles.map(c => {
|
|
118
|
+
const n = c.length;
|
|
119
|
+
const verts = new Array(n);
|
|
120
|
+
const edges = new Array(n);
|
|
121
|
+
const v1 = mesh.edge_read_vertex1(c[0]);
|
|
122
|
+
order_cycle_from(mesh, c, v1, verts, edges);
|
|
123
|
+
return loop_perimeter_and_area(mesh, verts);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
let outer = 0;
|
|
127
|
+
for (let i = 1; i < measures.length; i++) {
|
|
128
|
+
if (measures[i].area > measures[outer].area) outer = i;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let filled = 0;
|
|
132
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
133
|
+
if (i === outer) continue;
|
|
134
|
+
const { perimeter, area } = measures[i];
|
|
135
|
+
if (perimeter <= 0) continue;
|
|
136
|
+
// mean width of the hole = 2*area/perimeter; fill when the agent disc cannot fit
|
|
137
|
+
if (2 * area / perimeter < agent_radius) {
|
|
138
|
+
fill_loop(mesh, cycles[i]);
|
|
139
|
+
filled++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return filled;
|
|
144
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"navmesh_build_topology.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/navigation/mesh/build/navmesh_build_topology.js"],"names":[],"mappings":"AAiDA;;;;;;;;;;GAUG;AACH,wKATW,cAAc,QAmMxB;+BApP8B,mEAAmE"}
|
|
@@ -42,6 +42,9 @@ import { bt_mesh_carve_height_clearance } from "./bt_mesh_carve_height_clearance
|
|
|
42
42
|
import {
|
|
43
43
|
bt_mesh_resolve_t_junctions
|
|
44
44
|
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js";
|
|
45
|
+
import {
|
|
46
|
+
bt_mesh_fill_small_holes
|
|
47
|
+
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js";
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
/**
|
|
@@ -224,6 +227,16 @@ export function navmesh_build_topology({
|
|
|
224
227
|
bt_mesh_compact(mesh);
|
|
225
228
|
}
|
|
226
229
|
|
|
230
|
+
// Fill hair-thin sliver holes left by erosion/clearance around obstacle offsets. They are
|
|
231
|
+
// narrower than the agent (it could never fit inside them), so they are numerical noise rather
|
|
232
|
+
// than real obstacles - removing them restores wrongly-culled walkable area and keeps the
|
|
233
|
+
// boundary clean (one outer loop + one loop per genuine obstacle).
|
|
234
|
+
if (agent_radius > 0) {
|
|
235
|
+
if (bt_mesh_fill_small_holes(mesh, agent_radius) > 0) {
|
|
236
|
+
bt_mesh_compact(mesh);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
227
240
|
// face normals are consumed by navigation queries (string-pulling portal normals), populate them now
|
|
228
241
|
bt_mesh_compute_face_normals(mesh);
|
|
229
242
|
|