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.
Files changed (66) hide show
  1. rapidmesh-0.1.0/Cargo.toml +28 -0
  2. rapidmesh-0.1.0/PKG-INFO +80 -0
  3. rapidmesh-0.1.0/README.md +55 -0
  4. rapidmesh-0.1.0/crates/rapidmesh-csg/Cargo.toml +16 -0
  5. rapidmesh-0.1.0/crates/rapidmesh-csg/src/arrange.rs +400 -0
  6. rapidmesh-0.1.0/crates/rapidmesh-csg/src/boolean.rs +128 -0
  7. rapidmesh-0.1.0/crates/rapidmesh-csg/src/classify.rs +199 -0
  8. rapidmesh-0.1.0/crates/rapidmesh-csg/src/constraint.rs +57 -0
  9. rapidmesh-0.1.0/crates/rapidmesh-csg/src/facet.rs +244 -0
  10. rapidmesh-0.1.0/crates/rapidmesh-csg/src/lib.rs +34 -0
  11. rapidmesh-0.1.0/crates/rapidmesh-csg/src/planar.rs +347 -0
  12. rapidmesh-0.1.0/crates/rapidmesh-csg/src/pool.rs +48 -0
  13. rapidmesh-0.1.0/crates/rapidmesh-csg/src/tri.rs +72 -0
  14. rapidmesh-0.1.0/crates/rapidmesh-csg/src/tri_tri.rs +109 -0
  15. rapidmesh-0.1.0/crates/rapidmesh-csg/src/triangulate.rs +454 -0
  16. rapidmesh-0.1.0/crates/rapidmesh-csg/tests/arrange.rs +100 -0
  17. rapidmesh-0.1.0/crates/rapidmesh-csg/tests/boolean.rs +86 -0
  18. rapidmesh-0.1.0/crates/rapidmesh-csg/tests/planar.rs +177 -0
  19. rapidmesh-0.1.0/crates/rapidmesh-csg/tests/tri_tri.rs +265 -0
  20. rapidmesh-0.1.0/crates/rapidmesh-csg/tests/triangulate.rs +95 -0
  21. rapidmesh-0.1.0/crates/rapidmesh-exact/Cargo.toml +14 -0
  22. rapidmesh-0.1.0/crates/rapidmesh-exact/src/expansion.rs +352 -0
  23. rapidmesh-0.1.0/crates/rapidmesh-exact/src/geom.rs +135 -0
  24. rapidmesh-0.1.0/crates/rapidmesh-exact/src/interval.rs +111 -0
  25. rapidmesh-0.1.0/crates/rapidmesh-exact/src/lib.rs +120 -0
  26. rapidmesh-0.1.0/crates/rapidmesh-exact/src/log.rs +140 -0
  27. rapidmesh-0.1.0/crates/rapidmesh-exact/src/order.rs +106 -0
  28. rapidmesh-0.1.0/crates/rapidmesh-exact/src/orient.rs +382 -0
  29. rapidmesh-0.1.0/crates/rapidmesh-exact/src/point.rs +350 -0
  30. rapidmesh-0.1.0/crates/rapidmesh-exact/src/ring.rs +22 -0
  31. rapidmesh-0.1.0/crates/rapidmesh-exact/src/stats.rs +32 -0
  32. rapidmesh-0.1.0/crates/rapidmesh-exact/tests/exactness.rs +734 -0
  33. rapidmesh-0.1.0/crates/rapidmesh-exact/tests/order.rs +172 -0
  34. rapidmesh-0.1.0/crates/rapidmesh-geom/Cargo.toml +15 -0
  35. rapidmesh-0.1.0/crates/rapidmesh-geom/src/faceted.rs +344 -0
  36. rapidmesh-0.1.0/crates/rapidmesh-geom/src/import.rs +319 -0
  37. rapidmesh-0.1.0/crates/rapidmesh-geom/src/lib.rs +25 -0
  38. rapidmesh-0.1.0/crates/rapidmesh-geom/src/plc.rs +51 -0
  39. rapidmesh-0.1.0/crates/rapidmesh-geom/src/polygon.rs +142 -0
  40. rapidmesh-0.1.0/crates/rapidmesh-geom/src/prim.rs +634 -0
  41. rapidmesh-0.1.0/crates/rapidmesh-geom/src/scene.rs +831 -0
  42. rapidmesh-0.1.0/crates/rapidmesh-geom/tests/debug_coax.rs +26 -0
  43. rapidmesh-0.1.0/crates/rapidmesh-geom/tests/import.rs +150 -0
  44. rapidmesh-0.1.0/crates/rapidmesh-geom/tests/prim.rs +569 -0
  45. rapidmesh-0.1.0/crates/rapidmesh-geom/tests/scene.rs +149 -0
  46. rapidmesh-0.1.0/crates/rapidmesh-tet/Cargo.toml +18 -0
  47. rapidmesh-0.1.0/crates/rapidmesh-tet/src/cdt.rs +1458 -0
  48. rapidmesh-0.1.0/crates/rapidmesh-tet/src/conform.rs +1858 -0
  49. rapidmesh-0.1.0/crates/rapidmesh-tet/src/delaunay.rs +1244 -0
  50. rapidmesh-0.1.0/crates/rapidmesh-tet/src/lib.rs +27 -0
  51. rapidmesh-0.1.0/crates/rapidmesh-tet/src/optimize.rs +2827 -0
  52. rapidmesh-0.1.0/crates/rapidmesh-tet/tests/conform.rs +879 -0
  53. rapidmesh-0.1.0/crates/rapidmesh-tet/tests/delaunay.rs +275 -0
  54. rapidmesh-0.1.0/crates/rapidmesh-tet/tests/face_recovery.rs +207 -0
  55. rapidmesh-0.1.0/crates/rapidmesh-tet/tests/segment_recovery.rs +174 -0
  56. rapidmesh-0.1.0/pyproject.toml +41 -0
  57. rapidmesh-0.1.0/python/.gitignore +4 -0
  58. rapidmesh-0.1.0/python/Cargo.lock +372 -0
  59. rapidmesh-0.1.0/python/Cargo.toml +18 -0
  60. rapidmesh-0.1.0/python/README.md +55 -0
  61. rapidmesh-0.1.0/python/examples/export_showcase.py +66 -0
  62. rapidmesh-0.1.0/python/examples/rapidfem_geometries.py +152 -0
  63. rapidmesh-0.1.0/python/examples/showcase.py +582 -0
  64. rapidmesh-0.1.0/python/src/lib.rs +525 -0
  65. rapidmesh-0.1.0/python_src/rapidmesh/__init__.py +16 -0
  66. 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"
@@ -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
+ }