Revolve: a profile that comes back to the axis along a curve revolves right, and cuts with it work (#1853) (#1866)

Thanks @SychPL.
This commit is contained in:
Mateusz Sych authored and GitHub committed 2026-10-17 13:38:41 -04:00
1 parent f480898158
commit 4a70c03deb
4 files changed
+317 -81

No files matched your search

+192 -78
View File
@@ -417,6 +417,18 @@ fn revolved(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec
out.push(sphere_solid(plane.to_world(c), rad, [Vec3::X, Vec3::Y, Vec3::Z], &arcs)?);
continue;
}
// A full turn of a profile that comes back to the axis along a curve (a half slot about
// its centreline, a dome at the far end): truck's cone gives the faces
// swept from it the wrong region (their pole is at the end of their curve), and cuts with
// such a body failed (#1853). It is built as part turns are, a third of a turn at a time.
if angle.abs() >= std::f64::consts::TAU - 1e-9
&& r.holes.is_empty()
&& pole_at_end(&r.outer, axis_origin, d)
&& let Some(b) = revolve_on_axis(plane, &given.outer, axis_origin, d, angle)?
{
out.push(b);
continue;
}
// A full revolution of a profile with one edge on the axis: sweep the rest of the loop
// as a cone-like shell (no zero-area faces, which booleans can't handle).
if angle.abs() >= std::f64::consts::TAU - 1e-9
@@ -436,7 +448,7 @@ fn revolved(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec
let angle = if toward < 0.0 { -angle } else { angle };
// A profile with an edge on the axis: no zero-area face where that edge turns on itself.
if r.holes.is_empty()
&& let Some(b) = revolve_partial_on_axis(plane, &given.outer, axis_origin, d, angle)?
&& let Some(b) = revolve_on_axis(plane, &given.outer, axis_origin, d, angle)?
{
out.push(b);
continue;
@@ -1285,11 +1297,47 @@ fn half_disc(lp: &Loop2, axis_origin: Vec2, d: Vec2) -> Option<(Vec2, f64)> {
(same && (total - std::f64::consts::PI).abs() < 1e-6).then_some((c, r))
}
/// A partial revolve of a loop with one edge on the axis: the other edges sweep their faces,
/// the two caps (the profile at the start and at the end) share the axis edge. `None` when the
/// loop isn't like that.
fn revolve_partial_on_axis(plane: &Plane, numbered: &NumberedLoop, axis_origin: Vec2, d: Vec2, angle: f64) -> Result<Option<Body>> {
use mt::{BoundedCurve, Invertible, ParametricCurve, ParametricSurface3D, SearchNearestParameter, Transformed};
/// Whether the loop has one edge on the axis and comes back to it along a curve (the segment
/// before the axis edge is not a line). A line there (a drill point) sweeps a cone, which
/// truck's cone builds right.
fn pole_at_end(lp: &Loop2, axis_origin: Vec2, d: Vec2) -> bool {
let on_axis = |p: Vec2| d.cross(p - axis_origin).abs() < 1e-9;
let n = lp.segs.len();
let axis_segs: Vec<usize> =
(0..n).filter(|i| lp.segs.get(*i).is_some_and(|s| matches!(s, Seg2::Line { a, b } if on_axis(*a) && on_axis(*b)))).collect();
let [k] = axis_segs[..] else { return false };
lp.segs.get((k + n - 1) % n).is_some_and(|s| !on_axis(s.start()) && !matches!(s, Seg2::Line { .. }))
}
/// The edge from `a` to `b` along the profile segment `sg` (in `plane`), for a revolve's sweep.
/// One that ends on the axis (`ends_on_axis`) is built from there and turned round, so that the
/// surface swept from its curve has its pole at the curve's start. truck's revolved surface
/// reads a point on its axis at the start of its curve with the angle of the point before it,
/// but one at the end with an angle of 2π when it has no hint, past a part turn's span: the face
/// came out as another region (#1853).
fn swept_edge(plane: &Plane, sg: &Seg2, a: &mt::Vertex, b: &mt::Vertex, ends_on_axis: bool) -> mt::Edge {
if ends_on_axis {
return swept_edge(plane, &sg.reversed(), b, a, false).inverse();
}
match *sg {
Seg2::Line { .. } => builder::line(a, b),
Seg2::Arc { .. } => builder::circle_arc(a, b, p3(plane.to_world(sg.mid()))),
Seg2::Cubic { .. } | Seg2::Conic { .. } | Seg2::Bezier { .. } => {
crate::freeform::edge(plane, sg, a, b).unwrap_or_else(|| builder::line(a, b))
}
}
}
/// Where [`revolve_on_axis`]'s full turns start, from the profile: an angle no plane a part is
/// commonly cut along lies at (as full circles start, see `reseam`).
const FULL_TURN_START: f64 = 0.4142;
/// A revolve of a loop with one edge on the axis: the other edges sweep their faces, a third of a
/// turn at most each. A partial turn is closed by two caps (the profile at the start and at the
/// end), which share the axis edge; a full turn ends where it started. `None` when the loop
/// isn't like that.
fn revolve_on_axis(plane: &Plane, numbered: &NumberedLoop, axis_origin: Vec2, d: Vec2, angle: f64) -> Result<Option<Body>> {
use mt::{BoundedCurve, ParametricCurve, ParametricSurface3D, SearchNearestParameter, Transformed};
let lp = &numbered.lp;
let on_axis = |p: Vec2| d.cross(p - axis_origin).abs() < 1e-9;
let n = lp.segs.len();
@@ -1301,102 +1349,171 @@ fn revolve_partial_on_axis(plane: &Plane, numbered: &NumberedLoop, axis_origin:
if chain.is_empty() || chain.iter().skip(1).any(|s| on_axis(s.start())) || chain.iter().any(|s| on_axis(s.mid())) {
return Ok(None);
}
let full = angle.abs() >= std::f64::consts::TAU - 1e-9;
// Faces of a third of a turn at most, as truck sweeps a full turn: a boolean could not
// intersect a half turn's faces and the half circles bounding them (a half slot turned half a
// turn into a plate, #1853), and a sphere's face needs room for its poles (below).
let pieces = (angle.abs() / (std::f64::consts::TAU / 3.0) - 1e-9).ceil().max(1.0) as usize;
let step = angle / pieces as f64;
// Built with the axis through the world origin, then moved into place.
let shift = plane.to_world(axis_origin);
let local = Plane { origin: plane.origin - shift, ..*plane };
let plane = &local;
let axis = plane.dir_to_world(d);
let rot = mt::Matrix4::from_axis_angle(v3(axis), mt::Rad(angle));
let half = mt::Matrix4::from_axis_angle(v3(axis), mt::Rad(angle * 0.5));
let turned = |by: f64| mt::Matrix4::from_axis_angle(v3(axis), mt::Rad(by));
let turn = |m: &mt::Matrix4, p: Vec3| from_p3(mt::EuclideanSpace::from_vec((m * p3(p).to_homogeneous()).truncate()));
guard("revolve", || {
let fail = |m: &str| KernelError::Failed(format!("revolve: {m}"));
let pts: Vec<Vec3> = chain.iter().map(|s| plane.to_world(s.start())).chain(chain.last().map(|s| plane.to_world(s.end()))).collect();
let last = pts.len() - 1;
let pv: Vec<mt::Vertex> = pts.iter().map(|p| builder::vertex(p3(*p))).collect();
// The ends stay where they are (on the axis).
let qv: Vec<mt::Vertex> = pts
.iter()
.enumerate()
.map(|(i, p)| {
if i == 0 || i == last { pv.get(i).cloned().unwrap_or_else(|| builder::vertex(p3(*p))) } else { builder::vertex(p3(turn(&rot, *p))) }
})
.collect();
let mut e: Vec<mt::Edge> = Vec::new();
let mut er: Vec<mt::Edge> = Vec::new();
for (i, sg) in chain.iter().enumerate() {
let (Some(a), Some(b), Some(qa), Some(qb)) = (pv.get(i), pv.get(i + 1), qv.get(i), qv.get(i + 1)) else { return Err(fail("chain")) };
let edge = match *sg {
Seg2::Line { .. } => builder::line(a, b),
Seg2::Arc { .. } => builder::circle_arc(a, b, p3(plane.to_world(sg.mid()))),
Seg2::Cubic { .. } | Seg2::Conic { .. } | Seg2::Bezier { .. } => {
crate::freeform::edge(plane, sg, a, b).unwrap_or_else(|| builder::line(a, b))
}
};
let curve = edge.oriented_curve().transformed(rot);
er.push(mt::Edge::new(qa, qb, curve));
e.push(edge);
let (Some(a), Some(b)) = (pv.get(i), pv.get(i + 1)) else { return Err(fail("chain")) };
e.push(swept_edge(plane, sg, a, b, on_axis(sg.end())));
}
// Arcs swept by the vertices off the axis.
let mut arcs: Vec<Option<mt::Edge>> = vec![None; pts.len()];
for i in 1..last {
let (Some(a), Some(b), Some(p)) = (pv.get(i), qv.get(i), pts.get(i)) else { continue };
if let Some(slot) = arcs.get_mut(i) {
*slot = Some(builder::circle_arc(a, b, p3(turn(&half, *p))));
// The profile's vertices and edges where each piece starts and ends: turned by the step,
// the ends staying where they are (on the axis). A part turn starts on the profile. A full
// turn starts at an odd angle and ends where it started: the profile lies in the plane
// it was drawn on, often a face of the body cut, and a seam lying in that face as well
// left the cut nothing it could intersect there (#1853).
let start = if full { FULL_TURN_START } else { 0.0 };
let mut levels: Vec<(Vec<mt::Vertex>, Vec<mt::Edge>)> = Vec::new();
for j in 0..=pieces {
if j == 0 && !full {
levels.push((pv.clone(), e.clone()));
continue;
}
if let (true, Some(first)) = (j == pieces && full, levels.first()) {
levels.push(first.clone());
continue;
}
let rot = turned(start + step * j as f64);
let qv: Vec<mt::Vertex> = pts
.iter()
.enumerate()
.map(|(i, p)| {
if i == 0 || i == last {
pv.get(i).cloned().unwrap_or_else(|| builder::vertex(p3(*p)))
} else {
builder::vertex(p3(turn(&rot, *p)))
}
})
.collect();
let mut er: Vec<mt::Edge> = Vec::new();
for (i, edge) in e.iter().enumerate() {
let (Some(qa), Some(qb)) = (qv.get(i), qv.get(i + 1)) else { return Err(fail("chain")) };
let curve = edge.curve().transformed(rot);
er.push(if edge.orientation() { mt::Edge::new(qa, qb, curve) } else { mt::Edge::new(qb, qa, curve).inverse() });
}
levels.push((qv, er));
}
// Side faces: each edge's swept surface, oriented to agree with its boundary, from the
// profile segment the edge was built from.
let mut faces: Vec<(mt::Face, FaceProv)> = Vec::new();
for (i, sg) in chain.iter().enumerate() {
let (Some(ei), Some(eri)) = (e.get(i), er.get(i)) else { return Err(fail("edges")) };
let mut ws: Vec<mt::Edge> = vec![ei.clone()];
if let Some(Some(c)) = arcs.get(i + 1) {
ws.push(c.clone());
for (j, piece) in levels.windows(2).enumerate() {
let [(pv, e), (qv, er)] = piece else { return Err(fail("pieces")) };
let half = turned(start + step * (j as f64 + 0.5));
// Arcs swept by the vertices off the axis.
let mut arcs: Vec<Option<mt::Edge>> = vec![None; pts.len()];
for i in 1..last {
let (Some(a), Some(b), Some(p)) = (pv.get(i), qv.get(i), pts.get(i)) else { continue };
if let Some(slot) = arcs.get_mut(i) {
*slot = Some(builder::circle_arc(a, b, p3(turn(&half, *p))));
}
}
ws.push(eri.inverse());
if let Some(Some(c)) = arcs.get(i) {
ws.push(c.inverse());
}
let wire: mt::Wire = ws.into();
let radial = matches!(*sg, Seg2::Line { a, b } if (b - a).normalized().is_some_and(|u| u.dot(d).abs() < 1e-9));
let f = if radial {
builder::try_attach_plane(&[wire]).map_err(|er| fail(&er.to_string()))?
} else {
let template = builder::rsweep(ei, mt::Point3::new(0.0, 0.0, 0.0), v3(axis), mt::Rad(angle));
let mut surface = template.face_iter().next().map(|f| f.oriented_surface()).ok_or_else(|| fail("swept face"))?;
// The loop's turning (Newell) against the surface normal halfway round.
let mut nw = Vec3::ZERO;
let poly: Vec<Vec3> = wire
.edge_iter()
.flat_map(|ed| {
let c = ed.oriented_curve();
let (t0, t1) = c.range_tuple();
(0..8).map(move |j| from_p3(c.subs(t0 + (t1 - t0) * j as f64 / 8.0))).collect::<Vec<_>>()
})
.collect();
for (j, p) in poly.iter().enumerate() {
if let Some(q) = poly.get((j + 1) % poly.len()) {
nw += p.cross(*q);
for (i, sg) in chain.iter().enumerate() {
let (Some(ei), Some(eri)) = (e.get(i), er.get(i)) else { return Err(fail("edges")) };
let mut ws: Vec<mt::Edge> = vec![ei.clone()];
if let Some(Some(c)) = arcs.get(i + 1) {
ws.push(c.clone());
}
ws.push(eri.inverse());
if let Some(Some(c)) = arcs.get(i) {
ws.push(c.inverse());
}
let wire: mt::Wire = ws.into();
let radial = matches!(*sg, Seg2::Line { a, b } if (b - a).normalized().is_some_and(|u| u.dot(d).abs() < 1e-9));
let f = if radial {
builder::try_attach_plane(&[wire]).map_err(|er| fail(&er.to_string()))?
} else {
// The surface turns from the piece's start through |step|, so its angle runs
// from 0 across the face. Inverting it turns its axis round, which puts the
// face at 2π - |step| to 2π: its start then lies on the angle's seam, where
// points (and a pole without a hint) read as 0, the far side, and the face
// meshed and intersected as the wrong region (#1853). The face is turned
// round instead.
let (dir, by) = if step < 0.0 { (-axis, -step) } else { (axis, step) };
let surface = match *sg {
// An arc about a centre on the axis that reaches it sweeps part of a sphere
// with its pole there, and a boolean can't follow an intersection into a
// pole: a cut by the plane through the axis (the face the profile was drawn
// on) failed (#1853). The face lies on the sphere turned about the axis
// square to the revolve's and to the face's middle, as `sphere_solid`'s
// patches do: its poles are a quarter turn from that middle, past the
// face's third of a turn, and its seam runs round the back.
Seg2::Arc { center, radius, .. } if on_axis(center) && (on_axis(sg.start()) || on_axis(sg.end())) => {
let c = plane.to_world(center);
let out = turn(&half, plane.to_world(sg.mid())) - c;
let out = (out - axis * out.dot(axis)).normalized().ok_or_else(|| fail("sphere"))?;
let pole = axis.cross(out);
let meridian = builder::circle_arc(
&builder::vertex(p3(c + pole * radius)),
&builder::vertex(p3(c - pole * radius)),
p3(c - out * radius),
);
mt::Surface::RevolutedCurve(mt::Processor::new(mt::RevolutedCurve::by_revolution(
meridian.oriented_curve(),
p3(c),
v3(pole),
)))
}
_ => {
let template = builder::rsweep(ei, mt::Point3::new(0.0, 0.0, 0.0), v3(dir), mt::Rad(by));
template.face_iter().next().map(|f| f.surface()).ok_or_else(|| fail("swept face"))?
}
};
// The loop's turning (Newell) against the surface normal halfway round.
let mut nw = Vec3::ZERO;
let poly: Vec<Vec3> = wire
.edge_iter()
.flat_map(|ed| {
let c = ed.oriented_curve();
let (t0, t1) = c.range_tuple();
(0..8).map(move |j| from_p3(c.subs(t0 + (t1 - t0) * j as f64 / 8.0))).collect::<Vec<_>>()
})
.collect();
for (j, p) in poly.iter().enumerate() {
if let Some(q) = poly.get((j + 1) % poly.len()) {
nw += p.cross(*q);
}
}
}
let probe = turn(&half, plane.to_world(sg.mid()));
let sn = surface.search_nearest_parameter(p3(probe), None, 100).map(|(u, v)| surface.normal(u, v));
if sn.is_some_and(|m| nw.dot(Vec3::new(m.x, m.y, m.z)) < 0.0) {
surface = surface.inverse();
}
mt::Face::try_new(vec![wire], surface).map_err(|er| fail(&er.to_string()))?
};
faces.push((f, record::profile(numbered.numbers((k + 1 + i) % n))));
let probe = turn(&half, plane.to_world(sg.mid()));
let sn = surface.search_nearest_parameter(p3(probe), None, 100).map(|(u, v)| surface.normal(u, v));
if sn.is_some_and(|m| nw.dot(Vec3::new(m.x, m.y, m.z)) < 0.0) {
mt::Face::try_new(vec![wire.inverse()], surface).map_err(|er| fail(&er.to_string()))?.inverse()
} else {
mt::Face::try_new(vec![wire], surface).map_err(|er| fail(&er.to_string()))?
}
};
faces.push((f, record::profile(numbered.numbers((k + 1 + i) % n))));
}
}
let (faces, mut history): (Vec<mt::Face>, Vec<FaceProv>) = faces.into_iter().unzip();
let close = |solid: Solid, history: Vec<FaceProv>| {
let solid = if shift.len() > 0.0 { builder::translated(&solid, v3(shift)) } else { solid };
record::attach(Body::new(solid)?, &[], Some(history), Default::default()).map(Some)
};
if full {
return close(Solid::try_new(vec![faces.into()]).map_err(|er| fail(&er.to_string()))?, history);
}
// Caps: the profile at the start and at the end, sharing the axis edge.
let (Some(a0), Some(an)) = (pv.first(), pv.get(last)) else { return Err(fail("ends")) };
let ((Some(a0), Some(an)), Some((_, er))) = ((pv.first(), pv.get(last)), levels.last()) else { return Err(fail("ends")) };
let axis_edge = builder::line(an, a0);
let start: mt::Wire = e.iter().rev().map(|x| x.inverse()).chain(std::iter::once(axis_edge.inverse())).collect::<Vec<_>>().into();
let end: mt::Wire = er.iter().cloned().chain(std::iter::once(axis_edge.clone())).collect::<Vec<_>>().into();
let cs = builder::try_attach_plane(&[start]).map_err(|er| fail(&er.to_string()))?;
let ce = builder::try_attach_plane(&[end]).map_err(|er| fail(&er.to_string()))?;
let (faces, mut history): (Vec<mt::Face>, Vec<FaceProv>) = faces.into_iter().unzip();
history.extend([record::cap(false), record::cap(true)]);
let mut last_err = String::new();
for flip in 0..4 {
@@ -1404,10 +1521,7 @@ fn revolve_partial_on_axis(plane: &Plane, numbered: &NumberedLoop, axis_origin:
all.push(if flip & 1 == 0 { cs.clone() } else { cs.inverse() });
all.push(if flip & 2 == 0 { ce.clone() } else { ce.inverse() });
match Solid::try_new(vec![all.into()]) {
Ok(solid) => {
let solid = if shift.len() > 0.0 { builder::translated(&solid, v3(shift)) } else { solid };
return record::attach(Body::new(solid)?, &[], Some(history), Default::default()).map(Some);
}
Ok(solid) => return close(solid, history),
Err(er) => last_err = er.to_string(),
}
}