@woosh/meep-engine 2.163.7 → 2.163.8
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/2d/line/line_segment_intersection_fraction_2d.d.ts +21 -0
- package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.d.ts.map +1 -0
- package/src/core/geom/2d/line/line_segment_intersection_fraction_2d.js +42 -0
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts +2 -2
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js +120 -179
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts +9 -10
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +12 -13
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.js +45 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.d.ts +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_euclidean_distance_field.js +84 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js +53 -78
- package/src/core/geom/vec3/v3_matrix3_rotate.d.ts +16 -0
- package/src/core/geom/vec3/v3_matrix3_rotate.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_matrix3_rotate.js +49 -0
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts +2 -2
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_orthonormal_matrix_from_normal.js +46 -46
- package/src/engine/graphics/sh3/path_tracer/sampling/getBiasedNormalSample.d.ts.map +1 -1
- package/src/engine/graphics/sh3/path_tracer/sampling/getBiasedNormalSample.js +6 -28
- package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +185 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts +11 -0
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
- package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +623 -100
- package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.d.ts.map +1 -1
- package/src/engine/navigation/mesh/build/clip_soup_against_overhangs.js +354 -138
- package/src/engine/navigation/mesh/navmesh_polyanya_find_path.d.ts +17 -0
- package/src/engine/navigation/mesh/navmesh_polyanya_find_path.d.ts.map +1 -0
- package/src/engine/navigation/mesh/navmesh_polyanya_find_path.js +613 -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.8",
|
|
10
10
|
"main": "build/meep.module.js",
|
|
11
11
|
"module": "build/meep.module.js",
|
|
12
12
|
"exports": {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fraction `t` in [0,1] along the segment (s0 -> s1) at which the infinite line through (l0, l1)
|
|
3
|
+
* crosses it. Returns NaN when the segment lies entirely on one side of the line (no crossing within
|
|
4
|
+
* the segment) or the two are parallel/collinear.
|
|
5
|
+
*
|
|
6
|
+
* Unlike {@link line_segment_compute_line_segment_intersection_array_2d}, the first pair of points
|
|
7
|
+
* defines an unbounded line (not a segment) and the result is the parametric position on the segment
|
|
8
|
+
* rather than the intersection point.
|
|
9
|
+
*
|
|
10
|
+
* @param {number} l0_x line point A x
|
|
11
|
+
* @param {number} l0_y line point A y
|
|
12
|
+
* @param {number} l1_x line point B x
|
|
13
|
+
* @param {number} l1_y line point B y
|
|
14
|
+
* @param {number} s0_x segment start x
|
|
15
|
+
* @param {number} s0_y segment start y
|
|
16
|
+
* @param {number} s1_x segment end x
|
|
17
|
+
* @param {number} s1_y segment end y
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
export function line_segment_intersection_fraction_2d(l0_x: number, l0_y: number, l1_x: number, l1_y: number, s0_x: number, s0_y: number, s1_x: number, s1_y: number): number;
|
|
21
|
+
//# sourceMappingURL=line_segment_intersection_fraction_2d.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"line_segment_intersection_fraction_2d.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/2d/line/line_segment_intersection_fraction_2d.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,4DAVW,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,QACN,MAAM,GACJ,MAAM,CAsBlB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { v2_cross_product } from "../../vec2/v2_cross_product.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fraction `t` in [0,1] along the segment (s0 -> s1) at which the infinite line through (l0, l1)
|
|
5
|
+
* crosses it. Returns NaN when the segment lies entirely on one side of the line (no crossing within
|
|
6
|
+
* the segment) or the two are parallel/collinear.
|
|
7
|
+
*
|
|
8
|
+
* Unlike {@link line_segment_compute_line_segment_intersection_array_2d}, the first pair of points
|
|
9
|
+
* defines an unbounded line (not a segment) and the result is the parametric position on the segment
|
|
10
|
+
* rather than the intersection point.
|
|
11
|
+
*
|
|
12
|
+
* @param {number} l0_x line point A x
|
|
13
|
+
* @param {number} l0_y line point A y
|
|
14
|
+
* @param {number} l1_x line point B x
|
|
15
|
+
* @param {number} l1_y line point B y
|
|
16
|
+
* @param {number} s0_x segment start x
|
|
17
|
+
* @param {number} s0_y segment start y
|
|
18
|
+
* @param {number} s1_x segment end x
|
|
19
|
+
* @param {number} s1_y segment end y
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
export function line_segment_intersection_fraction_2d(
|
|
23
|
+
l0_x, l0_y, l1_x, l1_y,
|
|
24
|
+
s0_x, s0_y, s1_x, s1_y
|
|
25
|
+
) {
|
|
26
|
+
const dir_x = l1_x - l0_x;
|
|
27
|
+
const dir_y = l1_y - l0_y;
|
|
28
|
+
|
|
29
|
+
const side_0 = v2_cross_product(dir_x, dir_y, s0_x - l0_x, s0_y - l0_y);
|
|
30
|
+
const side_1 = v2_cross_product(dir_x, dir_y, s1_x - l0_x, s1_y - l0_y);
|
|
31
|
+
|
|
32
|
+
if ((side_0 > 0 && side_1 > 0) || (side_0 < 0 && side_1 < 0)) {
|
|
33
|
+
return NaN; // both endpoints on the same side -> no crossing within the segment
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const denom = side_0 - side_1;
|
|
37
|
+
if (denom === 0) {
|
|
38
|
+
return NaN; // parallel / collinear
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return side_0 / denom;
|
|
42
|
+
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* The erosion distance may be smaller than the smallest triangle height in the island, or it may be significantly larger - there is no restriction.
|
|
9
9
|
*
|
|
10
10
|
* @param {BinaryTopology} mesh
|
|
11
|
-
* @param {number[]} faces
|
|
12
|
-
* @param {number} erode_distance
|
|
11
|
+
* @param {number[]} faces seed faces of the island to erode
|
|
12
|
+
* @param {number} erode_distance distance to move the outline inwards by
|
|
13
13
|
*/
|
|
14
14
|
export function bt_mesh_face_island_erode(mesh: BinaryTopology, faces: number[], erode_distance: number): void;
|
|
15
15
|
//# sourceMappingURL=bt_mesh_face_island_erode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bt_mesh_face_island_erode.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bt_mesh_face_island_erode.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;GAYG;AACH,uEAHW,MAAM,EAAE,kBACR,MAAM,QAqbhB"}
|
|
@@ -5,34 +5,18 @@ import {
|
|
|
5
5
|
line3_compute_segment_point_distance_eikonal
|
|
6
6
|
} from "../../../../line/line3_compute_segment_point_distance_eikonal.js";
|
|
7
7
|
import { NULL_POINTER } from "../BinaryTopology.js";
|
|
8
|
+
import { BitSet } from "../../../../../../binary/BitSet.js";
|
|
8
9
|
import { bt_face_get_incenter } from "../query/bt_face_get_incenter.js";
|
|
10
|
+
import { bt_face_island_flood_fill } from "../query/bt_face_island_flood_fill.js";
|
|
11
|
+
import {
|
|
12
|
+
bt_collect_boundary_segments,
|
|
13
|
+
bt_point_distance_to_segments,
|
|
14
|
+
bt_mesh_build_boundary_euclidean_distance_field
|
|
15
|
+
} from "../query/bt_mesh_build_boundary_euclidean_distance_field.js";
|
|
9
16
|
import { bt_mesh_cleanup_faceless_references } from "./bt_mesh_cleanup_faceless_references.js";
|
|
10
17
|
import { bt_edge_split } from "./edge/bt_edge_split.js";
|
|
11
18
|
import { bt_face_kill } from "./face/bt_face_kill.js";
|
|
12
19
|
import { bt_mesh_face_poke } from "./face/bt_face_poke.js";
|
|
13
|
-
import { bt_face_get_neighbour_faces } from "../query/bt_face_get_neighbour_faces.js";
|
|
14
|
-
|
|
15
|
-
const _eb_a = new Float32Array(3);
|
|
16
|
-
const _eb_b = new Float32Array(3);
|
|
17
|
-
const _eb_p = new Float32Array(3);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Squared straight-line distance from point p to segment a-b in 3D.
|
|
21
|
-
*/
|
|
22
|
-
function point_segment_distance_sq(px, py, pz, ax, ay, az, bx, by, bz) {
|
|
23
|
-
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
24
|
-
const apx = px - ax, apy = py - ay, apz = pz - az;
|
|
25
|
-
const ab_len_sq = abx * abx + aby * aby + abz * abz;
|
|
26
|
-
|
|
27
|
-
let t = ab_len_sq > 0 ? (apx * abx + apy * aby + apz * abz) / ab_len_sq : 0;
|
|
28
|
-
if (t < 0) t = 0; else if (t > 1) t = 1;
|
|
29
|
-
|
|
30
|
-
const cx = ax + t * abx - px;
|
|
31
|
-
const cy = ay + t * aby - py;
|
|
32
|
-
const cz = az + t * abz - pz;
|
|
33
|
-
|
|
34
|
-
return cx * cx + cy * cy + cz * cz;
|
|
35
|
-
}
|
|
36
20
|
|
|
37
21
|
/**
|
|
38
22
|
* Shrinks an island of faces by a given distance.
|
|
@@ -44,8 +28,8 @@ function point_segment_distance_sq(px, py, pz, ax, ay, az, bx, by, bz) {
|
|
|
44
28
|
* The erosion distance may be smaller than the smallest triangle height in the island, or it may be significantly larger - there is no restriction.
|
|
45
29
|
*
|
|
46
30
|
* @param {BinaryTopology} mesh
|
|
47
|
-
* @param {number[]} faces
|
|
48
|
-
* @param {number} erode_distance
|
|
31
|
+
* @param {number[]} faces seed faces of the island to erode
|
|
32
|
+
* @param {number} erode_distance distance to move the outline inwards by
|
|
49
33
|
*/
|
|
50
34
|
export function bt_mesh_face_island_erode(
|
|
51
35
|
mesh,
|
|
@@ -58,11 +42,25 @@ export function bt_mesh_face_island_erode(
|
|
|
58
42
|
return;
|
|
59
43
|
}
|
|
60
44
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
45
|
+
// Reused island working sets. collect_island_sets clears and refills these in place, so the same
|
|
46
|
+
// four Set objects serve the whole erosion (which re-derives them many times) without allocating.
|
|
47
|
+
const island_vertices = new Set();
|
|
48
|
+
const island_edges = new Set();
|
|
49
|
+
const boundary_vertices = new Set();
|
|
50
|
+
const boundary_edges = new Set();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Refill the four island working sets for `island_faces`: every vertex and edge the island touches,
|
|
54
|
+
* plus the subset of each that lies on the boundary (an edge whose radial cycle is a single loop).
|
|
55
|
+
* Clears the sets first so their backing storage is reused across calls.
|
|
56
|
+
*
|
|
57
|
+
* @param {number[]} island_faces allocated face IDs making up the island
|
|
58
|
+
*/
|
|
63
59
|
function collect_island_sets(island_faces) {
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
island_vertices.clear();
|
|
61
|
+
island_edges.clear();
|
|
62
|
+
boundary_vertices.clear();
|
|
63
|
+
boundary_edges.clear();
|
|
66
64
|
|
|
67
65
|
for (let i = 0; i < island_faces.length; i++) {
|
|
68
66
|
const f = island_faces[i];
|
|
@@ -71,64 +69,44 @@ export function bt_mesh_face_island_erode(
|
|
|
71
69
|
|
|
72
70
|
let l_curr = l_first;
|
|
73
71
|
do {
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
island_vertices.add(mesh.loop_read_vertex(l_curr));
|
|
73
|
+
island_edges.add(mesh.loop_read_edge(l_curr));
|
|
76
74
|
l_curr = mesh.loop_read_next(l_curr);
|
|
77
75
|
} while (l_curr !== l_first);
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
const
|
|
81
|
-
const be = new Set();
|
|
82
|
-
|
|
83
|
-
for (const e of ie) {
|
|
78
|
+
for (const e of island_edges) {
|
|
84
79
|
const l = mesh.edge_read_loop(e);
|
|
85
80
|
if (mesh.loop_read_radial_next(l) === l) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
boundary_vertices.add(mesh.edge_read_vertex1(e));
|
|
82
|
+
boundary_vertices.add(mesh.edge_read_vertex2(e));
|
|
83
|
+
boundary_edges.add(e);
|
|
89
84
|
}
|
|
90
85
|
}
|
|
91
|
-
|
|
92
|
-
return { island_vertices: iv, island_edges: ie, boundary_vertices: bv, boundary_edges_set: be };
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
function recompute_island_faces(seed_faces) {
|
|
99
|
-
const seen = new Set();
|
|
100
|
-
const stack = [];
|
|
101
|
-
const result = [];
|
|
102
|
-
const nbr = [];
|
|
103
|
-
|
|
104
|
-
for (let i = 0; i < seed_faces.length; i++) {
|
|
105
|
-
const f = seed_faces[i];
|
|
106
|
-
if (mesh.faces.is_allocated(f)) {
|
|
107
|
-
stack.push(f);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
while (stack.length > 0) {
|
|
112
|
-
const f = stack.pop();
|
|
113
|
-
if (seen.has(f)) continue;
|
|
114
|
-
seen.add(f);
|
|
115
|
-
result.push(f);
|
|
116
|
-
|
|
117
|
-
const n = bt_face_get_neighbour_faces(nbr, 0, mesh, f);
|
|
118
|
-
for (let i = 0; i < n; i++) {
|
|
119
|
-
const g = nbr[i];
|
|
120
|
-
if (!seen.has(g)) {
|
|
121
|
-
stack.push(g);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
88
|
+
// Reused buffers for re-deriving the island's faces after edge splits.
|
|
89
|
+
const flood_visited = new BitSet();
|
|
90
|
+
const island_faces_scratch = [];
|
|
125
91
|
|
|
126
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Re-derive the (now larger) face list of this island after edge splits, by flood-filling
|
|
94
|
+
* edge-adjacency from the original seed faces. {@link bt_edge_split} reuses the original face IDs
|
|
95
|
+
* for one half of each split, so the seeds stay allocated and edge-connected to the new fragments.
|
|
96
|
+
* Returns a reused buffer - the caller must not retain it across calls.
|
|
97
|
+
*
|
|
98
|
+
* @param {number[]} seed_faces original island face IDs
|
|
99
|
+
* @returns {number[]} the island's current faces (reused buffer)
|
|
100
|
+
*/
|
|
101
|
+
function recompute_island_faces(seed_faces) {
|
|
102
|
+
flood_visited.reset();
|
|
103
|
+
island_faces_scratch.length = 0;
|
|
104
|
+
bt_face_island_flood_fill(mesh, seed_faces, island_faces_scratch, flood_visited);
|
|
105
|
+
return island_faces_scratch;
|
|
127
106
|
}
|
|
128
107
|
|
|
129
108
|
let island_faces = faces;
|
|
130
|
-
|
|
131
|
-
let { island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces);
|
|
109
|
+
collect_island_sets(island_faces);
|
|
132
110
|
|
|
133
111
|
if (boundary_vertices.size === 0) {
|
|
134
112
|
// no boundary vertices found
|
|
@@ -137,23 +115,23 @@ export function bt_mesh_face_island_erode(
|
|
|
137
115
|
}
|
|
138
116
|
|
|
139
117
|
// ---------------------------------------------------------
|
|
140
|
-
//
|
|
118
|
+
// CONNECTIVITY PRE-PASS (fix for erosion fragmentation)
|
|
141
119
|
// ---------------------------------------------------------
|
|
142
|
-
// The
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
120
|
+
// The distance field is sampled only at vertices and interpolated linearly along edges. An
|
|
121
|
+
// *interior* edge whose BOTH endpoints lie on the boundary therefore reads as ~0 along its entire
|
|
122
|
+
// length, even where its middle sits deep inside the island (the shared diagonal of a coarse quad,
|
|
123
|
+
// or the seam between two large coplanar tiles). Erosion then culls a band straddling that edge and
|
|
124
|
+
// severs one connected island into disconnected pieces.
|
|
147
125
|
//
|
|
148
126
|
// Fix: insert an interior vertex at the midpoint of every such edge. The new vertex is genuinely
|
|
149
|
-
// interior, so
|
|
150
|
-
//
|
|
151
|
-
//
|
|
127
|
+
// interior, so it gets the correct (large) clearance and the contour keeps the deep interior alive,
|
|
128
|
+
// preserving connectivity. A single pass suffices: splitting a boundary-boundary interior edge
|
|
129
|
+
// yields only boundary-interior edges, creating no new offenders.
|
|
152
130
|
{
|
|
153
131
|
const split_targets = [];
|
|
154
132
|
|
|
155
133
|
for (const e of island_edges) {
|
|
156
|
-
if (
|
|
134
|
+
if (boundary_edges.has(e)) {
|
|
157
135
|
// a true boundary edge defines the outline - never split it
|
|
158
136
|
continue;
|
|
159
137
|
}
|
|
@@ -172,8 +150,7 @@ export function bt_mesh_face_island_erode(
|
|
|
172
150
|
}
|
|
173
151
|
|
|
174
152
|
island_faces = recompute_island_faces(faces);
|
|
175
|
-
|
|
176
|
-
({ island_vertices, island_edges, boundary_vertices, boundary_edges_set } = collect_island_sets(island_faces));
|
|
153
|
+
collect_island_sets(island_faces);
|
|
177
154
|
|
|
178
155
|
if (boundary_vertices.size === 0) {
|
|
179
156
|
return;
|
|
@@ -181,57 +158,34 @@ export function bt_mesh_face_island_erode(
|
|
|
181
158
|
}
|
|
182
159
|
}
|
|
183
160
|
|
|
184
|
-
//
|
|
161
|
+
// Distance field: EXACT straight-line distance from each island vertex to the nearest boundary
|
|
185
162
|
// edge. Agent clearance is a Euclidean (line-of-sight) distance to the nearest wall, NOT a geodesic
|
|
186
|
-
// distance measured along the surface
|
|
187
|
-
// sensitive to the triangulation
|
|
188
|
-
//
|
|
163
|
+
// distance measured along the surface, so the eikonal field would be both semantically wrong here
|
|
164
|
+
// and sensitive to the triangulation (a lumpy, non-uniform inset). See
|
|
165
|
+
// bt_mesh_build_boundary_euclidean_distance_field.
|
|
189
166
|
/**
|
|
190
|
-
* Vertex ->
|
|
167
|
+
* Vertex -> distance from boundary.
|
|
191
168
|
* @type {Map<number, number>}
|
|
192
169
|
*/
|
|
193
170
|
const vertex_boundary_distances = new Map();
|
|
194
171
|
|
|
195
|
-
// boundary
|
|
196
|
-
|
|
172
|
+
// Flat boundary segment buffer (6 numbers per segment), reused across every field rebuild.
|
|
173
|
+
const boundary_segments = [];
|
|
197
174
|
|
|
198
|
-
// (
|
|
175
|
+
// (Re)compute the exact distance field for the current island vertices and boundary.
|
|
199
176
|
function compute_field() {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
mesh.vertex_read_coordinate(_eb_b, 0, bv2);
|
|
208
|
-
boundary_segments.push(_eb_a[0], _eb_a[1], _eb_a[2], _eb_b[0], _eb_b[1], _eb_b[2]);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
for (const v of island_vertices) {
|
|
212
|
-
mesh.vertex_read_coordinate(_eb_p, 0, v);
|
|
213
|
-
vertex_boundary_distances.set(v, dist_to_boundary(_eb_p[0], _eb_p[1], _eb_p[2]));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// exact straight-line distance from a point to the nearest boundary edge
|
|
218
|
-
function dist_to_boundary(px, py, pz) {
|
|
219
|
-
const seg = boundary_segments;
|
|
220
|
-
let best_sq = Infinity;
|
|
221
|
-
for (let i = 0; i < seg.length; i += 6) {
|
|
222
|
-
const d_sq = point_segment_distance_sq(
|
|
223
|
-
px, py, pz,
|
|
224
|
-
seg[i], seg[i + 1], seg[i + 2],
|
|
225
|
-
seg[i + 3], seg[i + 4], seg[i + 5]
|
|
226
|
-
);
|
|
227
|
-
if (d_sq < best_sq) best_sq = d_sq;
|
|
228
|
-
}
|
|
229
|
-
return best_sq === Infinity ? Infinity : Math.sqrt(best_sq);
|
|
177
|
+
bt_collect_boundary_segments(mesh, boundary_edges, boundary_segments);
|
|
178
|
+
bt_mesh_build_boundary_euclidean_distance_field(
|
|
179
|
+
vertex_boundary_distances,
|
|
180
|
+
mesh,
|
|
181
|
+
island_vertices,
|
|
182
|
+
boundary_segments
|
|
183
|
+
);
|
|
230
184
|
}
|
|
231
185
|
|
|
232
186
|
compute_field();
|
|
233
187
|
|
|
234
|
-
//
|
|
188
|
+
// CUT-BAND REFINEMENT. The inset contour is reconstructed piecewise-linearly from the faces the
|
|
235
189
|
// erosion threshold passes through, so on a coarse/skewed triangulation the cut chord deviates from
|
|
236
190
|
// the true offset (a strip-built platform eroded unevenly, 0.4 on one side and 0.6 on another). For
|
|
237
191
|
// each straddling face, place the two cut points and test whether their midpoint really lies at the
|
|
@@ -245,10 +199,13 @@ export function bt_mesh_face_island_erode(
|
|
|
245
199
|
|
|
246
200
|
const ca = new Float32Array(3), cb = new Float32Array(3), cc = new Float32Array(3);
|
|
247
201
|
|
|
202
|
+
// reused across rounds: edges whose longest side should be split this round
|
|
203
|
+
const refine_targets = new Set();
|
|
204
|
+
|
|
248
205
|
while (rounds < 48) {
|
|
249
206
|
rounds++;
|
|
250
207
|
|
|
251
|
-
|
|
208
|
+
refine_targets.clear();
|
|
252
209
|
|
|
253
210
|
for (let i = 0; i < island_faces.length; i++) {
|
|
254
211
|
const f = island_faces[i];
|
|
@@ -302,7 +259,7 @@ export function bt_mesh_face_island_erode(
|
|
|
302
259
|
|
|
303
260
|
if (crossings === 2) {
|
|
304
261
|
mx *= 0.5; my *= 0.5; mz *= 0.5;
|
|
305
|
-
const true_d =
|
|
262
|
+
const true_d = bt_point_distance_to_segments(boundary_segments, mx, my, mz);
|
|
306
263
|
if (Math.abs(true_d - erode_distance) <= tol) {
|
|
307
264
|
// the straight cut here is already accurate - leave this face coarse
|
|
308
265
|
continue;
|
|
@@ -310,37 +267,38 @@ export function bt_mesh_face_island_erode(
|
|
|
310
267
|
}
|
|
311
268
|
|
|
312
269
|
const sl = (longest === e_ab) ? la : (longest === e_bc) ? lb : lc;
|
|
313
|
-
|
|
270
|
+
refine_targets.add(mesh.loop_read_edge(sl));
|
|
314
271
|
}
|
|
315
272
|
|
|
316
|
-
if (
|
|
273
|
+
if (refine_targets.size === 0) break;
|
|
317
274
|
|
|
318
|
-
for (const e of
|
|
275
|
+
for (const e of refine_targets) {
|
|
319
276
|
if (mesh.edges.is_allocated(e)) bt_edge_split(mesh, e, 0.5);
|
|
320
277
|
}
|
|
321
278
|
|
|
322
279
|
island_faces = recompute_island_faces(faces);
|
|
323
|
-
|
|
280
|
+
collect_island_sets(island_faces);
|
|
324
281
|
|
|
325
282
|
compute_field();
|
|
326
283
|
|
|
327
284
|
if (mesh.faces.size > 200000) break;
|
|
328
285
|
}
|
|
329
286
|
}
|
|
287
|
+
|
|
330
288
|
// ---------------------------------------------------------
|
|
331
|
-
//
|
|
332
|
-
// Handle cases where a face is "submerged" by vertex values
|
|
333
|
-
//
|
|
289
|
+
// PEAK RESCUE PASS
|
|
290
|
+
// Handle cases where a face is "submerged" by its vertex values but geometrically contains a safe
|
|
291
|
+
// zone (e.g. a single triangle whose corners are all on the boundary, but whose incenter is not).
|
|
334
292
|
// ---------------------------------------------------------
|
|
335
293
|
|
|
336
|
-
//
|
|
294
|
+
// Snapshot the face list because we may modify topology (poke) while iterating.
|
|
337
295
|
const faces_to_check = [...island_faces];
|
|
338
296
|
const centroid = [];
|
|
339
297
|
const scratch_v1 = [];
|
|
340
298
|
const scratch_v2 = [];
|
|
341
299
|
|
|
342
300
|
for (const f of faces_to_check) {
|
|
343
|
-
// Collect face vertices and check if they are all "dead"
|
|
301
|
+
// Collect face vertices and check if they are all "dead".
|
|
344
302
|
let l_curr = mesh.face_read_loop(f);
|
|
345
303
|
const l_end = l_curr;
|
|
346
304
|
let all_dead = true;
|
|
@@ -354,16 +312,14 @@ export function bt_mesh_face_island_erode(
|
|
|
354
312
|
} while (l_curr !== l_end);
|
|
355
313
|
|
|
356
314
|
if (all_dead) {
|
|
357
|
-
// This face is slated for total deletion.
|
|
358
|
-
//
|
|
315
|
+
// This face is slated for total deletion. Check whether we should rescue it by adding a peak
|
|
316
|
+
// vertex at the incenter.
|
|
359
317
|
|
|
360
|
-
// 1.
|
|
318
|
+
// 1. Incenter (for a triangle; falls back to centroid for other n-gons).
|
|
361
319
|
bt_face_get_incenter(centroid, 0, mesh, f);
|
|
362
320
|
|
|
363
|
-
// 2.
|
|
364
|
-
// (For a single island face, min dist to its own boundary edges)
|
|
321
|
+
// 2. Incenter distance to boundary (min over this face's own boundary-crossing edges).
|
|
365
322
|
let min_dist = Infinity;
|
|
366
|
-
// Iterate edges of this face
|
|
367
323
|
l_curr = mesh.face_read_loop(f);
|
|
368
324
|
do {
|
|
369
325
|
const e = mesh.loop_read_edge(l_curr);
|
|
@@ -392,30 +348,23 @@ export function bt_mesh_face_island_erode(
|
|
|
392
348
|
if (d < min_dist) {
|
|
393
349
|
min_dist = d;
|
|
394
350
|
}
|
|
395
|
-
|
|
396
351
|
}
|
|
397
|
-
|
|
398
352
|
}
|
|
399
353
|
|
|
400
354
|
l_curr = mesh.loop_read_next(l_curr);
|
|
401
355
|
} while (l_curr !== l_end);
|
|
402
356
|
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
// We ignore it, or assume linear interpolation was correct.
|
|
406
|
-
// We only rescue faces touching the boundary that collapsed.
|
|
407
|
-
|
|
357
|
+
// min_dist === Infinity means this face has no boundary edges (it's internal); leave it to
|
|
358
|
+
// the interpolated field. We only rescue faces touching the boundary that collapsed.
|
|
408
359
|
if (min_dist !== Infinity && min_dist > erode_distance) {
|
|
409
|
-
// RESCUE:
|
|
410
|
-
// Poke the face to create a vertex at the centroid.
|
|
360
|
+
// RESCUE: the center is safe - poke the face to create a vertex at the incenter.
|
|
411
361
|
const new_vert = bt_mesh_face_poke(mesh, f, centroid[0], centroid[1], centroid[2]);
|
|
412
362
|
|
|
413
|
-
// Update Data
|
|
414
363
|
island_vertices.add(new_vert);
|
|
415
364
|
vertex_boundary_distances.set(new_vert, min_dist);
|
|
416
365
|
|
|
417
|
-
//
|
|
418
|
-
|
|
366
|
+
// Register the new edges fanning from the centre into the working edge set.
|
|
367
|
+
const e_first = mesh.vertex_read_edge(new_vert);
|
|
419
368
|
|
|
420
369
|
let e_scan = e_first;
|
|
421
370
|
do {
|
|
@@ -424,7 +373,7 @@ export function bt_mesh_face_island_erode(
|
|
|
424
373
|
const v1 = mesh.edge_read_vertex1(e_scan);
|
|
425
374
|
const is_v1 = v1 === new_vert;
|
|
426
375
|
|
|
427
|
-
//
|
|
376
|
+
// sanity: exactly one endpoint is the new vertex
|
|
428
377
|
const v2 = mesh.edge_read_vertex2(e_scan);
|
|
429
378
|
if (is_v1) {
|
|
430
379
|
assert.equal(v1, new_vert, 'edge vertex does not match new vertex');
|
|
@@ -439,15 +388,11 @@ export function bt_mesh_face_island_erode(
|
|
|
439
388
|
}
|
|
440
389
|
}
|
|
441
390
|
}
|
|
442
|
-
// ---------------------------------------------------------
|
|
443
|
-
|
|
444
|
-
// 4. Trace the Contour & Split Edges
|
|
445
391
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const edges_to_split = [];
|
|
392
|
+
// Trace the contour: split every island edge that strictly crosses the erosion threshold, exactly at
|
|
393
|
+
// the crossing. Parallel arrays (edge id + parameter) avoid one small object per crossing edge.
|
|
394
|
+
const split_edges = [];
|
|
395
|
+
const split_ts = [];
|
|
451
396
|
|
|
452
397
|
for (const e of island_edges) {
|
|
453
398
|
|
|
@@ -459,34 +404,31 @@ export function bt_mesh_face_island_erode(
|
|
|
459
404
|
const d1 = vertex_boundary_distances.get(v1);
|
|
460
405
|
const d2 = vertex_boundary_distances.get(v2);
|
|
461
406
|
|
|
462
|
-
//
|
|
407
|
+
// does the edge strictly cross the erosion threshold?
|
|
463
408
|
const crosses_erosion_boundary = (d1 < erode_distance && d2 > erode_distance) || (d2 < erode_distance && d1 > erode_distance);
|
|
464
409
|
|
|
465
410
|
if (crosses_erosion_boundary) {
|
|
466
|
-
// Calculate exact interpolation parameter
|
|
467
411
|
const t = clamp01(inverseLerp(d1, d2, erode_distance));
|
|
468
|
-
|
|
412
|
+
split_edges.push(e);
|
|
413
|
+
split_ts.push(t);
|
|
469
414
|
}
|
|
470
415
|
}
|
|
471
416
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
bt_edge_split(mesh, e, t);
|
|
417
|
+
for (let i = 0; i < split_edges.length; i++) {
|
|
418
|
+
// splits the edge as well as the adjacent faces
|
|
419
|
+
bt_edge_split(mesh, split_edges[i], split_ts[i]);
|
|
476
420
|
}
|
|
477
421
|
|
|
478
|
-
// Cull
|
|
479
|
-
//
|
|
480
|
-
// Any face connected to a dead vertex must be destroyed.
|
|
422
|
+
// Cull dead geometry: any vertex whose distance is < erode_distance is dead, and every face touching
|
|
423
|
+
// a dead vertex must be destroyed.
|
|
481
424
|
/**
|
|
482
|
-
*
|
|
483
425
|
* @type {Set<number>}
|
|
484
426
|
*/
|
|
485
427
|
const faces_to_kill = new Set();
|
|
486
428
|
|
|
487
|
-
|
|
488
|
-
const EPSILON = 1e-17;
|
|
429
|
+
const EPSILON = 1e-17;
|
|
489
430
|
|
|
431
|
+
for (const [vertex, distance] of vertex_boundary_distances.entries()) {
|
|
490
432
|
if (distance >= erode_distance - EPSILON) {
|
|
491
433
|
// too far, survives
|
|
492
434
|
continue;
|
|
@@ -500,7 +442,7 @@ export function bt_mesh_face_island_erode(
|
|
|
500
442
|
|
|
501
443
|
let e_curr = e_first;
|
|
502
444
|
do {
|
|
503
|
-
//
|
|
445
|
+
// traverse radial loops to gather all faces connected to this edge
|
|
504
446
|
const l_first = mesh.edge_read_loop(e_curr);
|
|
505
447
|
if (l_first !== NULL_POINTER) {
|
|
506
448
|
let l_curr = l_first;
|
|
@@ -510,18 +452,17 @@ export function bt_mesh_face_island_erode(
|
|
|
510
452
|
} while (l_curr !== l_first);
|
|
511
453
|
}
|
|
512
454
|
|
|
513
|
-
//
|
|
455
|
+
// move around the disk cycle
|
|
514
456
|
const v1 = mesh.edge_read_vertex1(e_curr);
|
|
515
457
|
e_curr = (v1 === vertex) ? mesh.edge_read_v1_disk_next(e_curr) : mesh.edge_read_v2_disk_next(e_curr);
|
|
516
458
|
} while (e_curr !== e_first && e_curr !== NULL_POINTER);
|
|
517
459
|
}
|
|
518
460
|
|
|
519
|
-
// Eradicate the dead faces
|
|
520
461
|
for (const f of faces_to_kill) {
|
|
521
462
|
bt_face_kill(mesh, f);
|
|
522
463
|
}
|
|
523
464
|
|
|
524
|
-
//
|
|
465
|
+
// Cleanup floating vertices and edges.
|
|
525
466
|
if (faces_to_kill.size > 0) {
|
|
526
467
|
bt_mesh_cleanup_faceless_references(mesh);
|
|
527
468
|
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fill boundary holes
|
|
2
|
+
* Fill inner boundary holes whose mean width falls below `min_width`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* largest-area loop (the outer boundary) is never filled.
|
|
4
|
+
* A mesh can contain hair-thin sliver holes - a strip of degenerate faces pinched off into its own
|
|
5
|
+
* tiny boundary loop - that are numerical noise rather than meaningful openings. Such slivers inflate
|
|
6
|
+
* the boundary length, the face count, and the loop count. This measures every boundary loop's mean
|
|
7
|
+
* width (2*area/perimeter, the inradius of the area-equivalent disc) and fills each inner loop narrower
|
|
8
|
+
* than `min_width`. The single largest-area loop is treated as the outer boundary and is never filled.
|
|
10
9
|
*
|
|
11
|
-
* @param {BinaryTopology} mesh
|
|
12
|
-
* @param {number}
|
|
10
|
+
* @param {BinaryTopology} mesh mesh whose boundary loops are inspected and (where too thin) filled in place
|
|
11
|
+
* @param {number} min_width minimum mean width an inner loop must have to be kept; thinner loops are filled
|
|
13
12
|
* @returns {number} number of holes filled
|
|
14
13
|
*/
|
|
15
|
-
export function bt_mesh_fill_small_holes(mesh: BinaryTopology,
|
|
14
|
+
export function bt_mesh_fill_small_holes(mesh: BinaryTopology, min_width: number): number;
|
|
16
15
|
//# sourceMappingURL=bt_mesh_fill_small_holes.d.ts.map
|
|
@@ -1 +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
|
|
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;;;;;;;;;;;;GAYG;AACH,0EAHW,MAAM,GACJ,MAAM,CA2ClB"}
|