meshers 0.1.0__tar.gz → 0.2.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 (90) hide show
  1. {meshers-0.1.0 → meshers-0.2.0}/PKG-INFO +5 -4
  2. {meshers-0.1.0/meshers-python → meshers-0.2.0}/README.md +4 -3
  3. {meshers-0.1.0/meshers-python → meshers-0.2.0}/THIRDPARTY.json +1 -1
  4. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/Cargo.toml +9 -1
  5. meshers-0.2.0/meshers-core/examples/adapt_gyroid.rs +65 -0
  6. meshers-0.2.0/meshers-core/examples/adapt_surface.rs +54 -0
  7. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/examples/curved_intersection.rs +1 -1
  8. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/adaptive.rs +146 -1
  9. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/geometry.rs +26 -0
  10. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/implicit/intersection/repair.rs +44 -10
  11. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/implicit/intersection.rs +124 -17
  12. meshers-0.2.0/meshers-core/src/implicit/periodic_feature_research.rs +174 -0
  13. meshers-0.2.0/meshers-core/src/implicit.rs +2625 -0
  14. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/lib.rs +10 -0
  15. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/optimize.rs +155 -15
  16. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/optimize_batch.rs +98 -10
  17. meshers-0.2.0/meshers-core/src/polyhedral.rs +284 -0
  18. meshers-0.2.0/meshers-core/src/projection.rs +178 -0
  19. meshers-0.2.0/meshers-core/src/quadratic/fit.rs +439 -0
  20. meshers-0.2.0/meshers-core/src/quadratic.rs +539 -0
  21. meshers-0.2.0/meshers-core/src/quads.rs +1704 -0
  22. meshers-0.2.0/meshers-core/src/remesh.rs +1115 -0
  23. meshers-0.2.0/meshers-core/src/testdata/frd-cap-star.json +362 -0
  24. meshers-0.2.0/meshers-core/src/testdata/spinodoid-cap-star.json +1258 -0
  25. meshers-0.2.0/meshers-core/src/tet_adapt.rs +2495 -0
  26. meshers-0.2.0/meshers-core/src/triangles.rs +1263 -0
  27. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/tests/implicit.rs +345 -0
  28. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/tests/intersection.rs +109 -0
  29. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/Cargo.lock +2 -2
  30. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/Cargo.toml +6 -2
  31. {meshers-0.1.0 → meshers-0.2.0/meshers-python}/README.md +4 -3
  32. {meshers-0.1.0 → meshers-0.2.0/meshers-python}/THIRDPARTY.json +1 -1
  33. meshers-0.2.0/meshers-python/src/compiled/polyhedral.rs +171 -0
  34. meshers-0.2.0/meshers-python/src/compiled.rs +571 -0
  35. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/src/lib.rs +403 -29
  36. meshers-0.2.0/meshers-python/src/surface.rs +131 -0
  37. meshers-0.2.0/meshers-python/tests/test_adaptive.py +146 -0
  38. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_api.py +76 -0
  39. meshers-0.2.0/meshers-python/tests/test_compiled.py +243 -0
  40. meshers-0.2.0/meshers-python/tests/test_curved_fit.py +63 -0
  41. meshers-0.2.0/meshers-python/tests/test_curved_volumes.py +77 -0
  42. meshers-0.2.0/meshers-python/tests/test_interp.py +87 -0
  43. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_intersection.py +237 -0
  44. meshers-0.2.0/meshers-python/tests/test_nonlinear_constraints.py +138 -0
  45. meshers-0.2.0/meshers-python/tests/test_observations.py +99 -0
  46. meshers-0.2.0/meshers-python/tests/test_periodic_zero_level.py +51 -0
  47. meshers-0.2.0/meshers-python/tests/test_polyhedral.py +392 -0
  48. meshers-0.2.0/meshers-python/tests/test_polyhedral_regions.py +155 -0
  49. meshers-0.2.0/meshers-python/tests/test_quality.py +130 -0
  50. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_rust_compile.py +4 -1
  51. meshers-0.2.0/meshers-python/tests/test_size_field.py +88 -0
  52. meshers-0.2.0/meshers-python/tests/test_surface.py +54 -0
  53. {meshers-0.1.0 → meshers-0.2.0}/pyproject.toml +1 -1
  54. {meshers-0.1.0 → meshers-0.2.0}/python/meshers/__init__.py +199 -13
  55. {meshers-0.1.0 → meshers-0.2.0}/python/meshers/_meshers.pyi +35 -1
  56. {meshers-0.1.0 → meshers-0.2.0}/python/meshers/expression.py +113 -4
  57. {meshers-0.1.0 → meshers-0.2.0}/python/meshers/intersection.py +95 -5
  58. meshers-0.2.0/python/meshers/surface.py +143 -0
  59. meshers-0.1.0/meshers-core/src/implicit.rs +0 -904
  60. meshers-0.1.0/meshers-python/src/compiled.rs +0 -252
  61. meshers-0.1.0/meshers-python/tests/test_compiled.py +0 -122
  62. meshers-0.1.0/meshers-python/tests/test_quality.py +0 -59
  63. {meshers-0.1.0 → meshers-0.2.0}/LICENSE +0 -0
  64. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/LICENSE +0 -0
  65. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/README.md +0 -0
  66. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/examples/gyroid.rs +0 -0
  67. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/examples/implicit.rs +0 -0
  68. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/examples/implicit_sphere.rs +0 -0
  69. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/accelerator.rs +0 -0
  70. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/error.rs +0 -0
  71. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/profile.rs +0 -0
  72. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/tests/batching.rs +0 -0
  73. {meshers-0.1.0 → meshers-0.2.0}/meshers-core/tests/public_api.rs +0 -0
  74. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/COMPILED.md +0 -0
  75. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/LICENSE +0 -0
  76. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/THIRDPARTY.md +0 -0
  77. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/basic.py +0 -0
  78. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/cancellation.py +0 -0
  79. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/curved_tpms.py +0 -0
  80. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/evaluators.py +0 -0
  81. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/graded.py +0 -0
  82. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/intersection.py +0 -0
  83. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/periodic.py +0 -0
  84. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/src/intersection.rs +0 -0
  85. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_angular.py +0 -0
  86. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_automatic.py +0 -0
  87. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_examples.py +0 -0
  88. {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_tpms_release.py +0 -0
  89. {meshers-0.1.0 → meshers-0.2.0}/python/meshers/compiler.py +0 -0
  90. {meshers-0.1.0 → meshers-0.2.0}/python/meshers/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshers
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Requires-Dist: numpy>=1.23
5
5
  Requires-Dist: h5py>=3.8 ; extra == 'io'
6
6
  Requires-Dist: pytest>=8 ; extra == 'test'
@@ -18,7 +18,7 @@ Project-URL: Documentation, https://github.com/kmarchais/meshers/tree/main/docs/
18
18
  Project-URL: Issues, https://github.com/kmarchais/meshers/issues
19
19
  Project-URL: Repository, https://github.com/kmarchais/meshers
20
20
 
21
- # Meshers Python 0.1.0
21
+ # Meshers Python 0.2.0
22
22
 
23
23
  In-process CPU float64 meshing of bounded implicit solids. No subprocess or intermediate mesh file is needed.
24
24
 
@@ -70,8 +70,9 @@ See the [graded example](https://github.com/kmarchais/meshers/blob/main/crates/m
70
70
  - Boundary tags: 0 implicit, 1/2 x-/x+, 3/4 y-/y+, 5/6 z-/z+.
71
71
  - Per-axis periodic node pairs plus geometry and quality diagnostics.
72
72
  - VTKHDF volume or `surface=True`, periodic master IDs and physical shift vectors.
73
- Volume files include `Volume` and `MMGQuality`, computed using the MMG formula
74
- in NumPy, not by calling mmgpy.
73
+ Linear volume files include `Volume` and `MMGQuality`. For Tet10, `Volume`
74
+ integrates the curved mapping, while `CornerVolume` and `CornerMMGQuality`
75
+ describe the linear parent. Quality is computed without calling mmgpy.
75
76
  - Invalid options raise `ValueError`; failed meshing raises `MeshingError`.
76
77
  Callback exceptions are preserved. `KeyboardInterrupt` is propagated.
77
78
  - Pass `cancel=CancellationToken()` and call `token.cancel()` from another thread
@@ -1,4 +1,4 @@
1
- # Meshers Python 0.1.0
1
+ # Meshers Python 0.2.0
2
2
 
3
3
  In-process CPU float64 meshing of bounded implicit solids. No subprocess or intermediate mesh file is needed.
4
4
 
@@ -50,8 +50,9 @@ See the [graded example](https://github.com/kmarchais/meshers/blob/main/crates/m
50
50
  - Boundary tags: 0 implicit, 1/2 x-/x+, 3/4 y-/y+, 5/6 z-/z+.
51
51
  - Per-axis periodic node pairs plus geometry and quality diagnostics.
52
52
  - VTKHDF volume or `surface=True`, periodic master IDs and physical shift vectors.
53
- Volume files include `Volume` and `MMGQuality`, computed using the MMG formula
54
- in NumPy, not by calling mmgpy.
53
+ Linear volume files include `Volume` and `MMGQuality`. For Tet10, `Volume`
54
+ integrates the curved mapping, while `CornerVolume` and `CornerMMGQuality`
55
+ describe the linear parent. Quality is computed without calling mmgpy.
55
56
  - Invalid options raise `ValueError`; failed meshing raises `MeshingError`.
56
57
  Callback exceptions are preserved. `KeyboardInterrupt` is propagated.
57
58
  - Pass `cancel=CancellationToken()` and call `token.cancel()` from another thread
@@ -531,7 +531,7 @@
531
531
  },
532
532
  {
533
533
  "package_name": "meshers-core",
534
- "package_version": "0.1.0",
534
+ "package_version": "0.2.0",
535
535
  "repository": "https://github.com/kmarchais/meshers",
536
536
  "license": "MIT",
537
537
  "licenses": [
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "meshers-core"
3
3
  description = "CPU tetrahedral meshing of implicit solids and constrained intersections"
4
- version = "0.1.0"
4
+ version = "0.2.0"
5
5
  edition = "2024"
6
6
  rust-version = "1.95.0"
7
7
  license = "MIT"
@@ -15,3 +15,11 @@ serde_json = "1"
15
15
 
16
16
  [lints.rust]
17
17
  unsafe_code = "forbid"
18
+
19
+ [features]
20
+ wide-constraints = []
21
+ experimental-quads = []
22
+
23
+ [[example]]
24
+ name = "adapt_surface"
25
+ required-features = ["experimental-quads"]
@@ -0,0 +1,65 @@
1
+ //! Periodic Gyroid sheet adaptation with per-phase timing.
2
+ //! cargo run --release --example adapt_gyroid -- 1e-3
3
+ use meshers_core::implicit::{Options, Region, generate};
4
+ use meshers_core::{Point, tet_adapt};
5
+
6
+ fn main() {
7
+ let mut args = std::env::args().skip(1);
8
+ let tolerance: f64 = args.next().map_or(1e-3, |v| v.parse().unwrap());
9
+ let passes: usize = args.next().map_or(1, |v| v.parse().unwrap());
10
+ let iterations: usize = args.next().map_or(4, |v| v.parse().unwrap());
11
+ let cells: usize = args.next().map_or(24, |v| v.parse().unwrap());
12
+ let field = |p: Point| {
13
+ let t = std::f64::consts::TAU;
14
+ ((t * p[0]).sin() * (t * p[1]).cos()
15
+ + (t * p[1]).sin() * (t * p[2]).cos()
16
+ + (t * p[2]).sin() * (t * p[0]).cos())
17
+ / 0.55
18
+ };
19
+ let base = Options {
20
+ bounds: [[-0.5; 3], [0.5; 3]],
21
+ cells: [cells; 3],
22
+ region: Region::Band {
23
+ lower: -1.,
24
+ upper: 1.,
25
+ },
26
+ periodic: [true; 3],
27
+ geometry_tolerance: tolerance,
28
+ optimize_passes: passes,
29
+ ..Default::default()
30
+ };
31
+ let start = std::time::Instant::now();
32
+ let out = generate(
33
+ &field,
34
+ Options {
35
+ adapt: Some(tet_adapt::Options {
36
+ tolerance,
37
+ min_length: tolerance,
38
+ max_length: 0.1,
39
+ iterations,
40
+ ..Default::default()
41
+ }),
42
+ ..base
43
+ },
44
+ )
45
+ .unwrap();
46
+ let total = start.elapsed().as_secs_f64();
47
+ let stats = out.diagnostics.adapt.unwrap();
48
+ println!(
49
+ "tets {} total {total:.2}s q_min {:.3}",
50
+ out.mesh.tets.len(),
51
+ out.diagnostics.minimum_mmg_quality
52
+ );
53
+ let names = [
54
+ "sizing",
55
+ "split",
56
+ "collapse",
57
+ "edge removal",
58
+ "face swap",
59
+ "relocate",
60
+ "enforce+unfold",
61
+ ];
62
+ for (name, s) in names.iter().zip(stats.seconds) {
63
+ println!(" {name:15} {s:.3}s");
64
+ }
65
+ }
@@ -0,0 +1,54 @@
1
+ //! Adaptive Gyroid sheet surface with per-phase timing.
2
+ //! cargo run --release --features experimental-quads --example adapt_surface -- 1e-3 32 8
3
+ use meshers_core::{Point, quads::Band, remesh, triangles};
4
+
5
+ fn main() {
6
+ let mut args = std::env::args().skip(1);
7
+ let tolerance: f64 = args.next().map_or(1e-3, |v| v.parse().unwrap());
8
+ let cells: usize = args.next().map_or(32, |v| v.parse().unwrap());
9
+ let iterations: usize = args.next().map_or(8, |v| v.parse().unwrap());
10
+ let field = |p: Point| {
11
+ let t = std::f64::consts::TAU;
12
+ ((t * p[0]).sin() * (t * p[1]).cos()
13
+ + (t * p[1]).sin() * (t * p[2]).cos()
14
+ + (t * p[2]).sin() * (t * p[0]).cos())
15
+ / 0.55
16
+ };
17
+ let band = Band {
18
+ field: &field,
19
+ bounds: [[-0.5; 3], [0.5; 3]],
20
+ levels: [-1., 1.],
21
+ };
22
+ let start = std::time::Instant::now();
23
+ let mut mesh = triangles::extract(&band, cells).unwrap();
24
+ let extraction = start.elapsed().as_secs_f64();
25
+ let extracted = mesh.faces.len();
26
+ let options = remesh::Options {
27
+ tolerance,
28
+ min_length: tolerance / 2.,
29
+ max_length: 0.25,
30
+ gradation: 0.3,
31
+ iterations,
32
+ balanced: std::env::args().any(|a| a == "balanced"),
33
+ fast: std::env::args().any(|a| a == "fast"),
34
+ };
35
+ let stats = remesh::adapt(&mut mesh, &band, &options).unwrap();
36
+ let total = start.elapsed().as_secs_f64();
37
+ println!(
38
+ "extracted {extracted} -> {} triangles, max error {:.2e}, total {total:.3}s",
39
+ mesh.faces.len(),
40
+ stats.maximum_error
41
+ );
42
+ println!(" {:15} {extraction:.3}s", "extraction");
43
+ let names = [
44
+ "sizing",
45
+ "split",
46
+ "collapse",
47
+ "flip",
48
+ "relax",
49
+ "enforce+validate",
50
+ ];
51
+ for (name, s) in names.iter().zip(stats.seconds) {
52
+ println!(" {name:15} {s:.3}s");
53
+ }
54
+ }
@@ -11,7 +11,7 @@ struct Export<'a> {
11
11
  parameters: &'a [Point],
12
12
  tetrahedra: &'a [[usize; 4]],
13
13
  surface: &'a [[usize; 3]],
14
- constraints: &'a [u8],
14
+ constraints: &'a [meshers_core::implicit::intersection::ConstraintMask],
15
15
  seconds: f64,
16
16
  minimum_quality: f64,
17
17
  elements_below_quality_01: usize,
@@ -5,6 +5,128 @@ type Edge = [usize; 2];
5
5
  fn edge(a: usize, b: usize) -> Edge {
6
6
  if a < b { [a, b] } else { [b, a] }
7
7
  }
8
+
9
+ /// One nested pass over disjoint interior edge stars, ranked by squared length.
10
+ /// Boundary coordinates and triangles remain unchanged. The node budget is an upper limit.
11
+ pub(crate) fn refine_interior(
12
+ mesh: &mut Mesh,
13
+ node_budget: usize,
14
+ minimum_quality: f64,
15
+ max_tets: usize,
16
+ geometry: &impl crate::geometry::Geometry,
17
+ ) -> Result<usize, String> {
18
+ if node_budget == 0 || mesh.tets.is_empty() {
19
+ return Ok(0);
20
+ }
21
+ geometry.check()?;
22
+ let boundary_edges: HashSet<_> = mesh
23
+ .surface
24
+ .iter()
25
+ .flat_map(|f| [edge(f[0], f[1]), edge(f[1], f[2]), edge(f[2], f[0])])
26
+ .collect();
27
+ let mut incidence = Vec::with_capacity(mesh.tets.len() * 6);
28
+ for (ci, t) in mesh.tets.iter().enumerate() {
29
+ for i in 0..4 {
30
+ for j in i + 1..4 {
31
+ incidence.push((edge(t[i], t[j]), ci));
32
+ }
33
+ }
34
+ }
35
+ incidence.sort_unstable();
36
+ geometry.check()?;
37
+ let [lo, hi] = geometry.bounds();
38
+ let midpoint = |e: Edge| {
39
+ std::array::from_fn::<_, 3, _>(|a| (mesh.points[e[0]][a] + mesh.points[e[1]][a]) * 0.5)
40
+ };
41
+ let mut candidates = Vec::new();
42
+ let mut start = 0;
43
+ while start < incidence.len() {
44
+ let e = incidence[start].0;
45
+ let mut end = start + 1;
46
+ while end < incidence.len() && incidence[end].0 == e {
47
+ end += 1;
48
+ }
49
+ let p = midpoint(e);
50
+ let delta = sub(mesh.points[e[0]], mesh.points[e[1]]);
51
+ let length2 = dot(delta, delta);
52
+ if !boundary_edges.contains(&e)
53
+ && (0..3).all(|a| {
54
+ p[a] > lo[a] + (hi[a] - lo[a]) * 1e-9 && p[a] < hi[a] - (hi[a] - lo[a]) * 1e-9
55
+ })
56
+ && length2 > 1e-20
57
+ {
58
+ candidates.push((e, start, end, length2));
59
+ }
60
+ start = end;
61
+ }
62
+ candidates.sort_unstable_by(|a, b| b.3.total_cmp(&a.3).then(a.0.cmp(&b.0)));
63
+ let mut removed = vec![false; mesh.tets.len()];
64
+ let mut additions = Vec::new();
65
+ let original_nodes = mesh.points.len();
66
+ let mut output_tets = mesh.tets.len();
67
+ for (index, (e, start, end, _)) in candidates.into_iter().enumerate() {
68
+ if mesh.points.len() - original_nodes >= node_budget {
69
+ break;
70
+ }
71
+ if index % 1024 == 0 {
72
+ geometry.check()?;
73
+ }
74
+ let star = &incidence[start..end];
75
+ if star.iter().any(|&(_, ci)| removed[ci])
76
+ || output_tets.saturating_add(star.len()) > max_tets
77
+ {
78
+ continue;
79
+ }
80
+ let p = std::array::from_fn(|a| (mesh.points[e[0]][a] + mesh.points[e[1]][a]) * 0.5);
81
+ let id = mesh.points.len();
82
+ let mut children = Vec::with_capacity(star.len() * 2);
83
+ for &(_, ci) in star {
84
+ for endpoint in e {
85
+ let mut t = mesh.tets[ci];
86
+ *t.iter_mut().find(|v| **v == endpoint).unwrap() = id;
87
+ let xyz = t.map(|i| if i == id { p } else { mesh.points[i] });
88
+ let det = determinant(xyz);
89
+ let edge_sum: f64 = (0..4)
90
+ .flat_map(|i| {
91
+ (i + 1..4).map(move |j| {
92
+ let delta = sub(xyz[i], xyz[j]);
93
+ dot(delta, delta)
94
+ })
95
+ })
96
+ .sum();
97
+ let q = (432. * det * det / edge_sum.powi(3)).sqrt();
98
+ if !det.is_finite() || det <= 0. || !q.is_finite() || q < minimum_quality {
99
+ children.clear();
100
+ break;
101
+ }
102
+ children.push(t);
103
+ }
104
+ if children.is_empty() {
105
+ break;
106
+ }
107
+ }
108
+ if children.len() == star.len() * 2 {
109
+ mesh.points.push(p);
110
+ for &(_, ci) in star {
111
+ removed[ci] = true;
112
+ }
113
+ additions.extend(children);
114
+ output_tets += star.len();
115
+ }
116
+ }
117
+ mesh.tets = mesh
118
+ .tets
119
+ .iter()
120
+ .enumerate()
121
+ .filter_map(|(i, &t)| (!removed[i]).then_some(t))
122
+ .chain(additions)
123
+ .collect();
124
+ geometry.check()?;
125
+ if mesh.surface != boundary(&mesh.tets)? {
126
+ return Err("interior refinement changed the oriented boundary".into());
127
+ }
128
+ Ok(mesh.points.len() - original_nodes)
129
+ }
8
130
  fn orbit(mesh: &Mesh, e: Edge, geometry: &impl crate::geometry::Geometry) -> [[i64; 3]; 2] {
9
131
  let mut k = e.map(|i| geometry.rank(mesh.points[i]));
10
132
  k.sort_unstable();
@@ -199,7 +321,9 @@ pub(crate) fn refine_for(
199
321
  let mut t = mesh.tets[ci];
200
322
  *t.iter_mut().find(|v| **v == endpoint).unwrap() = id;
201
323
  let xyz = t.map(|i| if i == id { p } else { mesh.points[i] });
202
- if determinant(xyz) <= 0. || quality(xyz).powf(1.5) < 0.25 {
324
+ if determinant(xyz) <= 0.
325
+ || quality(xyz).powf(1.5) < geometry.refinement_quality_floor()
326
+ {
203
327
  valid = false;
204
328
  break;
205
329
  }
@@ -313,6 +437,27 @@ fn update_boundary(
313
437
  mod tests {
314
438
  use super::*;
315
439
  #[test]
440
+ fn interior_quality_rejection_leaves_mesh_unchanged() {
441
+ let mut mesh = crate::implicit::generate(
442
+ &|_| -1.,
443
+ crate::implicit::Options {
444
+ cells: [4; 3],
445
+ optimize_passes: 0,
446
+ ..Default::default()
447
+ },
448
+ )
449
+ .unwrap()
450
+ .mesh;
451
+ let points = mesh.points.clone();
452
+ let tets = mesh.tets.clone();
453
+ assert_eq!(
454
+ refine_interior(&mut mesh, 20, 1., 10000, &crate::geometry::Legacy).unwrap(),
455
+ 0
456
+ );
457
+ assert_eq!(mesh.points, points);
458
+ assert_eq!(mesh.tets, tets);
459
+ }
460
+ #[test]
316
461
  fn incremental_boundary_matches_full_edge_bisection() {
317
462
  let old = [[0, 1, 2, 3]];
318
463
  let new = [[0, 4, 2, 3], [4, 1, 2, 3]];
@@ -1,6 +1,25 @@
1
1
  //! Internal geometry context shared by specialized and generic algorithms.
2
2
  use crate::Point;
3
3
  pub(crate) trait Geometry: Sync {
4
+ fn constraints(&self) -> Option<&[&dyn crate::implicit::ScalarField]> {
5
+ None
6
+ }
7
+ fn halfspaces(&self) -> Option<&[[f64; 4]]> {
8
+ None
9
+ }
10
+ fn halfspace_regions(&self) -> Option<&[crate::implicit::intersection::ConstraintMask]> {
11
+ None
12
+ }
13
+ /// Physical reference length for dimensionless optimizer penalties.
14
+ fn optimizer_length(&self) -> f64 {
15
+ 1.
16
+ }
17
+ fn refinement_quality_floor(&self) -> f64 {
18
+ 0.25
19
+ }
20
+ fn minimum_quality(&self) -> f64 {
21
+ 0.
22
+ }
4
23
  fn value(&self, p: Point) -> f64;
5
24
  fn batched(&self) -> bool {
6
25
  false
@@ -8,6 +27,9 @@ pub(crate) trait Geometry: Sync {
8
27
  fn values(&self, points: &[Point]) -> Vec<f64> {
9
28
  points.iter().map(|&p| self.value(p)).collect()
10
29
  }
30
+ fn target_lengths(&self, _points: &[Point]) -> Option<Vec<f64>> {
31
+ None
32
+ }
11
33
  fn gradients(&self, points: &[Point]) -> Vec<Point> {
12
34
  points.iter().map(|&p| self.gradient(p)).collect()
13
35
  }
@@ -20,6 +42,10 @@ pub(crate) trait Geometry: Sync {
20
42
  fn band(&self) -> bool {
21
43
  true
22
44
  }
45
+ /// Implicit pipelines preserve this limit while improving element quality.
46
+ fn geometry_tolerance(&self) -> Option<f64> {
47
+ None
48
+ }
23
49
  fn locked(&self, p: Point) -> [bool; 3] {
24
50
  let [lo, hi] = self.bounds();
25
51
  std::array::from_fn(|a| p[a] == lo[a] || p[a] == hi[a])
@@ -1,10 +1,14 @@
1
1
  //! Conservative short-edge removal, preserving constraint strata and periodic orbits.
2
- use super::Output;
2
+ use super::{MAX_CONSTRAINTS, Output};
3
3
  use crate::implicit::Options;
4
4
  use crate::{Point, Tet, determinant, quality};
5
5
  use std::collections::{BTreeMap, BTreeSet};
6
6
 
7
7
  type Simplex = Vec<usize>;
8
+ #[cfg(feature = "wide-constraints")]
9
+ type StratumMask = u64;
10
+ #[cfg(not(feature = "wide-constraints"))]
11
+ type StratumMask = u16;
8
12
  fn subsets(vertices: &[usize], out: &mut BTreeSet<Simplex>) {
9
13
  for bits in 1..(1 << vertices.len()) {
10
14
  let mut s: Vec<_> = vertices
@@ -16,7 +20,7 @@ fn subsets(vertices: &[usize], out: &mut BTreeSet<Simplex>) {
16
20
  out.insert(s);
17
21
  }
18
22
  }
19
- fn link_ok(a: usize, b: usize, ids: &BTreeSet<usize>, tets: &[Tet], masks: &[u16]) -> bool {
23
+ fn link_ok(a: usize, b: usize, ids: &BTreeSet<usize>, tets: &[Tet], masks: &[StratumMask]) -> bool {
20
24
  let mut la = BTreeSet::new();
21
25
  let mut lb = BTreeSet::new();
22
26
  let mut le = BTreeSet::new();
@@ -98,7 +102,7 @@ fn link_ok(a: usize, b: usize, ids: &BTreeSet<usize>, tets: &[Tet], masks: &[u16
98
102
  // A topologically valid solid collapse can still connect two separate rims.
99
103
  // Preserve the feature subcomplex too: no new labelled curve edge, no lost
100
104
  // curve edge except the collapsed one, and the 1D link condition on each rim.
101
- fn feature_link_ok(a: usize, b: usize, faces: &[[usize; 3]], masks: &[u16]) -> bool {
105
+ fn feature_link_ok(a: usize, b: usize, faces: &[[usize; 3]], masks: &[StratumMask]) -> bool {
102
106
  let features = |faces: &[[usize; 3]]| {
103
107
  let mut edges = BTreeSet::new();
104
108
  for f in faces {
@@ -107,11 +111,11 @@ fn feature_link_ok(a: usize, b: usize, faces: &[[usize; 3]], masks: &[u16]) -> b
107
111
  if common.count_ones() < 2 {
108
112
  continue;
109
113
  }
110
- for x in 0..14 {
114
+ for x in 0..MAX_CONSTRAINTS + 6 {
111
115
  if common & (1 << x) == 0 {
112
116
  continue;
113
117
  }
114
- for y in x + 1..14 {
118
+ for y in x + 1..MAX_CONSTRAINTS + 6 {
115
119
  let bits = (1 << x) | (1 << y);
116
120
  if common & bits == bits {
117
121
  edges.insert((bits, u.min(v), u.max(v)));
@@ -169,12 +173,12 @@ fn feature_link_ok(a: usize, b: usize, faces: &[[usize; 3]], masks: &[u16]) -> b
169
173
  fn q(points: &[Point], t: Tet) -> f64 {
170
174
  quality(t.map(|i| points[i])).powf(1.5)
171
175
  }
172
- fn cap(p: Point, o: &Options) -> u16 {
176
+ fn cap(p: Point, o: &Options) -> StratumMask {
173
177
  (0..3).fold(0, |bits, a| {
174
178
  bits | if p[a] == o.bounds[0][a] {
175
- 1 << (8 + 2 * a)
179
+ 1 << (MAX_CONSTRAINTS + 2 * a)
176
180
  } else if p[a] == o.bounds[1][a] {
177
- 1 << (9 + 2 * a)
181
+ 1 << (MAX_CONSTRAINTS + 1 + 2 * a)
178
182
  } else {
179
183
  0
180
184
  }
@@ -214,7 +218,7 @@ pub(super) fn improve(output: &mut Output, o: &Options) -> Result<usize, String>
214
218
  .constraints
215
219
  .iter()
216
220
  .zip(&output.parameters)
217
- .map(|(&m, &p)| u16::from(m) | cap(p, o))
221
+ .map(|(&m, &p)| StratumMask::from(m) | cap(p, o))
218
222
  .collect();
219
223
  let mut accepted = 0;
220
224
  for _ in 0..o.optimize_passes {
@@ -380,6 +384,7 @@ pub(super) fn smooth(
380
384
  o: &Options,
381
385
  fields: &[&dyn super::ScalarField],
382
386
  map: &impl Fn(Point) -> Point,
387
+ regions: Option<&[super::ConstraintMask]>,
383
388
  ) -> Result<usize, String> {
384
389
  let [lo, hi] = o.bounds;
385
390
  let h = (0..3)
@@ -468,7 +473,18 @@ pub(super) fn smooth(
468
473
  {
469
474
  continue;
470
475
  }
471
- if !active.is_empty() && fields.iter().any(|f| f.value(p) > 1e-9) {
476
+ let outside = |mask: super::ConstraintMask| {
477
+ fields
478
+ .iter()
479
+ .enumerate()
480
+ .any(|(i, f)| mask & (1 << i) != 0 && f.value(p) > 1e-9)
481
+ };
482
+ if !active.is_empty()
483
+ && regions.map_or_else(
484
+ || outside(super::ConstraintMask::MAX),
485
+ |regions| regions.iter().all(|&mask| outside(mask)),
486
+ )
487
+ {
472
488
  continue;
473
489
  }
474
490
  let parameters: Vec<Point> = group
@@ -719,4 +735,22 @@ mod tests {
719
735
  ];
720
736
  assert!(!feature_link_ok(0, 1, &faces, &masks));
721
737
  }
738
+ #[cfg(feature = "wide-constraints")]
739
+ #[test]
740
+ fn high_constraint_and_cap_bits_preserve_feature_links() {
741
+ let wall = 1 << 31;
742
+ let cap = 1 << (MAX_CONSTRAINTS + 5);
743
+ let masks = [wall | cap, wall | cap, wall | cap, wall | cap, wall, cap];
744
+ let faces = [
745
+ [2, 0, 4],
746
+ [2, 5, 0],
747
+ [0, 1, 4],
748
+ [0, 5, 1],
749
+ [1, 3, 4],
750
+ [1, 5, 3],
751
+ ];
752
+ assert!(feature_link_ok(0, 1, &faces, &masks));
753
+ let masks = [wall, wall | cap, wall | cap, wall, wall];
754
+ assert!(!feature_link_ok(0, 1, &[[0, 2, 3], [1, 3, 4]], &masks));
755
+ }
722
756
  }