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.
- {meshers-0.1.0 → meshers-0.2.0}/PKG-INFO +5 -4
- {meshers-0.1.0/meshers-python → meshers-0.2.0}/README.md +4 -3
- {meshers-0.1.0/meshers-python → meshers-0.2.0}/THIRDPARTY.json +1 -1
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/Cargo.toml +9 -1
- meshers-0.2.0/meshers-core/examples/adapt_gyroid.rs +65 -0
- meshers-0.2.0/meshers-core/examples/adapt_surface.rs +54 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/examples/curved_intersection.rs +1 -1
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/adaptive.rs +146 -1
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/geometry.rs +26 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/implicit/intersection/repair.rs +44 -10
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/implicit/intersection.rs +124 -17
- meshers-0.2.0/meshers-core/src/implicit/periodic_feature_research.rs +174 -0
- meshers-0.2.0/meshers-core/src/implicit.rs +2625 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/lib.rs +10 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/optimize.rs +155 -15
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/optimize_batch.rs +98 -10
- meshers-0.2.0/meshers-core/src/polyhedral.rs +284 -0
- meshers-0.2.0/meshers-core/src/projection.rs +178 -0
- meshers-0.2.0/meshers-core/src/quadratic/fit.rs +439 -0
- meshers-0.2.0/meshers-core/src/quadratic.rs +539 -0
- meshers-0.2.0/meshers-core/src/quads.rs +1704 -0
- meshers-0.2.0/meshers-core/src/remesh.rs +1115 -0
- meshers-0.2.0/meshers-core/src/testdata/frd-cap-star.json +362 -0
- meshers-0.2.0/meshers-core/src/testdata/spinodoid-cap-star.json +1258 -0
- meshers-0.2.0/meshers-core/src/tet_adapt.rs +2495 -0
- meshers-0.2.0/meshers-core/src/triangles.rs +1263 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/tests/implicit.rs +345 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/tests/intersection.rs +109 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/Cargo.lock +2 -2
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/Cargo.toml +6 -2
- {meshers-0.1.0 → meshers-0.2.0/meshers-python}/README.md +4 -3
- {meshers-0.1.0 → meshers-0.2.0/meshers-python}/THIRDPARTY.json +1 -1
- meshers-0.2.0/meshers-python/src/compiled/polyhedral.rs +171 -0
- meshers-0.2.0/meshers-python/src/compiled.rs +571 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/src/lib.rs +403 -29
- meshers-0.2.0/meshers-python/src/surface.rs +131 -0
- meshers-0.2.0/meshers-python/tests/test_adaptive.py +146 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_api.py +76 -0
- meshers-0.2.0/meshers-python/tests/test_compiled.py +243 -0
- meshers-0.2.0/meshers-python/tests/test_curved_fit.py +63 -0
- meshers-0.2.0/meshers-python/tests/test_curved_volumes.py +77 -0
- meshers-0.2.0/meshers-python/tests/test_interp.py +87 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_intersection.py +237 -0
- meshers-0.2.0/meshers-python/tests/test_nonlinear_constraints.py +138 -0
- meshers-0.2.0/meshers-python/tests/test_observations.py +99 -0
- meshers-0.2.0/meshers-python/tests/test_periodic_zero_level.py +51 -0
- meshers-0.2.0/meshers-python/tests/test_polyhedral.py +392 -0
- meshers-0.2.0/meshers-python/tests/test_polyhedral_regions.py +155 -0
- meshers-0.2.0/meshers-python/tests/test_quality.py +130 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_rust_compile.py +4 -1
- meshers-0.2.0/meshers-python/tests/test_size_field.py +88 -0
- meshers-0.2.0/meshers-python/tests/test_surface.py +54 -0
- {meshers-0.1.0 → meshers-0.2.0}/pyproject.toml +1 -1
- {meshers-0.1.0 → meshers-0.2.0}/python/meshers/__init__.py +199 -13
- {meshers-0.1.0 → meshers-0.2.0}/python/meshers/_meshers.pyi +35 -1
- {meshers-0.1.0 → meshers-0.2.0}/python/meshers/expression.py +113 -4
- {meshers-0.1.0 → meshers-0.2.0}/python/meshers/intersection.py +95 -5
- meshers-0.2.0/python/meshers/surface.py +143 -0
- meshers-0.1.0/meshers-core/src/implicit.rs +0 -904
- meshers-0.1.0/meshers-python/src/compiled.rs +0 -252
- meshers-0.1.0/meshers-python/tests/test_compiled.py +0 -122
- meshers-0.1.0/meshers-python/tests/test_quality.py +0 -59
- {meshers-0.1.0 → meshers-0.2.0}/LICENSE +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/LICENSE +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/README.md +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/examples/gyroid.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/examples/implicit.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/examples/implicit_sphere.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/accelerator.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/error.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/src/profile.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/tests/batching.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-core/tests/public_api.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/COMPILED.md +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/LICENSE +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/THIRDPARTY.md +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/basic.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/cancellation.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/curved_tpms.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/evaluators.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/graded.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/intersection.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/examples/periodic.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/src/intersection.rs +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_angular.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_automatic.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_examples.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/meshers-python/tests/test_tpms_release.py +0 -0
- {meshers-0.1.0 → meshers-0.2.0}/python/meshers/compiler.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
74
|
-
|
|
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
|
+
# 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
|
-
|
|
54
|
-
|
|
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
|
|
@@ -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.
|
|
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 [
|
|
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.
|
|
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: &[
|
|
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: &[
|
|
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..
|
|
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..
|
|
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) ->
|
|
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 << (
|
|
179
|
+
1 << (MAX_CONSTRAINTS + 2 * a)
|
|
176
180
|
} else if p[a] == o.bounds[1][a] {
|
|
177
|
-
1 << (
|
|
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)|
|
|
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
|
-
|
|
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
|
}
|