Revolve: a partial turn's planar faces come out whole (#726) (#1718)

Thanks @SychPL.
This commit is contained in:
Mateusz Sych authored and GitHub committed 2026-10-16 15:26:10 -04:00
1 parent 8907da5e86
commit 920ea42fe1
6 files changed
+118 -34

No files matched your search

+47
View File
@@ -305,6 +305,18 @@ pub fn canonical_revolve_axis(plane: &Plane, region: &Region2, axis_origin: Vec2
/// Revolve each region about the sketch axis by `angle` radians.
/// This legacy API preserves its historical sweep sign behavior.
pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2, angle: f64) -> Result<Vec<Body>> {
revolved(plane, regions, axis_origin, axis_dir, angle, false)
}
/// [`revolve`] for bodies that are final (new bodies, not a boolean's tools): a partial turn's
/// planar faces come out whole too, as a full turn's always do (#726). A tool is left as swept:
/// healed, its planar faces meet a target exactly where they are tangent to it, and the
/// boolean leaves pieces there that touch. The boolean heals its own result anyway.
pub fn revolve_whole(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2, angle: f64) -> Result<Vec<Body>> {
revolved(plane, regions, axis_origin, axis_dir, angle, true)
}
fn revolved(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2, angle: f64, whole: bool) -> Result<Vec<Body>> {
let angle = finite(angle, "angle")?;
if angle.abs() < 1e-6 {
return Err(KernelError::Invalid("revolve angle is zero".into()));
@@ -447,11 +459,46 @@ pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir:
let closed = angle.abs() >= std::f64::consts::TAU;
let history = guard("revolve history", || Ok(record::swept_face(&solid, &f, &seeds(&profile), closed)))?;
let solid = if shift.len() > 0.0 { builder::translated(&solid, v3(shift)) } else { solid };
let (solid, history) = if closed || whole { turned_whole(solid, history) } else { (solid, history) };
out.push(record::attach(Body::new(solid)?, &[], history, Default::default())?);
}
Ok(out)
}
/// A revolved solid with its planar faces whole (#726): the sweep turns each profile edge into
/// two or three patches, the planar ones on revolved surfaces, which stayed apart until a later
/// feature healed the body, so a pick on one took part of the face. Healed as booleans heal
/// their results, each face is still generated, from the profile edges of the patches merged
/// into it. The solid as it was when healing fails or can't be followed.
fn turned_whole(solid: Solid, faces: Option<Vec<FaceProv>>) -> (Solid, Option<Vec<FaceProv>>) {
let Some(given) = faces.as_ref() else { return (solid, faces) };
let Ok(size) = Body::new(solid.clone()).map(|b| b.size()) else { return (solid, faces) };
let healed = guard("heal revolve", || {
let (healed, map) = crate::heal::heal_traced(solid.clone(), size, &[]);
let failed = || KernelError::Failed("heal revolve".into());
if !crate::boolean_record::edges_evaluate(&healed) {
return Err(failed());
}
let carried = map
.ok_or_else(failed)?
.faces
.iter()
.map(|under| {
let mut origins: Vec<_> = under.iter().map(|i| given.get(*i).map(|f| f.origins.clone())).collect::<Option<Vec<_>>>()?.concat();
origins.sort();
origins.dedup();
Some(FaceProv { origins, change: crate::history::Change::Generated })
})
.collect::<Option<Vec<_>>>()
.ok_or_else(failed)?;
Ok((healed, carried))
});
match healed {
Ok((healed, carried)) => (healed, Some(carried)),
Err(_) => (solid, faces),
}
}
/// Axis-aligned box between two corners.
pub fn box_solid(a: Vec3, b: Vec3) -> Result<Body> {
let (lo, hi) = (a.min(b), a.max(b));