kernel: trim on the record: split, side by the tool's field, pieces removed

trim / trim_keep / mutual_trim keep their signatures and behaviour; the
truck walk over the split solid is gone. The body is split where the tool
meets it (splitface::split_any), each piece's side is the tool's field at
the centre of its mesh triangle where the field is largest (the record
mesher), regions come from splitface::regions, and the pieces not kept go
through remove_faces, whose history record::after carries back to the
body. A mutual trim's nearest boundary edge is found on the schema 4
record: edges one face uses once, their middles through CurveEval.

(not built: base in progress)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 00:41:28 -04:00
parent 065e61ec7b
commit 22734a55f9
+62 -66
View File
@@ -4,14 +4,12 @@
//! when one stops short of the other.
use solidcraft_geom::Vec3;
use truck_modeling as mt;
use solidcraft_geom::brep::eval::CurveEval;
use crate::body::Body;
use crate::record::{Rebuild, rebuilt_body};
use crate::splitface::{SplitTool, ToolSide};
use crate::surface_extend::{ExtendLimit, ExtendMode, ExtendOptions};
use crate::surfaces::surface_body;
use crate::{KernelError, Result, guard};
use crate::{KernelError, Result};
/// Most points one trim picks with.
const MAX_PICKS: usize = 10_000;
@@ -58,53 +56,62 @@ pub fn trim(body: &Body, tool: &SplitTool, keep: Vec3) -> Result<Body> {
/// A surface body trimmed by a tool, keeping what `keep` says. Each kept face is the face it is
/// a piece of (its history is over `[body]`).
///
/// The body is split where the tool meets it ([`crate::splitface::split_any`]) and the pieces
/// not kept are taken away ([`crate::remove_faces`]); both carry their history, so the kept
/// faces are the body's own, cut.
pub fn trim_keep(body: &Body, tool: &SplitTool, keep: TrimKeep) -> Result<Body> {
body.require_brep("trim")?;
keep.check()?;
let total = body.solid.face_iter().count();
let all: Vec<usize> = (0..total).collect();
let all: Vec<usize> = (0..body.face_count()).collect();
let (split, _) = crate::splitface::split_any(body, &all, tool)?;
let one_side = match keep {
TrimKeep::Side(p) if crate::splitface::field_count(tool)? == 1 => Some(crate::splitface::side_of_point(tool, p)?),
_ => None,
};
let picked = match one_side {
Some(_) => Vec::new(),
None => nearest_faces(&split, keep.points())?,
};
guard("trim", || {
let mut kept = Vec::new();
match one_side {
Some(side) => {
for f in split.solid.face_iter() {
let x = crate::splitface::face_side(f, tool, split.size())?;
if (x >= 0.0) == (side >= 0.0) {
kept.push(f.clone());
}
}
}
None => {
let (region, count) = crate::splitface::regions(&split, tool)?;
let mut wanted = vec![false; count];
for f in &picked {
if let Some(w) = region.get(*f).and_then(|r| wanted.get_mut(*r)) {
*w = true;
}
}
for (f, r) in split.solid.face_iter().zip(&region) {
if wanted.get(*r).copied().unwrap_or(false) {
kept.push(f.clone());
}
let kept: Vec<bool> = match keep {
TrimKeep::Side(p) if crate::splitface::field_count(tool)? == 1 => {
let side = ToolSide::of(tool)?;
let want = side.at(p);
if !want.is_finite() {
return Err(KernelError::Invalid("the side of the tool the point is on can't be told".into()));
}
face_sides(&split, &side)?.into_iter().map(|x| (x >= 0.0) == (want >= 0.0)).collect()
}
_ => {
let picked = nearest_faces(&split, keep.points())?;
let (region, count) = crate::splitface::regions(&split, tool)?;
let mut wanted = vec![false; count];
for f in &picked {
if let Some(w) = region.get(*f).and_then(|r| wanted.get_mut(*r)) {
*w = true;
}
}
region.iter().map(|r| wanted.get(*r).copied().unwrap_or(false)).collect()
}
if kept.is_empty() {
return Err(KernelError::Invalid("nothing is left on that side of the tool".into()));
};
if !kept.iter().any(|k| *k) {
return Err(KernelError::Invalid("nothing is left on that side of the tool".into()));
}
let dropped: Vec<usize> = kept.iter().enumerate().filter(|(_, k)| !**k).map(|(i, _)| i).collect();
let trimmed = if dropped.is_empty() { split.clone() } else { crate::remove_faces(&split, &dropped)? };
// The kept pieces are the split's own; its history carries them back to `body`.
Ok(crate::record::after(trimmed, &split, &crate::record::counts(&[body])))
}
/// The side of the tool each face of `body` (in its face order) lies on: the tool's field at
/// the centre of the face's mesh triangle where it is largest (a face lies on one side once
/// the tool has cut it; on the tool's own surface the field is the rounding of zero).
fn face_sides(body: &Body, side: &ToolSide) -> Result<Vec<f64>> {
let m = body.mesh(&crate::MeshTolerance::chord(body.size() * 2e-3))?;
let mut best = vec![0.0f64; body.face_count()];
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
let Some([a, b, c]) = m.tri(t) else { continue };
let x = side.at((a + b + c) / 3.0);
if let Some(slot) = best.get_mut(*f as usize)
&& x.is_finite()
&& x.abs() > slot.abs()
{
*slot = x;
}
// The kept pieces are the split's own; its history carries them back to `body`.
let trimmed = rebuilt_body(surface_body(kept), &[&split.solid], &Rebuild::default());
Ok(crate::record::after(trimmed, &split, &crate::record::counts(&[body])))
})
}
Ok(best)
}
/// For each point, the face of `body` nearest it (over one tessellation).
@@ -182,31 +189,20 @@ fn cut_by(x: &Body, tool: &SplitTool, keep: TrimKeep, extend: bool, name: &str)
crate::extend_surface(x, &[edge], &opts)
}
/// A point in the middle of the boundary edge of `x` nearest the tool.
/// A point in the middle of the boundary edge of `x` nearest the tool: of the record's edges
/// that one face uses once, the one whose middle (half-way along its interval) is nearest.
fn nearest_boundary_edge(x: &Body, side: &ToolSide) -> Result<Vec3> {
guard("trim", || {
let mut uses: std::collections::HashMap<mt::EdgeID, (usize, mt::Edge)> = std::collections::HashMap::new();
for f in x.solid.face_iter() {
for e in f.boundaries().iter().flat_map(|w| w.edge_iter()) {
uses.entry(e.id()).or_insert((0, e.clone())).0 += 1;
}
let record = crate::native::brep_record(x)?;
let mut best: Option<(f64, Vec3)> = None;
for e in record.edges.iter().filter(|e| e.uses.len() == 1) {
let curve = CurveEval::new(&e.curve).map_err(|err| KernelError::Failed(format!("trim: an edge's curve: {err}")))?;
let Some(mid) = curve.point(0.5 * (e.interval[0] + e.interval[1])) else { continue };
let d = side.at(mid).abs();
if d.is_finite() && best.is_none_or(|(b, _)| d < b) {
best = Some((d, mid));
}
let mut best: Option<(f64, Vec3)> = None;
for (count, e) in uses.values() {
if *count != 1 {
continue;
}
use mt::{BoundedCurve, ParametricCurve};
let c = e.curve();
let (t0, t1) = c.range_tuple();
let mid = crate::body::from_p3(c.subs(0.5 * (t0 + t1)));
let d = side.at(mid).abs();
if d.is_finite() && best.is_none_or(|(b, _)| d < b) {
best = Some((d, mid));
}
}
best.map(|(_, p)| p).ok_or_else(|| KernelError::Invalid("the surface has no boundary edge to extend".into()))
})
}
best.map(|(_, p)| p).ok_or_else(|| KernelError::Invalid("the surface has no boundary edge to extend".into()))
}
#[cfg(test)]