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));
+2 -2
View File
@@ -134,8 +134,8 @@ pub use boolean_diagnostics::{BooleanDiagnostics, BooleanFallback, BooleanStrate
pub use boundary::{Boundary, boundary_patch};
pub use bridge::{Continuity, Frame, blend_between, bridge, chain, chain_closes, frames, frames_across, sample_grid};
pub use build::{
PathSeg, box_solid, canonical_revolve_axis, cylinder, extrude, extrude_tapered, extrude_tapered_between, loft, revolve, sphere, sweep,
sweep_with, torus,
PathSeg, box_solid, canonical_revolve_axis, cylinder, extrude, extrude_tapered, extrude_tapered_between, loft, revolve, revolve_whole, sphere,
sweep, sweep_with, torus,
};
pub use continuity::{
ContinuityReport, ContinuityTolerances, EdgePick, MAX_SAMPLES, MIN_SAMPLES, SeamReport, SeamVerdict, SeamWorst, pick_edges, seam_diagnostics,
+16 -8
View File
@@ -208,15 +208,23 @@ fn expected_revolution(b: &Body, r: &Region2) -> Vec<Vec<FaceOrigin>> {
#[test]
fn revolve_partial_and_full() {
let r = Region2 { outer: rect(10.0, 0.0, 20.0, 5.0), holes: vec![rect(12.0, 1.0, 14.0, 2.0).reversed()] };
// Under half a turn truck sweeps in two parts, beyond it in three; a full turn has no caps.
for (angle, parts, caps) in [(FRAC_PI_2, 2, 2), (1.5 * PI, 3, 2), (TAU, 3, 0)] {
let b = crate::revolve(&SIDE, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, angle).unwrap().pop().unwrap();
let got = origins(&b);
assert_eq!(got, expected_revolution(&b, &r), "angle {angle}");
for k in 0..8 {
assert_eq!(got.iter().filter(|o| **o == [P(k)]).count(), parts, "segment {k} at angle {angle}");
// Under half a turn truck sweeps in two parts, beyond it in three; a full turn has no caps,
// and its planar faces (from the segments square to the axis) are whole (#726), as a partial
// turn's are when it is final (`revolve_whole`).
let segs: Vec<Seg2> = r.outer.segs.iter().chain(r.holes.iter().flat_map(|h| &h.segs)).cloned().collect();
for whole in [false, true] {
for (angle, parts, caps) in [(FRAC_PI_2, 2, 2), (1.5 * PI, 3, 2), (TAU, 3, 0)] {
let revolve = if whole { crate::revolve_whole } else { crate::revolve };
let b = revolve(&SIDE, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, angle).unwrap().pop().unwrap();
let got = origins(&b);
assert_eq!(got, expected_revolution(&b, &r), "angle {angle}, whole {whole}");
for (k, s) in segs.iter().enumerate() {
let square = matches!(s, Seg2::Line { a, b } if (a.y - b.y).abs() < 1e-12);
let want = if (angle == TAU || whole) && square { 1 } else { parts };
assert_eq!(got.iter().filter(|o| **o == [P(k)]).count(), want, "segment {k} at angle {angle}, whole {whole}");
}
assert_eq!(got.iter().filter(|o| **o == [START] || **o == [END]).count(), caps);
}
assert_eq!(got.iter().filter(|o| **o == [START] || **o == [END]).count(), caps);
}
}