Thanks @moritzuehling.
This commit is contained in:
1 parent
843fa0dac6
commit
9be8bb5e6b
7 files changed
+536
-24
No files matched your search
Vendored
+7
@@ -83,6 +83,13 @@ Changes:
|
||||
## truck-meshalgo 0.4.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck)
|
||||
|
||||
Changes:
|
||||
- `tessellation/triangulation.rs`, `closed.rs`: retry nearest trim projections outside a
|
||||
spline's knot domain without the preceding point's hint, preferring a bounded fit within
|
||||
the caller's chord tolerance, with a bounded damped least-squares fallback where
|
||||
Newton's method diverges. Affine plane charts remain unbounded; modelling surfaces may
|
||||
retain genuine continuation when no bounded fit is accurate enough. Geometrically closed,
|
||||
smooth spline seams get a local periodic chart during meshing, with wrapped evaluation,
|
||||
so a trim crossing the seam stays a small patch. The stored geometry is unchanged (#1974).
|
||||
- `tessellation/mod.rs`, `triangulation.rs`: a separate provisional `preview_face`
|
||||
path accepts at most 4,096 already sampled boundary points and a fixed grid of
|
||||
at most 16 subdivisions per direction, including periodic seam connections.
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
//! SolidCraft: a local periodic chart for geometrically closed spline surfaces.
|
||||
//! The stored spline still has a finite knot domain; only meshing wraps its parameters.
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct Closed<'a, S> {
|
||||
pub base: &'a S,
|
||||
ranges: [Option<(f64, f64)>; 2],
|
||||
periods: [Option<f64>; 2],
|
||||
inferred: [bool; 2],
|
||||
}
|
||||
|
||||
impl<'a, S: PreMeshableSurface> Closed<'a, S> {
|
||||
pub fn new(base: &'a S) -> Self {
|
||||
let (u, v) = base.try_range_tuple();
|
||||
let mut out = Self { base, ranges: [u, v], periods: [base.u_period(), base.v_period()], inferred: [false; 2] };
|
||||
let (Some((u0, u1)), Some((v0, v1))) = (u, v) else { return out };
|
||||
if ![u0, u1, v0, v1].iter().all(|v| v.is_finite()) || u1 <= u0 || v1 <= v0 {
|
||||
return out;
|
||||
}
|
||||
let origin = base.subs(u0, v0);
|
||||
let scale = (0..=4)
|
||||
.flat_map(|i| (0..=4).map(move |j| (i, j)))
|
||||
.map(|(i, j)| base.subs(u0 + (u1 - u0) * i as f64 / 4., v0 + (v1 - v0) * j as f64 / 4.).distance(origin))
|
||||
.fold(0.0_f64, f64::max);
|
||||
let tol2 = (scale * 1e-8).max(1e-9).powi(2);
|
||||
for d in 0..2 {
|
||||
if out.periods[d].is_some() {
|
||||
continue;
|
||||
}
|
||||
let (lo, hi, a, b) = if d == 0 { (u0, u1, v0, v1) } else { (v0, v1, u0, u1) };
|
||||
let mut noncollapsed = false;
|
||||
let closed = (0..=8).all(|k| {
|
||||
let t = a + (b - a) * k as f64 / 8.;
|
||||
let at = |x| if d == 0 { (x, t) } else { (t, x) };
|
||||
let (p, q, m) = (at(lo), at(hi), at((lo + hi) * 0.5));
|
||||
let first = base.subs(p.0, p.1);
|
||||
let (second, middle) = (base.subs(q.0, q.1), base.subs(m.0, m.1));
|
||||
if ![first.x, first.y, first.z, second.x, second.y, second.z, middle.x, middle.y, middle.z].iter().all(|x| x.is_finite()) {
|
||||
return false;
|
||||
}
|
||||
noncollapsed |= first.distance2(middle) > tol2;
|
||||
if first.distance2(second) > tol2 {
|
||||
return false;
|
||||
}
|
||||
let (n0, n1) = (base.normal(p.0, p.1), base.normal(q.0, q.1));
|
||||
!n0.magnitude2().is_finite() || !n1.magnitude2().is_finite() || n0.dot(n1) > 1.0 - 1e-6
|
||||
});
|
||||
if closed && noncollapsed {
|
||||
out.periods[d] = Some(hi - lo);
|
||||
out.inferred[d] = true;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn periods(&self) -> [Option<f64>; 2] {
|
||||
self.periods
|
||||
}
|
||||
|
||||
pub fn wrap(&self, (u, v): (f64, f64)) -> (f64, f64) {
|
||||
let mut p = [u, v];
|
||||
for (d, x) in p.iter_mut().enumerate() {
|
||||
if let (true, Some((lo, hi)), Some(period)) = (self.inferred[d], self.ranges[d], self.periods[d]) {
|
||||
if *x < lo || *x > hi {
|
||||
*x = lo + (*x - lo).rem_euclid(period);
|
||||
}
|
||||
}
|
||||
}
|
||||
(p[0], p[1])
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: PreMeshableSurface> ParametricSurface for Closed<'_, S> {
|
||||
type Point = Point3;
|
||||
type Vector = Vector3;
|
||||
fn subs(&self, u: f64, v: f64) -> Point3 {
|
||||
let (u, v) = self.wrap((u, v));
|
||||
self.base.subs(u, v)
|
||||
}
|
||||
fn uder(&self, u: f64, v: f64) -> Vector3 {
|
||||
let (u, v) = self.wrap((u, v));
|
||||
self.base.uder(u, v)
|
||||
}
|
||||
fn vder(&self, u: f64, v: f64) -> Vector3 {
|
||||
let (u, v) = self.wrap((u, v));
|
||||
self.base.vder(u, v)
|
||||
}
|
||||
fn uuder(&self, u: f64, v: f64) -> Vector3 {
|
||||
let (u, v) = self.wrap((u, v));
|
||||
self.base.uuder(u, v)
|
||||
}
|
||||
fn uvder(&self, u: f64, v: f64) -> Vector3 {
|
||||
let (u, v) = self.wrap((u, v));
|
||||
self.base.uvder(u, v)
|
||||
}
|
||||
fn vvder(&self, u: f64, v: f64) -> Vector3 {
|
||||
let (u, v) = self.wrap((u, v));
|
||||
self.base.vvder(u, v)
|
||||
}
|
||||
fn parameter_range(&self) -> (ParameterRange, ParameterRange) {
|
||||
self.base.parameter_range()
|
||||
}
|
||||
fn u_period(&self) -> Option<f64> {
|
||||
self.periods[0]
|
||||
}
|
||||
fn v_period(&self) -> Option<f64> {
|
||||
self.periods[1]
|
||||
}
|
||||
}
|
||||
impl<S: PreMeshableSurface> ParametricSurface3D for Closed<'_, S> {
|
||||
fn normal(&self, u: f64, v: f64) -> Vector3 {
|
||||
let (u, v) = self.wrap((u, v));
|
||||
self.base.normal(u, v)
|
||||
}
|
||||
}
|
||||
impl<S: PreMeshableSurface> ParameterDivision2D for Closed<'_, S> {
|
||||
fn parameter_division(&self, range: ((f64, f64), (f64, f64)), tol: f64) -> (Vec<f64>, Vec<f64>) {
|
||||
triangulation::bounded_surface_division(self, range, Deflection::chord(tol), triangulation::MAX_GRID)
|
||||
}
|
||||
}
|
||||
+13
@@ -25,6 +25,18 @@ pub fn preview_face<S: RobustMeshableSurface>(surface: &S, loops: &[Vec<Point3>]
|
||||
triangulation::preview_face(surface, loops, steps)
|
||||
}
|
||||
|
||||
/// Periods of a surface, including smooth geometric closure of bounded splines.
|
||||
/// Meshing wraps inferred periods into the original knot domain before evaluation.
|
||||
pub fn surface_periods<S: PreMeshableSurface>(surface: &S) -> [Option<f64>; 2] {
|
||||
closed::Closed::new(surface).periods()
|
||||
}
|
||||
|
||||
/// Project a trim point with the same bounded, tolerance-aware search used by the mesher.
|
||||
/// A preceding parameter is a hint, never proof that the search converged correctly.
|
||||
pub fn project_trim_point<S: RobustMeshableSurface>(surface: &S, point: Point3, hint: Option<(f64, f64)>, tolerance: f64) -> Option<(f64, f64)> {
|
||||
triangulation::nearest_projection(surface, point, hint, tolerance, 100)
|
||||
}
|
||||
|
||||
/// SolidCraft: an edge's polyline as the meshers divide it (at most 20 000 points). Exposed for
|
||||
/// the kernel's regressions in the workspace CI.
|
||||
#[doc(hidden)]
|
||||
@@ -508,6 +520,7 @@ impl<C: PolylineableCurve, S: RobustMeshableSurface> RobustMeshableShape
|
||||
}
|
||||
}
|
||||
|
||||
mod closed;
|
||||
mod triangulation;
|
||||
#[cfg(feature = "test-hooks")]
|
||||
#[doc(hidden)]
|
||||
|
||||
+114
-22
@@ -16,9 +16,9 @@ type MeshedShell = Shell<Point3, PolylineCurve, Option<PolygonMesh>>;
|
||||
type MeshedCShell = CompressedShell<Point3, PolylineCurve, Option<PolygonMesh>>;
|
||||
|
||||
pub(super) trait SP<S>:
|
||||
Fn(&S, Point3, Option<(f64, f64)>) -> Option<(f64, f64)> + Parallelizable {
|
||||
Fn(&S, Point3, Option<(f64, f64)>, f64) -> Option<(f64, f64)> + Parallelizable {
|
||||
}
|
||||
impl<S, F> SP<S> for F where F: Fn(&S, Point3, Option<(f64, f64)>) -> Option<(f64, f64)> + Parallelizable {}
|
||||
impl<S, F> SP<S> for F where F: Fn(&S, Point3, Option<(f64, f64)>, f64) -> Option<(f64, f64)> + Parallelizable {}
|
||||
|
||||
/// SolidCraft: a parameter inside the surface's bounded, non-periodic ranges.
|
||||
fn in_domain<S: ParametricSurface>(surface: &S, (u, v): (f64, f64)) -> bool {
|
||||
@@ -93,6 +93,7 @@ pub(super) fn by_search_parameter<S>(
|
||||
surface: &S,
|
||||
point: Point3,
|
||||
hint: Option<(f64, f64)>,
|
||||
_accept: f64,
|
||||
) -> Option<(f64, f64)>
|
||||
where
|
||||
S: MeshableSurface,
|
||||
@@ -106,15 +107,97 @@ pub(super) fn by_search_nearest_parameter<S>(
|
||||
surface: &S,
|
||||
point: Point3,
|
||||
hint: Option<(f64, f64)>,
|
||||
accept: f64,
|
||||
) -> Option<(f64, f64)>
|
||||
where
|
||||
S: RobustMeshableSurface,
|
||||
{
|
||||
surface
|
||||
.search_parameter(point, hint, 100)
|
||||
.or_else(|| surface.search_parameter(point, None, 100))
|
||||
.or_else(|| surface.search_nearest_parameter(point, hint, 100))
|
||||
.or_else(|| surface.search_nearest_parameter(point, None, 100))
|
||||
where S: RobustMeshableSurface {
|
||||
nearest_projection(surface, point, hint, accept, 100)
|
||||
}
|
||||
|
||||
/// A hint can converge to a spline's continuation outside its knot domain. Prefer a
|
||||
/// bounded fit within the caller's chord tolerance. Some modelling surfaces really are
|
||||
/// extended beyond their stored domain; retain that continuation when no bounded fit is
|
||||
/// accurate enough, rather than losing a valid blend face.
|
||||
pub(super) fn nearest_projection<S: RobustMeshableSurface>(surface: &S, point: Point3, hint: Option<(f64,f64)>, accept: f64, trials: usize) -> Option<(f64,f64)> {
|
||||
// An affine plane's unit parameter box is only a chart, not a finite support.
|
||||
// Preserve its exact projection even when the trim extends beyond that box.
|
||||
if affine_chart(surface) {
|
||||
return surface.search_parameter(point, hint, trials)
|
||||
.or_else(|| surface.search_parameter(point, None, trials))
|
||||
.or_else(|| surface.search_nearest_parameter(point, hint, trials))
|
||||
.or_else(|| surface.search_nearest_parameter(point, None, trials));
|
||||
}
|
||||
let candidate = |uv| {
|
||||
let uv = sane(surface, point, uv);
|
||||
let error = surface.subs(uv.0, uv.1).distance2(point);
|
||||
(uv.0.is_finite() && uv.1.is_finite() && error.is_finite()).then_some((uv, error))
|
||||
};
|
||||
let fits = |&(uv, error): &((f64, f64), f64)| in_domain(surface, uv) && error <= accept * accept;
|
||||
let exact = surface.search_parameter(point, hint, trials)
|
||||
.or_else(|| surface.search_parameter(point, None, trials)).and_then(candidate);
|
||||
if let Some((uv, _)) = exact.filter(fits) { return Some(uv); }
|
||||
let hinted = surface.search_nearest_parameter(point, hint, trials).and_then(candidate);
|
||||
if let Some((uv, _)) = hinted.filter(fits) { return Some(uv); }
|
||||
let fresh = surface.search_nearest_parameter(point, None, trials).and_then(candidate);
|
||||
if let Some((uv, _)) = fresh.filter(fits) { return Some(uv); }
|
||||
let best = [exact, hinted, fresh].into_iter().flatten().min_by(|a, b| a.1.total_cmp(&b.1));
|
||||
if let Some(uv) = best.map(|x| x.0).or(hint) {
|
||||
if let Some(bounded) = bounded_projection(surface, point, uv).filter(fits) {
|
||||
return Some(bounded.0);
|
||||
}
|
||||
}
|
||||
// Even the unhinted search can fail for shallow patches of very large cylinders.
|
||||
// Their domain midpoint supplies a finite start without trusting a previous failure.
|
||||
if let (Some((a, b)), Some((c, d))) = surface.try_range_tuple() {
|
||||
if [a, b, c, d].iter().all(|x| x.is_finite()) {
|
||||
let start = (a * 0.5 + b * 0.5, c * 0.5 + d * 0.5);
|
||||
if let Some(bounded) = bounded_projection(surface, point, start).filter(fits) {
|
||||
return Some(bounded.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|x| x.0)
|
||||
}
|
||||
|
||||
fn affine_chart<S: ParametricSurface3D>(surface: &S) -> bool {
|
||||
let (Some((a, b)), Some((c, d))) = surface.try_range_tuple() else { return false };
|
||||
[a, b, c, d].iter().all(|x| x.is_finite()) && [0.0, 0.5, 1.0].into_iter().all(|t| {
|
||||
let (u, v) = (a + (b - a) * t, c + (d - c) * t);
|
||||
[surface.uuder(u, v), surface.uvder(u, v), surface.vvder(u, v)]
|
||||
.iter().all(|x| x.magnitude2() <= 1e-24)
|
||||
})
|
||||
}
|
||||
|
||||
/// Damped least squares constrained to the finite knot domain. An unconstrained Newton
|
||||
/// step is unreliable where the two tangents almost coincide. This fallback has fixed work
|
||||
/// limits and is used only after the usual searches failed to find a bounded fit.
|
||||
fn bounded_projection<S: ParametricSurface3D>(surface: &S, point: Point3, start: (f64, f64)) -> Option<((f64, f64), f64)> {
|
||||
let mut uv = clamped(surface, start);
|
||||
let mut error = surface.subs(uv.0, uv.1).distance2(point);
|
||||
let mut lambda = 1e-6_f64;
|
||||
for _ in 0..60 {
|
||||
let q = surface.subs(uv.0, uv.1);
|
||||
let (du, dv) = (surface.uder(uv.0, uv.1), surface.vder(uv.0, uv.1));
|
||||
let r = point - q;
|
||||
let (a, b, c) = (du.magnitude2(), du.dot(dv), dv.magnitude2());
|
||||
let scale = a + c;
|
||||
if !scale.is_finite() || scale <= 0.0 { break; }
|
||||
let (aa, cc) = (a + lambda * scale, c + lambda * scale);
|
||||
let det = aa * cc - b * b;
|
||||
if !det.is_finite() || det <= 0.0 { break; }
|
||||
let (g, h) = (du.dot(r), dv.dot(r));
|
||||
let next = clamped(surface, (uv.0 + (cc * g - b * h) / det, uv.1 + (aa * h - b * g) / det));
|
||||
let nd = surface.subs(next.0, next.1).distance2(point);
|
||||
if nd.is_finite() && nd <= error {
|
||||
let moved = (next.0 - uv.0).abs() + (next.1 - uv.1).abs();
|
||||
(uv, error) = (next, nd);
|
||||
lambda = (lambda * 0.1).max(1e-12);
|
||||
if moved <= 1e-15 * (1.0 + uv.0.abs() + uv.1.abs()) { break; }
|
||||
} else {
|
||||
lambda *= 10.0;
|
||||
if lambda > 1e8 { break; }
|
||||
}
|
||||
}
|
||||
(error.is_finite() && in_domain(surface, uv)).then_some((uv, error))
|
||||
}
|
||||
|
||||
/// Tessellates faces
|
||||
@@ -341,14 +424,18 @@ where
|
||||
let edges: Vec<_> = shell.edges.iter().map(tessellate_edge).collect();
|
||||
let tessellate_face = |face: &CompressedFace<S>| {
|
||||
let boundaries = face.boundaries.clone();
|
||||
let surface = &face.surface;
|
||||
let wrapped = closed::Closed::new(&face.surface);
|
||||
let search = |s: &closed::Closed<'_, S>, point, hint: Option<(f64, f64)>, accept| {
|
||||
sp(s.base, point, hint.map(|p| s.wrap(p)), accept)
|
||||
};
|
||||
let surface = &wrapped;
|
||||
let create_edge = |edge_idx: &CompressedEdgeIndex| match edge_idx.orientation {
|
||||
true => Some(edges.get(edge_idx.index)?.curve.clone()),
|
||||
false => Some(edges.get(edge_idx.index)?.curve.inverse()),
|
||||
};
|
||||
let create_boundary = |wire: &Vec<CompressedEdgeIndex>| {
|
||||
let wire_iter = wire.iter().filter_map(create_edge);
|
||||
PolyBoundaryPiece::try_new(surface, wire_iter, &sp)
|
||||
PolyBoundaryPiece::try_new(surface, wire_iter, &search, tol.chord)
|
||||
};
|
||||
let preboundary: Option<Vec<_>> = boundaries.iter().map(create_boundary).collect();
|
||||
let polygon: Option<PolygonMesh> = (|| {
|
||||
@@ -380,11 +467,16 @@ fn shell_create_polygon<S: PreMeshableSurface>(
|
||||
sp: impl SP<S>,
|
||||
cap: usize,
|
||||
) -> Face<Point3, PolylineCurve, Option<PolygonMesh>> {
|
||||
let wrapped = closed::Closed::new(surface);
|
||||
let search = |s: &closed::Closed<'_, S>, point, hint: Option<(f64, f64)>, accept| {
|
||||
sp(s.base, point, hint.map(|p| s.wrap(p)), accept)
|
||||
};
|
||||
let surface = &wrapped;
|
||||
let preboundary = wires
|
||||
.iter()
|
||||
.map(|wire: &Wire<_, _>| {
|
||||
let wire_iter = wire.iter().map(Edge::oriented_curve);
|
||||
PolyBoundaryPiece::try_new(surface, wire_iter, &sp)
|
||||
PolyBoundaryPiece::try_new(surface, wire_iter, &search, tol.chord)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>();
|
||||
let polygon: Option<PolygonMesh> = (|| {
|
||||
@@ -451,6 +543,7 @@ impl PolyBoundaryPiece {
|
||||
surface: &S,
|
||||
wire: impl Iterator<Item = PolylineCurve>,
|
||||
sp: impl SP<S>,
|
||||
accept: f64,
|
||||
) -> Option<Self> {
|
||||
let (up, vp) = (surface.u_period(), surface.v_period());
|
||||
let (urange, vrange) = surface.try_range_tuple();
|
||||
@@ -466,7 +559,7 @@ impl PolyBoundaryPiece {
|
||||
bdry3d
|
||||
.into_iter()
|
||||
.flat_map(|pt| {
|
||||
let (mut u, mut v) = match sp(surface, pt, previous) {
|
||||
let (mut u, mut v) = match sp(surface, pt, previous, accept) {
|
||||
// SolidCraft: validate exact and nearest results alike, once per
|
||||
// boundary point, including the bounded provisional preview.
|
||||
Some(hint) => sane(surface, pt, hint),
|
||||
@@ -502,7 +595,7 @@ impl PolyBoundaryPiece {
|
||||
let mut vec = match walk(bdry3d.clone()) {
|
||||
Some(vec) => vec,
|
||||
None => {
|
||||
let start = bdry3d.iter().skip(1).position(|pt| sp(surface, *pt, None).is_some())? + 1;
|
||||
let start = bdry3d.iter().skip(1).position(|pt| sp(surface, *pt, None, accept).is_some())? + 1;
|
||||
let mut rotated = bdry3d;
|
||||
rotated.rotate_left(start);
|
||||
walk(rotated)?
|
||||
@@ -845,14 +938,13 @@ pub(super) fn preview_face<S: RobustMeshableSurface>(surface: &S, loops: &[Vec<P
|
||||
|| loops.iter().map(Vec::len).sum::<usize>() > 4096 {
|
||||
return None;
|
||||
}
|
||||
let search = |surface: &S, point, hint| {
|
||||
surface.search_parameter(point, hint, 16)
|
||||
.or_else(|| surface.search_parameter(point, None, 16))
|
||||
.or_else(|| surface.search_nearest_parameter(point, hint, 16))
|
||||
.or_else(|| surface.search_nearest_parameter(point, None, 16))
|
||||
let wrapped = closed::Closed::new(surface);
|
||||
let search = |s: &closed::Closed<'_, S>, point, hint: Option<(f64, f64)>, accept| {
|
||||
nearest_projection(s.base, point, hint.map(|p| s.wrap(p)), accept, 16)
|
||||
};
|
||||
let surface = &wrapped;
|
||||
let pieces = loops.iter().map(|w| {
|
||||
PolyBoundaryPiece::try_new(surface, std::iter::once(PolylineCurve(w.clone())), search)
|
||||
PolyBoundaryPiece::try_new(surface, std::iter::once(PolylineCurve(w.clone())), search, 1e-3)
|
||||
}).collect::<Option<Vec<_>>>()?;
|
||||
let boundary = PolyBoundary::with_steps(pieces, surface, Deflection::chord(1e-3), Some(steps));
|
||||
// Projection can duplicate points at singularities; periodic seams add at most
|
||||
@@ -995,7 +1087,7 @@ pub(super) fn bounded_polyline<C: PolylineableCurve>(curve: &C, range: (f64, f64
|
||||
/// `cap` points or after 24 rounds; cells that do not evaluate (NaN) are not refined. A
|
||||
/// direction within the chord tolerance is still halved where a cell turns more than the
|
||||
/// deflection's angle along it, while the grid stays within `Deflection::MAX_ANGLE_GRID`.
|
||||
fn bounded_surface_division<S: PreMeshableSurface>(surface: &S, (ur, vr): ((f64, f64), (f64, f64)), d: Deflection, cap: usize) -> (Vec<f64>, Vec<f64>) {
|
||||
pub(super) fn bounded_surface_division<S: PreMeshableSurface>(surface: &S, (ur, vr): ((f64, f64), (f64, f64)), d: Deflection, cap: usize) -> (Vec<f64>, Vec<f64>) {
|
||||
let (tol, turn2) = (d.chord, d.turn2());
|
||||
let (mut udiv, mut vdiv) = (vec![ur.0, ur.1], vec![vr.0, vr.1]);
|
||||
for _ in 0..24 {
|
||||
|
||||
Reference in new issue
Block a user