forked from 1b-agent/fieldcraft-upstream
mesh: projection and relaxation keep to a merged face's B-rep pieces
A merged face on several surfaces (a sliver absorbed into its neighbour) had no exact surface, so the projection target fell back to its triangles: flat curvature probes gave fewer divisions round the boss and the enclosure base's symmetric mesh dropped from 0.70 to 0.51 once its absorbs reached the cells. The target now projects a point of such a face onto the exact surface of the B-rep piece it is on, trimmed to that piece (Tessellation::with_pieces, Shape::tessellate_pieces); node relaxation slides on it the same way. Test from commands: the enclosure base's four slivers absorbed, meshed whole and by symmetry, the absorbs kept in every cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
09385f069f
commit
a2e747b9ba
7 files changed
+186
-61
No files matched your search
@@ -42,6 +42,9 @@ pub(super) struct Input<'a> {
|
|||||||
/// The design's edge seeds ([`super::seeds::edge_seeds`]); the mesher applies those on its
|
/// The design's edge seeds ([`super::seeds::edge_seeds`]); the mesher applies those on its
|
||||||
/// edges.
|
/// edges.
|
||||||
pub(super) seeds: Vec<fieldcraft_mesh::seeds::EdgeSeed>,
|
pub(super) seeds: Vec<fieldcraft_mesh::seeds::EdgeSeed>,
|
||||||
|
/// Per triangle of `mesh`, the B-rep face it lies on: its piece of a merged face (empty for
|
||||||
|
/// a surface that is not a body's).
|
||||||
|
pub(super) pieces: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analytic(s: FaceSurface) -> Surface {
|
fn analytic(s: FaceSurface) -> Surface {
|
||||||
@@ -73,7 +76,8 @@ pub(super) fn shape_input(shape: &Shape, tolerance: Option<f64>) -> Result<Input
|
|||||||
}
|
}
|
||||||
let surfaces = shape.face_surfaces()?.into_iter().map(|f| f.map(analytic)).collect();
|
let surfaces = shape.face_surfaces()?.into_iter().map(|f| f.map(analytic)).collect();
|
||||||
let face_names = shape.faces().iter().map(|f| f.name.clone()).collect();
|
let face_names = shape.faces().iter().map(|f| f.name.clone()).collect();
|
||||||
Ok(Input { name: shape.name.clone(), mesh: Cow::Owned(shape.tessellate(tol)?), surfaces, face_names, diagonal: diag, seeds: Vec::new() })
|
let (mesh, pieces) = shape.tessellate_pieces(tol)?;
|
||||||
|
Ok(Input { name: shape.name.clone(), mesh: Cow::Owned(mesh), surfaces, face_names, diagonal: diag, seeds: Vec::new(), pieces })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The imported surface `surface` names, with the body name its mesh is tagged with.
|
/// The imported surface `surface` names, with the body name its mesh is tagged with.
|
||||||
@@ -109,7 +113,15 @@ pub(super) fn inputs<'a>(s: &'a Session, p: &Value) -> Result<Vec<Input<'a>>> {
|
|||||||
}
|
}
|
||||||
// Faces named by index, as mesh.tet.generate names them.
|
// Faces named by index, as mesh.tet.generate names them.
|
||||||
let face_names = (0..t.faces).map(|f| f.to_string()).collect();
|
let face_names = (0..t.faces).map(|f| f.to_string()).collect();
|
||||||
Ok(vec![Input { name, mesh: Cow::Borrowed(&t.mesh), surfaces: Vec::new(), face_names, diagonal: diagonal(lo, hi), seeds: Vec::new() }])
|
Ok(vec![Input {
|
||||||
|
name,
|
||||||
|
mesh: Cow::Borrowed(&t.mesh),
|
||||||
|
surfaces: Vec::new(),
|
||||||
|
face_names,
|
||||||
|
diagonal: diagonal(lo, hi),
|
||||||
|
seeds: Vec::new(),
|
||||||
|
pieces: Vec::new(),
|
||||||
|
}])
|
||||||
}
|
}
|
||||||
(None, None) => {
|
(None, None) => {
|
||||||
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first, or give `surface`".into()))?;
|
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first, or give `surface`".into()))?;
|
||||||
|
|||||||
@@ -216,21 +216,30 @@ pub(super) fn quality_options(p: &Value, max_tets: u64) -> Result<(QualityOption
|
|||||||
/// The exact surfaces of a body's faces, for the quality stage's projections (none when the
|
/// The exact surfaces of a body's faces, for the quality stage's projections (none when the
|
||||||
/// kernel cannot give them: the stage then keeps to the tessellation).
|
/// kernel cannot give them: the stage then keeps to the tessellation).
|
||||||
pub fn analytic_surfaces(shape: &fieldcraft_geometry::Shape) -> Vec<Option<AnalyticSurface>> {
|
pub fn analytic_surfaces(shape: &fieldcraft_geometry::Shape) -> Vec<Option<AnalyticSurface>> {
|
||||||
|
shape.face_surfaces().unwrap_or_default().into_iter().map(|f| f.map(analytic_surface)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn analytic_surface(f: fieldcraft_geometry::FaceSurface) -> AnalyticSurface {
|
||||||
use fieldcraft_geometry::FaceSurface as F;
|
use fieldcraft_geometry::FaceSurface as F;
|
||||||
shape
|
match f {
|
||||||
.face_surfaces()
|
F::Plane { normal, point } => AnalyticSurface::Plane { normal, point },
|
||||||
.unwrap_or_default()
|
F::Cylinder { origin, axis, radius } => AnalyticSurface::Cylinder { origin, axis, radius },
|
||||||
.into_iter()
|
F::Cone { apex, axis, half_angle } => AnalyticSurface::Cone { apex, axis, half_angle },
|
||||||
.map(|f| {
|
F::Sphere { center, radius } => AnalyticSurface::Sphere { center, radius },
|
||||||
f.map(|f| match f {
|
F::Torus { center, axis, major, minor } => AnalyticSurface::Torus { center, axis, major, minor },
|
||||||
F::Plane { normal, point } => AnalyticSurface::Plane { normal, point },
|
}
|
||||||
F::Cylinder { origin, axis, radius } => AnalyticSurface::Cylinder { origin, axis, radius },
|
}
|
||||||
F::Cone { apex, axis, half_angle } => AnalyticSurface::Cone { apex, axis, half_angle },
|
|
||||||
F::Sphere { center, radius } => AnalyticSurface::Sphere { center, radius },
|
/// The B-rep pieces of `shape`'s merged faces (virtual topology) for a projection target built
|
||||||
F::Torus { center, axis, major, minor } => AnalyticSurface::Torus { center, axis, major, minor },
|
/// on its tessellation whose triangles lie on the B-rep faces `tri_piece`
|
||||||
})
|
/// ([`fieldcraft_geometry::Shape::tessellate_pieces`]): a merged face on no one surface then
|
||||||
})
|
/// projects onto each piece's own; none when no face is merged.
|
||||||
.collect()
|
pub(crate) fn pieces(shape: &fieldcraft_geometry::Shape, tri_piece: &[u32]) -> fieldcraft_mesh::tet::geometry::Pieces {
|
||||||
|
if !shape.has_merged_faces() {
|
||||||
|
return fieldcraft_mesh::tet::geometry::Pieces::default();
|
||||||
|
}
|
||||||
|
let analytic = shape.brep_face_surfaces().unwrap_or_default().into_iter().map(|f| f.map(analytic_surface)).collect();
|
||||||
|
fieldcraft_mesh::tet::geometry::Pieces { tri_piece: tri_piece.to_vec(), analytic }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn quality_json(q: &TetQuality) -> Value {
|
pub(super) fn quality_json(q: &TetQuality) -> Value {
|
||||||
|
|||||||
@@ -2405,6 +2405,42 @@ mod acceptance {
|
|||||||
accept(&mut s, &r, 0.5, 10.0);
|
accept(&mut s, &r, 0.5, 10.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The enclosure base's bosses cut a 0.42 mm strip out of the middle of the inner fillet
|
||||||
|
/// (#511, #513): geometry.check finds the slivers and geometry.face.absorb absorbs them.
|
||||||
|
/// The absorbed faces stay absorbed in the cells the plan's symmetry cuts the body into, and
|
||||||
|
/// the projection keeps to the exact surfaces of a merged face's pieces, so the mesh is no
|
||||||
|
/// worse for them, whole or by symmetry.
|
||||||
|
#[test]
|
||||||
|
fn absorbed_slivers_stay_absorbed_in_the_plan_s_cells() {
|
||||||
|
let mut commands = vec![
|
||||||
|
json!({"command": "solid.box", "params": {"length": 56, "width": 42, "height": 16, "body_name": "Base"}}),
|
||||||
|
json!({"command": "solid.fillet", "params": {"edges": [[0, 0, 8], [56, 0, 8], [56, 42, 8], [0, 42, 8]], "radius": 5}}),
|
||||||
|
json!({"command": "solid.shell", "params": {"faces": [[28, 21, 16]], "thickness": 3, "body": "Base"}}),
|
||||||
|
];
|
||||||
|
for (x, y) in [(-1.0, -1.0), (57.0, -1.0), (-1.0, 43.0), (57.0, 43.0)] {
|
||||||
|
commands.push(json!({"command": "solid.cylinder", "params": {"diameter": 13, "height": 16, "base": [x, y, 0], "operation": "join", "targets": ["Base"]}}));
|
||||||
|
}
|
||||||
|
let (_d, mut s) = open(json!(commands));
|
||||||
|
let check = s.execute("geometry.check", &json!({"body": "Base", "size": 1.5, "mark": true})).unwrap();
|
||||||
|
assert_eq!(check["counts"]["sliver_face"], 4, "{check}");
|
||||||
|
let absorbed = s.execute("geometry.face.absorb", &json!({"from_check": true})).unwrap();
|
||||||
|
assert_eq!(absorbed["absorbed"].as_array().map(Vec::len), Some(4), "{absorbed}");
|
||||||
|
let plan = s.execute("mesh.plan", &json!({"body": "Base", "size": 1.5, "layers": 2})).unwrap();
|
||||||
|
assert_eq!(plan["mesher"], "project", "{plan}");
|
||||||
|
let r = s.execute("mesh.plan.apply", &json!({"plan": plan["name"], "symmetry": false})).unwrap();
|
||||||
|
accept(&mut s, &r, 0.6, 10.0);
|
||||||
|
let r = s.execute("mesh.plan.apply", &json!({"plan": plan["name"]})).unwrap();
|
||||||
|
assert_eq!(r["symmetry"].as_array().map(Vec::len), Some(2), "{r}");
|
||||||
|
let design = s.design.as_deref().unwrap();
|
||||||
|
assert!(design.errors.is_empty(), "{:?}", design.errors);
|
||||||
|
// Each quarter holds one corner, its sliver absorbed.
|
||||||
|
let cells: Vec<_> = design.bodies.iter().filter(|b| b.name.starts_with("Base/")).collect();
|
||||||
|
assert_eq!(cells.len(), 4);
|
||||||
|
assert!(cells.iter().all(|c| c.merged_faces().len() == 1), "{:?}", cells.iter().map(|c| c.merged_faces()).collect::<Vec<_>>());
|
||||||
|
assert_eq!(design.source_body("Base").map(|b| b.merged_faces().len()), Some(4));
|
||||||
|
accept(&mut s, &r, 0.65, 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
/// The crank arm sample, whole (#455, #190): its sides are at an angle to the axes and its
|
/// The crank arm sample, whole (#455, #190): its sides are at an angle to the axes and its
|
||||||
/// root arcs meet the bosses at a kink, so it takes the ring-and-pillow layout: each boss an
|
/// root arcs meet the bosses at a kink, so it takes the ring-and-pillow layout: each boss an
|
||||||
/// annulus from its bore or pedal hole to its circle through every level, the arm a carved
|
/// annulus from its bore or pedal hole to its circle through every level, the arm a carved
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ pub(crate) fn open_body(s: &Session, name: &str, tolerance: Option<f64>, raw: &V
|
|||||||
let surface =
|
let surface =
|
||||||
BoundarySurface::new(&input.mesh.positions, &input.mesh.triangles, &input.mesh.tri_face, &super::mesh::surface_options(raw, &input.mesh)?)
|
BoundarySurface::new(&input.mesh.positions, &input.mesh.triangles, &input.mesh.tri_face, &super::mesh::surface_options(raw, &input.mesh)?)
|
||||||
.map_err(|e| EngineError::Params(format!("body `{name}`: {e}")))?;
|
.map_err(|e| EngineError::Params(format!("body `{name}`: {e}")))?;
|
||||||
let target = Tessellation::new(&surface, super::mesh::analytic_surfaces(shape));
|
let target = Tessellation::new(&surface, super::mesh::analytic_surfaces(shape)).with_pieces(&surface, super::mesh::pieces(shape, &input.pieces));
|
||||||
let m = shape.measure()?;
|
let m = shape.measure()?;
|
||||||
Ok(Body { input, surface, target, lo: m.bbox_min, hi: m.bbox_max })
|
Ok(Body { input, surface, target, lo: m.bbox_min, hi: m.bbox_max })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ fn design_geometry(s: &Session, nodes: &BTreeSet<u32>) -> Result<DesignGeometry>
|
|||||||
None => None,
|
None => None,
|
||||||
Some(shape) => {
|
Some(shape) => {
|
||||||
let tol = (shape.size()? * 1e-3).max(1e-6);
|
let tol = (shape.size()? * 1e-3).max(1e-6);
|
||||||
let mesh = shape.tessellate(tol)?;
|
let (mesh, pieces) = shape.tessellate_pieces(tol)?;
|
||||||
let surface = BoundarySurface::new(
|
let surface = BoundarySurface::new(
|
||||||
&mesh.positions,
|
&mesh.positions,
|
||||||
&mesh.triangles,
|
&mesh.triangles,
|
||||||
@@ -202,7 +202,7 @@ fn design_geometry(s: &Session, nodes: &BTreeSet<u32>) -> Result<DesignGeometry>
|
|||||||
&SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() },
|
&SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() },
|
||||||
)?;
|
)?;
|
||||||
let analytic = super::mesh::analytic_surfaces(shape);
|
let analytic = super::mesh::analytic_surfaces(shape);
|
||||||
g.bodies.push(Tessellation::new(&surface, analytic.clone()));
|
g.bodies.push(Tessellation::new(&surface, analytic.clone()).with_pieces(&surface, super::mesh::pieces(shape, &pieces)));
|
||||||
Some((g.bodies.len() - 1, analytic))
|
Some((g.bodies.len() - 1, analytic))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -206,8 +206,8 @@ impl Shape {
|
|||||||
self.virtual_surfaces(brep)
|
self.virtual_surfaces(brep)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Shape::face_surfaces`] by B-rep face index.
|
/// [`Shape::face_surfaces`] by B-rep face index (the pieces of [`Shape::tessellate_pieces`]).
|
||||||
pub(crate) fn brep_face_surfaces(&self) -> Result<Vec<Option<FaceSurface>>> {
|
pub fn brep_face_surfaces(&self) -> Result<Vec<Option<FaceSurface>>> {
|
||||||
guard("face surfaces", || {
|
guard("face surfaces", || {
|
||||||
Ok((0..self.body.face_count())
|
Ok((0..self.body.face_count())
|
||||||
.map(|f| {
|
.map(|f| {
|
||||||
@@ -257,18 +257,37 @@ impl Shape {
|
|||||||
/// by face index ([`Shape::faces`]): merged faces are one face, without the edges between
|
/// by face index ([`Shape::faces`]): merged faces are one face, without the edges between
|
||||||
/// their B-rep faces.
|
/// their B-rep faces.
|
||||||
pub fn tessellate(&self, tolerance: f64) -> Result<SurfaceMesh> {
|
pub fn tessellate(&self, tolerance: f64) -> Result<SurfaceMesh> {
|
||||||
|
Ok(self.virtual_mesh(self.tessellate_brep(tolerance)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Shape::tessellate`], and per triangle the B-rep face it lies on: its piece of a merged
|
||||||
|
/// face, whose surface is in [`Shape::brep_face_surfaces`] (the same as its tag when no face
|
||||||
|
/// is merged).
|
||||||
|
pub fn tessellate_pieces(&self, tolerance: f64) -> Result<(SurfaceMesh, Vec<u32>)> {
|
||||||
|
let brep = self.tessellate_brep(tolerance)?;
|
||||||
|
let pieces = brep.tri_face.clone();
|
||||||
|
Ok((self.virtual_mesh(brep), pieces))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The B-rep's own tessellation, tagged by B-rep face.
|
||||||
|
fn tessellate_brep(&self, tolerance: f64) -> Result<SurfaceMesh> {
|
||||||
if !(tolerance > 0.0 && tolerance.is_finite()) {
|
if !(tolerance > 0.0 && tolerance.is_finite()) {
|
||||||
return Err(Error::Invalid(format!("tessellation tolerance must be a positive number, got {tolerance}")));
|
return Err(Error::Invalid(format!("tessellation tolerance must be a positive number, got {tolerance}")));
|
||||||
}
|
}
|
||||||
let m = guard("tessellate", || self.body.tessellate(tolerance))?;
|
let m = guard("tessellate", || self.body.tessellate(tolerance))?;
|
||||||
Ok(self.virtual_mesh(SurfaceMesh {
|
Ok(SurfaceMesh {
|
||||||
positions: m.positions.into_iter().map(xyz).collect(),
|
positions: m.positions.into_iter().map(xyz).collect(),
|
||||||
triangles: m.triangles,
|
triangles: m.triangles,
|
||||||
tri_face: m.tri_face,
|
tri_face: m.tri_face,
|
||||||
weld_distance: 2.0 * LINEAR_TOLERANCE,
|
weld_distance: 2.0 * LINEAR_TOLERANCE,
|
||||||
edges: m.edges.into_iter().map(|e| e.into_iter().map(xyz).collect()).collect(),
|
edges: m.edges.into_iter().map(|e| e.into_iter().map(xyz).collect()).collect(),
|
||||||
edge_faces: m.edge_faces,
|
edge_faces: m.edge_faces,
|
||||||
}))
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether some face is made of several B-rep faces ([`virtual_faces`]).
|
||||||
|
pub fn has_merged_faces(&self) -> bool {
|
||||||
|
!self.face_map.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -429,6 +429,20 @@ pub struct Tessellation {
|
|||||||
/// A tree over each face pair's segments.
|
/// A tree over each face pair's segments.
|
||||||
pair_trees: std::collections::BTreeMap<[u32; 2], Tree>,
|
pair_trees: std::collections::BTreeMap<[u32; 2], Tree>,
|
||||||
analytic: Vec<Option<AnalyticSurface>>,
|
analytic: Vec<Option<AnalyticSurface>>,
|
||||||
|
/// Faces made of several B-rep faces (virtual topology): each triangle's B-rep face (its
|
||||||
|
/// piece) and each piece's exact surface, for a face with no one surface of its own. Empty
|
||||||
|
/// when no face is merged.
|
||||||
|
tri_piece: Vec<u32>,
|
||||||
|
piece_analytic: Vec<Option<AnalyticSurface>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The B-rep faces of merged faces: per triangle of the surface a [`Tessellation`] is built on
|
||||||
|
/// (as given to [`BoundarySurface::new`]) the B-rep face it lies on, and each B-rep face's exact
|
||||||
|
/// surface when known.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Pieces {
|
||||||
|
pub tri_piece: Vec<u32>,
|
||||||
|
pub analytic: Vec<Option<AnalyticSurface>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tessellation {
|
impl Tessellation {
|
||||||
@@ -480,7 +494,31 @@ impl Tessellation {
|
|||||||
by_pair.entry(*pair).or_default().push(i as u32);
|
by_pair.entry(*pair).or_default().push(i as u32);
|
||||||
}
|
}
|
||||||
let pair_trees = by_pair.into_iter().map(|(pair, items)| (pair, Tree::build_over(&seg_boxes, items))).collect();
|
let pair_trees = by_pair.into_iter().map(|(pair, items)| (pair, Tree::build_over(&seg_boxes, items))).collect();
|
||||||
Tessellation { points, triangles, tri_face, tri_tree, face_trees, segments, seg_tree, pair_trees, analytic }
|
Tessellation {
|
||||||
|
points,
|
||||||
|
triangles,
|
||||||
|
tri_face,
|
||||||
|
tri_tree,
|
||||||
|
face_trees,
|
||||||
|
segments,
|
||||||
|
seg_tree,
|
||||||
|
pair_trees,
|
||||||
|
analytic,
|
||||||
|
tri_piece: Vec::new(),
|
||||||
|
piece_analytic: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same target with the B-rep pieces of its merged faces (`surface` the boundary
|
||||||
|
/// surface it was built on): a point on a merged face that lies on no one surface goes onto
|
||||||
|
/// the exact surface of the piece it is on, trimmed to that piece, instead of its triangles.
|
||||||
|
pub fn with_pieces(mut self, surface: &BoundarySurface, pieces: Pieces) -> Tessellation {
|
||||||
|
if pieces.tri_piece.is_empty() {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
self.tri_piece = surface.source_triangle().iter().map(|&t| pieces.tri_piece.get(t as usize).copied().unwrap_or(u32::MAX)).collect();
|
||||||
|
self.piece_analytic = pieces.analytic;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pt(&self, v: u32) -> P {
|
fn pt(&self, v: u32) -> P {
|
||||||
@@ -492,19 +530,42 @@ impl Tessellation {
|
|||||||
self.analytic.get(face as usize).and_then(|a| a.as_ref())
|
self.analytic.get(face as usize).and_then(|a| a.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The closest point on the face's triangles, and the longest edge of the triangle it is
|
/// The exact surface of a point of face `face` on its triangle `t`: the face's own, else
|
||||||
/// on (the local scale of the tessellation).
|
/// that of the B-rep piece of a merged face the triangle lies on.
|
||||||
fn on_tessellation(&self, face: u32, p: P) -> Option<(P, f64)> {
|
fn exact(&self, face: u32, t: u32) -> Option<&AnalyticSurface> {
|
||||||
|
self.analytic(face).or_else(|| {
|
||||||
|
let piece = *self.tri_piece.get(t as usize)?;
|
||||||
|
self.piece_analytic.get(piece as usize).and_then(|a| a.as_ref())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether triangle `u` lies on the same piece of face `face` as triangle `t` (always, for
|
||||||
|
/// a face with a surface of its own or without pieces).
|
||||||
|
fn same_piece(&self, face: u32, t: u32, u: u32) -> bool {
|
||||||
|
self.analytic(face).is_some() || self.tri_piece.get(t as usize) == self.tri_piece.get(u as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The nearest triangle of face `face` to `p` and the closest point on it.
|
||||||
|
fn nearest_triangle(&self, face: u32, p: P) -> Option<(P, u32)> {
|
||||||
let accept = |t: u32| self.tri_face.get(t as usize) == Some(&face);
|
let accept = |t: u32| self.tri_face.get(t as usize) == Some(&face);
|
||||||
let dist = |t: u32| {
|
let dist = |t: u32| {
|
||||||
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
||||||
let (a, b, c) = (self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]));
|
let q = closest_on_triangle(p, self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]));
|
||||||
let q = closest_on_triangle(p, a, b, c);
|
|
||||||
let d = sub(q, p);
|
let d = sub(q, p);
|
||||||
let longest = measure::dist(a, b).max(measure::dist(b, c)).max(measure::dist(c, a));
|
// The triangle index travels through the f64 exactly (counts are capped far below
|
||||||
(dot(d, d), [q[0], q[1], q[2], longest])
|
// 2^53).
|
||||||
|
(dot(d, d), [q[0], q[1], q[2], f64::from(t)])
|
||||||
};
|
};
|
||||||
self.face_tree(face).nearest4(p, &accept, &dist).map(|r| ([r[0], r[1], r[2]], r[3]))
|
self.face_tree(face).nearest4(p, &accept, &dist).map(|r| ([r[0], r[1], r[2]], r[3] as u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The closest point on the face's triangles, the longest edge of the triangle it is on
|
||||||
|
/// (the local scale of the tessellation) and that triangle.
|
||||||
|
fn on_tessellation(&self, face: u32, p: P) -> Option<(P, f64, u32)> {
|
||||||
|
let (q, t) = self.nearest_triangle(face, p)?;
|
||||||
|
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
||||||
|
let (a, b, c) = (self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]));
|
||||||
|
Some((q, measure::dist(a, b).max(measure::dist(b, c)).max(measure::dist(c, a)), t))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The B-rep face whose triangles lie nearest to `p`.
|
/// The B-rep face whose triangles lie nearest to `p`.
|
||||||
@@ -552,24 +613,17 @@ impl Tessellation {
|
|||||||
/// closest point of the face's triangles. Unlike [`BoundaryGeometry::project_face`] it never
|
/// closest point of the face's triangles. Unlike [`BoundaryGeometry::project_face`] it never
|
||||||
/// returns a point of the surface's extension beyond the face's boundary, whatever the
|
/// returns a point of the surface's extension beyond the face's boundary, whatever the
|
||||||
/// distance from `p` — nor of the same surface's other faces: the pieces of one fillet split
|
/// distance from `p` — nor of the same surface's other faces: the pieces of one fillet split
|
||||||
/// by a cut share their cylinder, and a point on one piece is off the others.
|
/// by a cut share their cylinder, and a point on one piece is off the others. On a merged
|
||||||
|
/// face lying on no one surface, the surface is that of the piece its nearest triangle is
|
||||||
|
/// on, trimmed to that piece.
|
||||||
pub fn project_trimmed(&self, face: u32, p: P) -> Option<P> {
|
pub fn project_trimmed(&self, face: u32, p: P) -> Option<P> {
|
||||||
let accept = |t: u32| self.tri_face.get(t as usize) == Some(&face);
|
|
||||||
let corners = |t: u32| {
|
let corners = |t: u32| {
|
||||||
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
||||||
(self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]))
|
(self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]))
|
||||||
};
|
};
|
||||||
let near = {
|
let (near, nearest) = self.nearest_triangle(face, p)?;
|
||||||
let dist = |t: u32| {
|
let accept = |t: u32| self.tri_face.get(t as usize) == Some(&face) && self.same_piece(face, nearest, t);
|
||||||
let (a, b, c) = corners(t);
|
let Some(surface) = self.exact(face, nearest) else { return Some(near) };
|
||||||
let q = closest_on_triangle(p, a, b, c);
|
|
||||||
let d = sub(q, p);
|
|
||||||
(dot(d, d), [q[0], q[1], q[2], 0.0])
|
|
||||||
};
|
|
||||||
let r = self.face_tree(face).nearest4(p, &accept, &dist)?;
|
|
||||||
[r[0], r[1], r[2]]
|
|
||||||
};
|
|
||||||
let Some(surface) = self.analytic(face) else { return Some(near) };
|
|
||||||
let Some(q) = surface.project(p) else { return Some(near) };
|
let Some(q) = surface.project(p) else { return Some(near) };
|
||||||
// The nearest triangle to the surface's point, with its slack: the largest gap between
|
// The nearest triangle to the surface's point, with its slack: the largest gap between
|
||||||
// the triangle and the surface (at its edges' midpoints and centre: a chord's deepest
|
// the triangle and the surface (at its edges' midpoints and centre: a chord's deepest
|
||||||
@@ -589,19 +643,14 @@ impl Tessellation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The outward unit normal of B-rep face `face` near `p`: the exact surface's (oriented like
|
/// The outward unit normal of B-rep face `face` near `p`: the exact surface's (oriented like
|
||||||
/// the nearest triangle) when known, else the nearest triangle's.
|
/// the nearest triangle; on a merged face, its piece's) when known, else the nearest
|
||||||
|
/// triangle's.
|
||||||
pub fn face_normal(&self, face: u32, p: P) -> Option<P> {
|
pub fn face_normal(&self, face: u32, p: P) -> Option<P> {
|
||||||
let accept = |t: u32| self.tri_face.get(t as usize) == Some(&face);
|
let (_, t) = self.nearest_triangle(face, p)?;
|
||||||
let dist = |t: u32| {
|
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
||||||
let tri = self.triangles.get(t as usize).copied().unwrap_or([0; 3]);
|
let (a, b, c) = (self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]));
|
||||||
let (a, b, c) = (self.pt(tri[0]), self.pt(tri[1]), self.pt(tri[2]));
|
let flat = unit(cross(sub(b, a), sub(c, a)))?;
|
||||||
let d = sub(closest_on_triangle(p, a, b, c), p);
|
match self.exact(face, t).and_then(|s| s.normal_at(p)) {
|
||||||
let n = unit(cross(sub(b, a), sub(c, a))).unwrap_or([0.0; 3]);
|
|
||||||
(dot(d, d), [n[0], n[1], n[2], 0.0])
|
|
||||||
};
|
|
||||||
let r = self.face_tree(face).nearest4(p, &accept, &dist)?;
|
|
||||||
let flat = unit([r[0], r[1], r[2]])?;
|
|
||||||
match self.analytic(face).and_then(|s| s.normal_at(p)) {
|
|
||||||
Some(n) if dot(n, flat) < 0.0 => Some(scale(n, -1.0)),
|
Some(n) if dot(n, flat) < 0.0 => Some(scale(n, -1.0)),
|
||||||
Some(n) => Some(n),
|
Some(n) => Some(n),
|
||||||
None => Some(flat),
|
None => Some(flat),
|
||||||
@@ -611,8 +660,8 @@ impl Tessellation {
|
|||||||
|
|
||||||
impl BoundaryGeometry for Tessellation {
|
impl BoundaryGeometry for Tessellation {
|
||||||
fn project_face(&self, face: u32, p: P) -> Option<P> {
|
fn project_face(&self, face: u32, p: P) -> Option<P> {
|
||||||
let (near, scale_len) = self.on_tessellation(face, p)?;
|
let (near, scale_len, t) = self.on_tessellation(face, p)?;
|
||||||
match self.analytic(face) {
|
match self.exact(face, t) {
|
||||||
// The exact surface's closest point, kept when it is near the tessellation's: the
|
// The exact surface's closest point, kept when it is near the tessellation's: the
|
||||||
// tessellation deviates from its surface by far less than half its triangles' size,
|
// tessellation deviates from its surface by far less than half its triangles' size,
|
||||||
// so a point further away is a projection onto another part of the surface (the far
|
// so a point further away is a projection onto another part of the surface (the far
|
||||||
@@ -664,8 +713,8 @@ impl Tessellation {
|
|||||||
/// tessellation's chordal tolerance, as [`BoundaryGeometry::project_face`]).
|
/// tessellation's chordal tolerance, as [`BoundaryGeometry::project_face`]).
|
||||||
pub fn slide_face(&self, face: u32, p: P, d: P) -> Option<P> {
|
pub fn slide_face(&self, face: u32, p: P, d: P) -> Option<P> {
|
||||||
let target = add(p, d);
|
let target = add(p, d);
|
||||||
let (near, scale_len) = self.on_tessellation(face, target)?;
|
let (near, scale_len, t) = self.on_tessellation(face, target)?;
|
||||||
if let Some(q) = self.analytic(face).and_then(|s| s.slide(p, d)) {
|
if let Some(q) = self.exact(face, t).and_then(|s| s.slide(p, d)) {
|
||||||
let tol = (0.5 * scale_len).max(2.0 * measure::dist(target, near));
|
let tol = (0.5 * scale_len).max(2.0 * measure::dist(target, near));
|
||||||
if measure::dist(q, near) <= tol {
|
if measure::dist(q, near) <= tol {
|
||||||
return Some(q);
|
return Some(q);
|
||||||
|
|||||||
Reference in new issue
Block a user