rapidmesh 0.1.0__tar.gz
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.
- rapidmesh-0.1.0/Cargo.toml +28 -0
- rapidmesh-0.1.0/PKG-INFO +80 -0
- rapidmesh-0.1.0/README.md +55 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/Cargo.toml +16 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/arrange.rs +400 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/boolean.rs +128 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/classify.rs +199 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/constraint.rs +57 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/facet.rs +244 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/lib.rs +34 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/planar.rs +347 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/pool.rs +48 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/tri.rs +72 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/tri_tri.rs +109 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/src/triangulate.rs +454 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/tests/arrange.rs +100 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/tests/boolean.rs +86 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/tests/planar.rs +177 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/tests/tri_tri.rs +265 -0
- rapidmesh-0.1.0/crates/rapidmesh-csg/tests/triangulate.rs +95 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/Cargo.toml +14 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/expansion.rs +352 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/geom.rs +135 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/interval.rs +111 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/lib.rs +120 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/log.rs +140 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/order.rs +106 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/orient.rs +382 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/point.rs +350 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/ring.rs +22 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/src/stats.rs +32 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/tests/exactness.rs +734 -0
- rapidmesh-0.1.0/crates/rapidmesh-exact/tests/order.rs +172 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/Cargo.toml +15 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/src/faceted.rs +344 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/src/import.rs +319 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/src/lib.rs +25 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/src/plc.rs +51 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/src/polygon.rs +142 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/src/prim.rs +634 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/src/scene.rs +831 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/tests/debug_coax.rs +26 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/tests/import.rs +150 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/tests/prim.rs +569 -0
- rapidmesh-0.1.0/crates/rapidmesh-geom/tests/scene.rs +149 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/Cargo.toml +18 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/src/cdt.rs +1458 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/src/conform.rs +1858 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/src/delaunay.rs +1244 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/src/lib.rs +27 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/src/optimize.rs +2827 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/tests/conform.rs +879 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/tests/delaunay.rs +275 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/tests/face_recovery.rs +207 -0
- rapidmesh-0.1.0/crates/rapidmesh-tet/tests/segment_recovery.rs +174 -0
- rapidmesh-0.1.0/pyproject.toml +41 -0
- rapidmesh-0.1.0/python/.gitignore +4 -0
- rapidmesh-0.1.0/python/Cargo.lock +372 -0
- rapidmesh-0.1.0/python/Cargo.toml +18 -0
- rapidmesh-0.1.0/python/README.md +55 -0
- rapidmesh-0.1.0/python/examples/export_showcase.py +66 -0
- rapidmesh-0.1.0/python/examples/rapidfem_geometries.py +152 -0
- rapidmesh-0.1.0/python/examples/showcase.py +582 -0
- rapidmesh-0.1.0/python/src/lib.rs +525 -0
- rapidmesh-0.1.0/python_src/rapidmesh/__init__.py +16 -0
- rapidmesh-0.1.0/python_src/rapidmesh/geometry.py +704 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
resolver = "2"
|
|
3
|
+
members = [
|
|
4
|
+
"crates/rapidmesh-exact",
|
|
5
|
+
"crates/rapidmesh-geom",
|
|
6
|
+
"crates/rapidmesh-csg",
|
|
7
|
+
"crates/rapidmesh-tet",
|
|
8
|
+
"crates/rapidmesh",
|
|
9
|
+
"crates/rapidmesh-testutil",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[workspace.package]
|
|
13
|
+
version = "0.1.0"
|
|
14
|
+
edition = "2021"
|
|
15
|
+
publish = false
|
|
16
|
+
license = "MIT"
|
|
17
|
+
|
|
18
|
+
[workspace.dependencies]
|
|
19
|
+
rapidmesh-exact = { path = "crates/rapidmesh-exact" }
|
|
20
|
+
rapidmesh-geom = { path = "crates/rapidmesh-geom" }
|
|
21
|
+
rapidmesh-testutil = { path = "crates/rapidmesh-testutil" }
|
|
22
|
+
rapidmesh-csg = { path = "crates/rapidmesh-csg" }
|
|
23
|
+
rapidmesh-tet = { path = "crates/rapidmesh-tet" }
|
|
24
|
+
geometry-predicates = "0.3"
|
|
25
|
+
rayon = "1"
|
|
26
|
+
rustc-hash = "2"
|
|
27
|
+
serde = { version = "1", features = ["derive"] }
|
|
28
|
+
serde_json = "1"
|
rapidmesh-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rapidmesh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Science/Research
|
|
6
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
7
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
8
|
+
Classifier: Operating System :: MacOS
|
|
9
|
+
Classifier: Programming Language :: Rust
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Requires-Dist: numpy>=1.26
|
|
16
|
+
Summary: Pure-Rust conforming tetrahedral mesher for electromagnetic FEM, with exact CSG and a Python builder API.
|
|
17
|
+
Keywords: mesh,tetrahedral,fem,delaunay,csg
|
|
18
|
+
Author-email: Milan Rother <sporkhomat@gmail.com>
|
|
19
|
+
License-Expression: MIT
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
22
|
+
Project-URL: Homepage, https://mesh.rapidpassives.org
|
|
23
|
+
Project-URL: Repository, https://github.com/milanofthe/rapidmesh
|
|
24
|
+
|
|
25
|
+
# rapidmesh
|
|
26
|
+
|
|
27
|
+
Pure-Rust conforming **tetrahedral mesh generator** for 3-D electromagnetic FEM
|
|
28
|
+
(Maxwell, H(curl)/Nédélec), with a Python builder API.
|
|
29
|
+
|
|
30
|
+
- **Primitives + exact CSG booleans** — boxes, cylinders, spheres, cones, tori,
|
|
31
|
+
prisms, sweeps, lofts; unioned/subtracted by an exact mesh arrangement (no
|
|
32
|
+
float snapping, material interfaces stay exactly conforming).
|
|
33
|
+
- **Constrained Delaunay tetrahedralization** with exact boundary recovery.
|
|
34
|
+
- **Sizing-field-driven refinement** and dihedral-angle-targeted optimization.
|
|
35
|
+
- **Observability** — per-stage timings, statistics, a leveled log, and quality
|
|
36
|
+
with location, all from Python.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install rapidmesh
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import rapidmesh as rm
|
|
48
|
+
|
|
49
|
+
g = rm.Geometry(maxh=0.4)
|
|
50
|
+
g.box(4, 4, 2) # air / substrate box
|
|
51
|
+
g.cylinder(radius=0.8, height=2, position=(2, 2, 0), void=True) # a bore
|
|
52
|
+
|
|
53
|
+
mesh = g.mesh()
|
|
54
|
+
print(mesh) # Mesh(... tets, ... points, min dihedral ... deg)
|
|
55
|
+
|
|
56
|
+
mesh.points # (n_points, 3) float64
|
|
57
|
+
mesh.tets # (n_tets, 4) uint64
|
|
58
|
+
mesh.tet_regions # (n_tets,) region tag per tet
|
|
59
|
+
mesh.faces # (n_faces, 3) surface faces (region interfaces, sheets)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### What happened, how long, and where the quality is worst
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
print(mesh.report()) # per-stage timings + quality-with-location + warnings
|
|
66
|
+
mesh.timings # {"mesh.faces": 0.42, "mesh.refine": 0.16, ...} seconds
|
|
67
|
+
mesh.metrics # predicate counts, recovery work, point/tet counts
|
|
68
|
+
mesh.quality # min_dihedral_deg, worst_location, worst_region, regions[]
|
|
69
|
+
mesh.log # [{level, stage, message, at}, ...]
|
|
70
|
+
mesh.warnings # the warn/error subset (budget caps, slivers)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Set `RAPIDMESH_LOG=1` in the environment to stream the log live to stderr
|
|
74
|
+
(including per-refinement-round progress, so you can see what is running and
|
|
75
|
+
where it spends or hangs time).
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
80
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# rapidmesh
|
|
2
|
+
|
|
3
|
+
Pure-Rust conforming **tetrahedral mesh generator** for 3-D electromagnetic FEM
|
|
4
|
+
(Maxwell, H(curl)/Nédélec), with a Python builder API.
|
|
5
|
+
|
|
6
|
+
- **Primitives + exact CSG booleans** — boxes, cylinders, spheres, cones, tori,
|
|
7
|
+
prisms, sweeps, lofts; unioned/subtracted by an exact mesh arrangement (no
|
|
8
|
+
float snapping, material interfaces stay exactly conforming).
|
|
9
|
+
- **Constrained Delaunay tetrahedralization** with exact boundary recovery.
|
|
10
|
+
- **Sizing-field-driven refinement** and dihedral-angle-targeted optimization.
|
|
11
|
+
- **Observability** — per-stage timings, statistics, a leveled log, and quality
|
|
12
|
+
with location, all from Python.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install rapidmesh
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import rapidmesh as rm
|
|
24
|
+
|
|
25
|
+
g = rm.Geometry(maxh=0.4)
|
|
26
|
+
g.box(4, 4, 2) # air / substrate box
|
|
27
|
+
g.cylinder(radius=0.8, height=2, position=(2, 2, 0), void=True) # a bore
|
|
28
|
+
|
|
29
|
+
mesh = g.mesh()
|
|
30
|
+
print(mesh) # Mesh(... tets, ... points, min dihedral ... deg)
|
|
31
|
+
|
|
32
|
+
mesh.points # (n_points, 3) float64
|
|
33
|
+
mesh.tets # (n_tets, 4) uint64
|
|
34
|
+
mesh.tet_regions # (n_tets,) region tag per tet
|
|
35
|
+
mesh.faces # (n_faces, 3) surface faces (region interfaces, sheets)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### What happened, how long, and where the quality is worst
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
print(mesh.report()) # per-stage timings + quality-with-location + warnings
|
|
42
|
+
mesh.timings # {"mesh.faces": 0.42, "mesh.refine": 0.16, ...} seconds
|
|
43
|
+
mesh.metrics # predicate counts, recovery work, point/tet counts
|
|
44
|
+
mesh.quality # min_dihedral_deg, worst_location, worst_region, regions[]
|
|
45
|
+
mesh.log # [{level, stage, message, at}, ...]
|
|
46
|
+
mesh.warnings # the warn/error subset (budget caps, slivers)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Set `RAPIDMESH_LOG=1` in the environment to stream the log live to stderr
|
|
50
|
+
(including per-refinement-round progress, so you can see what is running and
|
|
51
|
+
where it spends or hangs time).
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rapidmesh-csg"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
publish.workspace = true
|
|
6
|
+
|
|
7
|
+
[dependencies]
|
|
8
|
+
rapidmesh-exact.workspace = true
|
|
9
|
+
geometry-predicates.workspace = true
|
|
10
|
+
rayon.workspace = true
|
|
11
|
+
|
|
12
|
+
[dev-dependencies]
|
|
13
|
+
rapidmesh-testutil.workspace = true
|
|
14
|
+
num-rational = "0.4"
|
|
15
|
+
num-bigint = "0.4"
|
|
16
|
+
num-traits = "0.2"
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
//! Arrangement of a triangle soup: every facet is subdivided by its exact
|
|
2
|
+
//! intersections with all other facets.
|
|
3
|
+
//!
|
|
4
|
+
//! Pair candidates come from a BVH over the facet bounding boxes. Each
|
|
5
|
+
//! intersecting pair contributes constraints to both facets (with line
|
|
6
|
+
//! provenance, see [`crate::constraint`]); coplanar pairs contribute the
|
|
7
|
+
//! other facet's edges clipped to this facet. Each facet is then
|
|
8
|
+
//! independently retriangulated — exact constructions and exact coincidence
|
|
9
|
+
//! guarantee that shared intersection vertices match across facets, which is
|
|
10
|
+
//! what downstream inside/outside classification relies on.
|
|
11
|
+
|
|
12
|
+
use crate::constraint::{Constraint, ConstraintLine};
|
|
13
|
+
use crate::tri::Tri;
|
|
14
|
+
use crate::tri_tri::{tri_tri_intersection, TriTriIsect};
|
|
15
|
+
use crate::triangulate::{triangulate_facet, FacetTriangulation};
|
|
16
|
+
use rapidmesh_exact::{cmp_along, collinear, within_closed, Point3, Sign};
|
|
17
|
+
|
|
18
|
+
// ------------------------------------------------------------------- BVH
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone, Copy)]
|
|
21
|
+
pub(crate) struct Aabb {
|
|
22
|
+
min: [f64; 3],
|
|
23
|
+
max: [f64; 3],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl Aabb {
|
|
27
|
+
pub(crate) fn of_tri(t: &Tri) -> Aabb {
|
|
28
|
+
let mut min = t.v[0];
|
|
29
|
+
let mut max = t.v[0];
|
|
30
|
+
for v in &t.v[1..] {
|
|
31
|
+
for k in 0..3 {
|
|
32
|
+
min[k] = min[k].min(v[k]);
|
|
33
|
+
max[k] = max[k].max(v[k]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
Aabb { min, max }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn union(&self, o: &Aabb) -> Aabb {
|
|
40
|
+
Aabb {
|
|
41
|
+
min: std::array::from_fn(|k| self.min[k].min(o.min[k])),
|
|
42
|
+
max: std::array::from_fn(|k| self.max[k].max(o.max[k])),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Closed-box overlap: touching boxes count (touching facets intersect).
|
|
47
|
+
fn overlaps(&self, o: &Aabb) -> bool {
|
|
48
|
+
(0..3).all(|k| self.min[k] <= o.max[k] && o.min[k] <= self.max[k])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn centroid(&self, k: usize) -> f64 {
|
|
52
|
+
0.5 * (self.min[k] + self.max[k])
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub(crate) enum Bvh {
|
|
57
|
+
Leaf { aabb: Aabb, items: Vec<usize> },
|
|
58
|
+
Inner { aabb: Aabb, left: Box<Bvh>, right: Box<Bvh> },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl Bvh {
|
|
62
|
+
fn aabb(&self) -> &Aabb {
|
|
63
|
+
match self {
|
|
64
|
+
Bvh::Leaf { aabb, .. } | Bvh::Inner { aabb, .. } => aabb,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const BVH_LEAF_SIZE: usize = 4;
|
|
70
|
+
|
|
71
|
+
pub(crate) fn build_bvh(items: &mut [usize], boxes: &[Aabb]) -> Bvh {
|
|
72
|
+
let aabb = items
|
|
73
|
+
.iter()
|
|
74
|
+
.map(|&i| boxes[i])
|
|
75
|
+
.reduce(|a, b| a.union(&b))
|
|
76
|
+
.expect("non-empty");
|
|
77
|
+
if items.len() <= BVH_LEAF_SIZE {
|
|
78
|
+
return Bvh::Leaf {
|
|
79
|
+
aabb,
|
|
80
|
+
items: items.to_vec(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Split along the axis with the largest centroid spread.
|
|
84
|
+
let axis = (0..3)
|
|
85
|
+
.max_by(|&a, &b| {
|
|
86
|
+
let spread = |k: usize| {
|
|
87
|
+
let lo = items.iter().map(|&i| boxes[i].centroid(k)).fold(f64::MAX, f64::min);
|
|
88
|
+
let hi = items.iter().map(|&i| boxes[i].centroid(k)).fold(f64::MIN, f64::max);
|
|
89
|
+
hi - lo
|
|
90
|
+
};
|
|
91
|
+
spread(a).partial_cmp(&spread(b)).expect("finite")
|
|
92
|
+
})
|
|
93
|
+
.expect("three axes");
|
|
94
|
+
items.sort_unstable_by(|&a, &b| {
|
|
95
|
+
boxes[a]
|
|
96
|
+
.centroid(axis)
|
|
97
|
+
.partial_cmp(&boxes[b].centroid(axis))
|
|
98
|
+
.expect("finite")
|
|
99
|
+
});
|
|
100
|
+
let mid = items.len() / 2;
|
|
101
|
+
let (l, r) = items.split_at_mut(mid);
|
|
102
|
+
Bvh::Inner {
|
|
103
|
+
aabb,
|
|
104
|
+
left: Box::new(build_bvh(l, boxes)),
|
|
105
|
+
right: Box::new(build_bvh(r, boxes)),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub(crate) fn self_pairs(n: &Bvh, boxes: &[Aabb], out: &mut Vec<(usize, usize)>) {
|
|
110
|
+
match n {
|
|
111
|
+
Bvh::Leaf { items, .. } => {
|
|
112
|
+
for (a, &i) in items.iter().enumerate() {
|
|
113
|
+
for &j in &items[a + 1..] {
|
|
114
|
+
if boxes[i].overlaps(&boxes[j]) {
|
|
115
|
+
out.push((i.min(j), i.max(j)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
Bvh::Inner { left, right, .. } => {
|
|
121
|
+
self_pairs(left, boxes, out);
|
|
122
|
+
self_pairs(right, boxes, out);
|
|
123
|
+
cross_pairs(left, right, boxes, out);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn cross_pairs(a: &Bvh, b: &Bvh, boxes: &[Aabb], out: &mut Vec<(usize, usize)>) {
|
|
129
|
+
if !a.aabb().overlaps(b.aabb()) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
match (a, b) {
|
|
133
|
+
(Bvh::Leaf { items: ia, .. }, Bvh::Leaf { items: ib, .. }) => {
|
|
134
|
+
for &i in ia {
|
|
135
|
+
for &j in ib {
|
|
136
|
+
if boxes[i].overlaps(&boxes[j]) {
|
|
137
|
+
out.push((i.min(j), i.max(j)));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
(Bvh::Inner { left, right, .. }, _) => {
|
|
143
|
+
cross_pairs(left, b, boxes, out);
|
|
144
|
+
cross_pairs(right, b, boxes, out);
|
|
145
|
+
}
|
|
146
|
+
(_, Bvh::Inner { left, right, .. }) => {
|
|
147
|
+
cross_pairs(a, left, boxes, out);
|
|
148
|
+
cross_pairs(a, right, boxes, out);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// -------------------------------------------------------- coplanar clip
|
|
154
|
+
|
|
155
|
+
/// Clips the explicit edge (u, v) of a triangle coplanar with `facet` to the
|
|
156
|
+
/// (closed, convex) facet. Returns the clipped sub-segment endpoints ordered
|
|
157
|
+
/// along u→v; they coincide for a single-point touch. `None` if the edge
|
|
158
|
+
/// misses the facet.
|
|
159
|
+
pub(crate) fn clip_coplanar_edge(facet: &Tri, u: [f64; 3], v: [f64; 3]) -> Option<(Point3, Point3)> {
|
|
160
|
+
let (axis, orientation) = facet.projection_axis();
|
|
161
|
+
let pu = Point3::Explicit(u);
|
|
162
|
+
let pv = Point3::Explicit(v);
|
|
163
|
+
let mut cands: Vec<Point3> = Vec::new();
|
|
164
|
+
for p in [&pu, &pv] {
|
|
165
|
+
if facet.contains_coplanar(p, axis, orientation) {
|
|
166
|
+
cands.push(p.clone());
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for e in 0..3 {
|
|
170
|
+
let (a, b) = (facet.v[e], facet.v[(e + 1) % 3]);
|
|
171
|
+
let (pa, pb) = (Point3::Explicit(a), Point3::Explicit(b));
|
|
172
|
+
// Proper line crossing with this facet edge, clamped to both
|
|
173
|
+
// segments.
|
|
174
|
+
if let Some(x) = Point3::lli_coplanar(u, v, a, b) {
|
|
175
|
+
if within_closed(&pu, &pv, &x).expect("valid")
|
|
176
|
+
&& within_closed(&pa, &pb, &x).expect("valid")
|
|
177
|
+
{
|
|
178
|
+
cands.push(x);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Facet corner lying on the edge (covers collinear-overlap cases).
|
|
182
|
+
if collinear(&pu, &pv, &pa).expect("valid")
|
|
183
|
+
&& within_closed(&pu, &pv, &pa).expect("valid")
|
|
184
|
+
{
|
|
185
|
+
cands.push(pa);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// The facet is convex, so the clip is the extreme candidates along u→v.
|
|
189
|
+
let mut iter = cands.into_iter();
|
|
190
|
+
let first = iter.next()?;
|
|
191
|
+
let (mut lo, mut hi) = (first.clone(), first);
|
|
192
|
+
for c in iter {
|
|
193
|
+
if cmp_along(&pu, &pv, &c, &lo).expect("valid") == Sign::Positive {
|
|
194
|
+
lo = c.clone();
|
|
195
|
+
}
|
|
196
|
+
if cmp_along(&pu, &pv, &hi, &c).expect("valid") == Sign::Positive {
|
|
197
|
+
hi = c;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
Some((lo, hi))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------- adjacency fast path
|
|
204
|
+
|
|
205
|
+
/// True if the triangle pair contributes NOTHING to the arrangement and the
|
|
206
|
+
/// full intersection machinery can be skipped. These are the dominant
|
|
207
|
+
/// candidate kinds on clean closed surfaces (shared edges/vertices are
|
|
208
|
+
/// bit-identical there): a shared edge of two non-coplanar triangles is
|
|
209
|
+
/// exactly their intersection and already a boundary edge of both; a shared
|
|
210
|
+
/// vertex with the rest strictly on one side is a touching corner already
|
|
211
|
+
/// present; strictly-one-side pairs are disjoint; and exactly-coplanar pairs
|
|
212
|
+
/// separated by an exact 2D line among the six edges meet at most in a shared
|
|
213
|
+
/// edge/vertex. Every other configuration (coplanar overlap, piercing) falls
|
|
214
|
+
/// through to the full machinery. All signs come from exact predicates, so the
|
|
215
|
+
/// skips are exact.
|
|
216
|
+
pub(crate) fn adjacency_skip(ti: &Tri, tj: &Tri) -> bool {
|
|
217
|
+
let side = |t: &Tri, q: [f64; 3]| -> Sign {
|
|
218
|
+
Sign::of_f64(geometry_predicates::orient3d(t.v[0], t.v[1], t.v[2], q))
|
|
219
|
+
};
|
|
220
|
+
let mut shared_j = [false; 3];
|
|
221
|
+
let mut n_shared = 0;
|
|
222
|
+
for (b, flag) in shared_j.iter_mut().enumerate() {
|
|
223
|
+
if ti.v.iter().any(|&a| a == tj.v[b]) {
|
|
224
|
+
*flag = true;
|
|
225
|
+
n_shared += 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let signs: [Sign; 3] = std::array::from_fn(|b| side(ti, tj.v[b]));
|
|
229
|
+
if signs == [Sign::Zero; 3] {
|
|
230
|
+
let is_shared = |q: [f64; 3]| -> bool { ti.v.contains(&q) && tj.v.contains(&q) };
|
|
231
|
+
let (axis, _) = ti.projection_axis();
|
|
232
|
+
return (0..6).any(|e| {
|
|
233
|
+
let t_edge = if e < 3 { ti } else { tj };
|
|
234
|
+
let (p, q) = (t_edge.v[e % 3], t_edge.v[(e + 1) % 3]);
|
|
235
|
+
let line_sign = |r: [f64; 3]| -> Option<Sign> {
|
|
236
|
+
rapidmesh_exact::orient2d(
|
|
237
|
+
&Point3::Explicit(p),
|
|
238
|
+
&Point3::Explicit(q),
|
|
239
|
+
&Point3::Explicit(r),
|
|
240
|
+
axis,
|
|
241
|
+
)
|
|
242
|
+
};
|
|
243
|
+
let mut side_i = Sign::Zero;
|
|
244
|
+
let mut side_j = Sign::Zero;
|
|
245
|
+
for r in ti.v {
|
|
246
|
+
match line_sign(r) {
|
|
247
|
+
Some(Sign::Zero) => {
|
|
248
|
+
if !(r == p || r == q || is_shared(r)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
Some(sg) => {
|
|
253
|
+
if side_i != Sign::Zero && side_i != sg {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
side_i = sg;
|
|
257
|
+
}
|
|
258
|
+
None => return false,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for r in tj.v {
|
|
262
|
+
match line_sign(r) {
|
|
263
|
+
Some(Sign::Zero) => {
|
|
264
|
+
if !(r == p || r == q || is_shared(r)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
Some(sg) => {
|
|
269
|
+
if side_j != Sign::Zero && side_j != sg {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
side_j = sg;
|
|
273
|
+
}
|
|
274
|
+
None => return false,
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
side_i != Sign::Zero && side_j != Sign::Zero && side_i != side_j
|
|
278
|
+
});
|
|
279
|
+
} else if n_shared == 2 {
|
|
280
|
+
let opp = (0..3).find(|&b| !shared_j[b]).expect("one non-shared");
|
|
281
|
+
return signs[opp] != Sign::Zero;
|
|
282
|
+
} else if n_shared == 1 {
|
|
283
|
+
let mut others = (0..3).filter(|&b| !shared_j[b]);
|
|
284
|
+
let (b1, b2) = (others.next().expect("two"), others.next().expect("two"));
|
|
285
|
+
let (s1, s2) = (signs[b1], signs[b2]);
|
|
286
|
+
return s1 != Sign::Zero && s1 == s2;
|
|
287
|
+
} else if n_shared == 0
|
|
288
|
+
&& (signs.iter().all(|&x| x == Sign::Positive)
|
|
289
|
+
|| signs.iter().all(|&x| x == Sign::Negative))
|
|
290
|
+
{
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
false
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ----------------------------------------------------------- arrangement
|
|
297
|
+
|
|
298
|
+
/// The arrangement of a triangle soup.
|
|
299
|
+
#[derive(Debug)]
|
|
300
|
+
pub struct Arrangement {
|
|
301
|
+
/// Per input facet: its exact constrained triangulation.
|
|
302
|
+
pub facets: Vec<FacetTriangulation>,
|
|
303
|
+
/// Per input facet: the constraints that subdivided it (for downstream
|
|
304
|
+
/// classification and inspection).
|
|
305
|
+
pub constraints: Vec<Vec<Constraint>>,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Computes the arrangement of `tris`: each facet triangulated so that all
|
|
309
|
+
/// pairwise intersections appear as triangulation edges/vertices, exactly.
|
|
310
|
+
pub fn arrange(tris: &[Tri]) -> Arrangement {
|
|
311
|
+
let boxes: Vec<Aabb> = tris.iter().map(Aabb::of_tri).collect();
|
|
312
|
+
let mut pairs: Vec<(usize, usize)> = Vec::new();
|
|
313
|
+
if !tris.is_empty() {
|
|
314
|
+
let mut idx: Vec<usize> = (0..tris.len()).collect();
|
|
315
|
+
let bvh = build_bvh(&mut idx, &boxes);
|
|
316
|
+
self_pairs(&bvh, &boxes, &mut pairs);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let trace = std::env::var_os("RAPIDMESH_TRACE").is_some();
|
|
320
|
+
let t_pairs = std::time::Instant::now();
|
|
321
|
+
let n_pairs = pairs.len();
|
|
322
|
+
let mut points: Vec<Vec<Point3>> = vec![Vec::new(); tris.len()];
|
|
323
|
+
let mut constraints: Vec<Vec<Constraint>> = vec![Vec::new(); tris.len()];
|
|
324
|
+
let mut skipped = [0usize; 1];
|
|
325
|
+
for (i, j) in pairs {
|
|
326
|
+
// Fast path for mesh-adjacent / disjoint pairs (see adjacency_skip):
|
|
327
|
+
// the dominant candidate kind on clean closed surfaces, contributing
|
|
328
|
+
// nothing the arrangement does not already have. Every other
|
|
329
|
+
// configuration falls through to the full intersection machinery.
|
|
330
|
+
if adjacency_skip(&tris[i], &tris[j]) {
|
|
331
|
+
skipped[0] += 1;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
match tri_tri_intersection(&tris[i], &tris[j]) {
|
|
335
|
+
TriTriIsect::Disjoint => {}
|
|
336
|
+
TriTriIsect::Touching(p) => {
|
|
337
|
+
points[i].push(p.clone());
|
|
338
|
+
points[j].push(p);
|
|
339
|
+
}
|
|
340
|
+
TriTriIsect::Segment(a, b) => {
|
|
341
|
+
constraints[i].push(Constraint {
|
|
342
|
+
a: a.clone(),
|
|
343
|
+
b: b.clone(),
|
|
344
|
+
line: ConstraintLine::PlaneCut(tris[j].v),
|
|
345
|
+
});
|
|
346
|
+
constraints[j].push(Constraint {
|
|
347
|
+
a,
|
|
348
|
+
b,
|
|
349
|
+
line: ConstraintLine::PlaneCut(tris[i].v),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
TriTriIsect::Coplanar => {
|
|
353
|
+
for (fi, fj) in [(i, j), (j, i)] {
|
|
354
|
+
let facet = &tris[fi];
|
|
355
|
+
let other = &tris[fj];
|
|
356
|
+
for e in 0..3 {
|
|
357
|
+
let (u, v) = (other.v[e], other.v[(e + 1) % 3]);
|
|
358
|
+
if let Some((lo, hi)) = clip_coplanar_edge(facet, u, v) {
|
|
359
|
+
if lo.coincides(&hi) {
|
|
360
|
+
points[fi].push(lo);
|
|
361
|
+
} else {
|
|
362
|
+
constraints[fi].push(Constraint {
|
|
363
|
+
a: lo,
|
|
364
|
+
b: hi,
|
|
365
|
+
line: ConstraintLine::Edge(u, v),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if trace {
|
|
376
|
+
eprintln!(
|
|
377
|
+
"arrange: {n_pairs} pairs in {:.1?} (skipped {})",
|
|
378
|
+
t_pairs.elapsed(),
|
|
379
|
+
skipped[0]
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
let t_tri = std::time::Instant::now();
|
|
383
|
+
// Each facet retriangulates independently from its own intersection
|
|
384
|
+
// points and constraints (read-only), and this dominates assembly on
|
|
385
|
+
// boolean-heavy scenes; run it in parallel. collect() into an indexed
|
|
386
|
+
// Vec keeps the result order identical to the serial map.
|
|
387
|
+
use rayon::prelude::*;
|
|
388
|
+
let facets = tris
|
|
389
|
+
.par_iter()
|
|
390
|
+
.enumerate()
|
|
391
|
+
.map(|(i, t)| triangulate_facet(t, &points[i], &constraints[i]))
|
|
392
|
+
.collect();
|
|
393
|
+
if trace {
|
|
394
|
+
eprintln!("arrange: triangulate {:.1?}", t_tri.elapsed());
|
|
395
|
+
}
|
|
396
|
+
Arrangement {
|
|
397
|
+
facets,
|
|
398
|
+
constraints,
|
|
399
|
+
}
|
|
400
|
+
}
|