Surface Sweep: sheets carried from open curves and profiles along a path (#843)
Surface Sweep carries open curves and closed profile boundaries along a sketch path into surface bodies, with smooth walls along curved point paths. Adds the SurfaceSweep feature (format 18 note), the surface.sweep command, a dialog with Edit Feature support, and kernel and engine tests. Part of #757.
This commit is contained in:
1 parent
57d738eac4
commit
aba57f1ca2
16 files changed
+480
-27
No files matched your search
@@ -4,7 +4,7 @@
|
||||
//! between stations, mitred at the corners.
|
||||
|
||||
use mt::builder;
|
||||
use solidcraft_geom::{Plane, Region2, Vec2, Vec3};
|
||||
use solidcraft_geom::{Plane, Region2, Seg2, Vec2, Vec3};
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::body::{Body, p3};
|
||||
@@ -175,9 +175,9 @@ fn start_cos(plane: &Plane, tangents: &[Vec3]) -> f64 {
|
||||
tangents.first().map(|t| t.dot(plane.normal()).abs()).unwrap_or(1.0).max(1e-3)
|
||||
}
|
||||
|
||||
/// Ruled walls between stations at the path's points (mitred at corners), every loop of the
|
||||
/// (oriented) profile.
|
||||
fn ruled(plane: &Plane, profile: &NumberedRegion, pts: &[Vec3]) -> Result<Body> {
|
||||
/// Stations at the path's points (at most [`MAX_RULED_STATIONS`]), mitred at corners, with the
|
||||
/// points they stand at.
|
||||
fn ruled_stations(pts: &[Vec3]) -> Result<(Vec<Vec3>, Vec<Station>)> {
|
||||
let pts: Vec<Vec3> = if pts.len() > MAX_RULED_STATIONS { resample(pts, MAX_RULED_STATIONS) } else { pts.to_vec() };
|
||||
let n = pts.len();
|
||||
let seg_dir = |i: usize| -> Option<Vec3> { (*pts.get(i + 1)? - *pts.get(i)?).normalized() };
|
||||
@@ -202,6 +202,133 @@ fn ruled(plane: &Plane, profile: &NumberedRegion, pts: &[Vec3]) -> Result<Body>
|
||||
mitre.push(m);
|
||||
}
|
||||
let st = stations(&pts, &tangents, &mitre)?;
|
||||
Ok((pts, st))
|
||||
}
|
||||
|
||||
/// Walls of chains of sketch segments on `plane` (open, or closed) carried along a 3D polyline
|
||||
/// path that starts near them: ruled between stations at the path's points, mitred at corners,
|
||||
/// with no caps. Neighbouring walls share their edges.
|
||||
pub(crate) fn ruled_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Vec<mt::Face>> {
|
||||
let (pts, st) = ruled_stations(&clean(path)?)?;
|
||||
let edges: usize = chains.iter().map(Vec::len).sum();
|
||||
if st.len().saturating_mul(edges) > 400_000 {
|
||||
return Err(KernelError::Invalid("the sweep is too detailed (curves × path points)".into()));
|
||||
}
|
||||
let s0 = pts.first().copied().unwrap_or_default();
|
||||
let wires: Vec<Vec<mt::Wire>> = st
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let pl = station_plane(plane, s0, s);
|
||||
chains.iter().map(|c| crate::build::chain_wire(&pl, c)).collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
guard("path sweep", || {
|
||||
let mut faces = Vec::new();
|
||||
for w in wires.windows(2) {
|
||||
let (Some(w0), Some(w1)) = (w.first(), w.get(1)) else { continue };
|
||||
for (a, b) in w0.iter().zip(w1) {
|
||||
let sides: mt::Shell = builder::try_wire_homotopy(a, b).map_err(|e| KernelError::Failed(format!("path sweep: {e}")))?;
|
||||
faces.extend(sides.face_iter().cloned());
|
||||
}
|
||||
}
|
||||
Ok(faces)
|
||||
})
|
||||
}
|
||||
|
||||
/// Walls of chains of sketch segments on `plane` carried along a smooth 3D polyline path (no
|
||||
/// turn sharper than [`CORNER`]): one B-spline wall per segment through evenly spaced,
|
||||
/// rotation-minimising stations, neighbouring walls sharing their seam edges; no caps. `None`
|
||||
/// for a path with corners (see [`ruled_sheet`]).
|
||||
pub(crate) fn smooth_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Option<Vec<mt::Face>>> {
|
||||
let pts = clean(path)?;
|
||||
if pts.windows(3).any(|w| turn(w[1] - w[0], w[2] - w[1]) > CORNER) {
|
||||
return Ok(None);
|
||||
}
|
||||
let fail = |w: &str| KernelError::Failed(format!("path sweep: {w}"));
|
||||
if chains.iter().map(Vec::len).sum::<usize>() > 4096 {
|
||||
return Err(fail("too many curves for a smooth sweep"));
|
||||
}
|
||||
let n = (pts.len() - 1).clamp(4, MAX_SMOOTH_STATIONS);
|
||||
let sp = resample(&pts, n);
|
||||
let tangents = central_tangents(&sp)?;
|
||||
let st = stations(&sp, &tangents, &[])?;
|
||||
let s0 = st.first().map(|s| s.at).unwrap_or_default();
|
||||
let at = |i: usize, q: Vec2| st.get(i).map(|s| s.at + s.apply(plane.to_world(q) - s0)).unwrap_or_default();
|
||||
let n = st.len() - 1;
|
||||
let mut faces = Vec::new();
|
||||
for chain in chains {
|
||||
let closed = chain.first().zip(chain.last()).is_some_and(|(f, l)| l.end().dist(f.start()) <= 1e-6);
|
||||
// Each segment: its curve at the first and last station, the columns through its start
|
||||
// and end, and the wall surface.
|
||||
let mut data = Vec::with_capacity(chain.len());
|
||||
for s in chain {
|
||||
let mut station_curves = Vec::with_capacity(n + 1);
|
||||
for i in 0..=n {
|
||||
let q: Vec<Vec3> = (0..SEG_SAMPLES).map(|j| at(i, s.point_at(j as f64 / (SEG_SAMPLES - 1) as f64))).collect();
|
||||
station_curves.push(interpolate_cubic(&q).ok_or_else(|| fail("profile curve"))?);
|
||||
}
|
||||
let kv = station_curves.first().map(|c| c.knot_vec().clone()).ok_or_else(|| fail("no stations"))?;
|
||||
let mut columns = Vec::with_capacity(SEG_SAMPLES);
|
||||
for j in 0..SEG_SAMPLES {
|
||||
let q: Vec<Vec3> = station_curves
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let p = c.control_point(j);
|
||||
Vec3::new(p.x, p.y, p.z)
|
||||
})
|
||||
.collect();
|
||||
columns.push(interpolate_cubic(&q).ok_or_else(|| fail("path curve"))?);
|
||||
}
|
||||
let ku = columns.first().map(|c| c.knot_vec().clone()).ok_or_else(|| fail("no columns"))?;
|
||||
let net: Vec<Vec<mt::Point3>> = (0..=n).map(|m| columns.iter().map(|c| *c.control_point(m)).collect()).collect();
|
||||
let surf = mt::BSplineSurface::try_new((ku, kv), net).map_err(|e| fail(&e.to_string()))?;
|
||||
let (Some(c0), Some(c1), Some(first), Some(last)) =
|
||||
(station_curves.first().cloned(), station_curves.last().cloned(), columns.first().cloned(), columns.last().cloned())
|
||||
else {
|
||||
return Err(fail("stations"));
|
||||
};
|
||||
data.push((c0, c1, first, last, surf));
|
||||
}
|
||||
let walls: Vec<mt::Face> = guard("path sweep", || {
|
||||
let m = chain.len();
|
||||
// Vertices along the chain (an open chain has one more than it has segments).
|
||||
let corners: Vec<Vec2> = chain.iter().map(Seg2::start).chain(chain.last().map(Seg2::end).filter(|_| !closed)).collect();
|
||||
let start: Vec<mt::Vertex> = corners.iter().map(|q| builder::vertex(p3(at(0, *q)))).collect();
|
||||
let end: Vec<mt::Vertex> = corners.iter().map(|q| builder::vertex(p3(at(n, *q)))).collect();
|
||||
let next = |j: usize| if closed { (j + 1) % m } else { j + 1 };
|
||||
let mut seams = Vec::with_capacity(corners.len());
|
||||
for (k, (a, b)) in start.iter().zip(&end).enumerate() {
|
||||
// The column through each corner: the start column of the segment leaving it, or
|
||||
// the end column of the last segment.
|
||||
let col = match data.get(k) {
|
||||
Some((_, _, first, _, _)) => first,
|
||||
None => data.last().map(|d| &d.3).ok_or_else(|| fail("seam"))?,
|
||||
};
|
||||
seams.push(mt::Edge::try_new(a, b, mt::Curve::BSplineCurve(col.clone())).map_err(|e| fail(&e.to_string()))?);
|
||||
}
|
||||
let mut walls = Vec::with_capacity(m);
|
||||
for (j, (c0, c1, _, _, surf)) in data.iter().enumerate() {
|
||||
let (Some(a0), Some(b0), Some(a1), Some(b1), Some(sa), Some(sb)) =
|
||||
(start.get(j), start.get(next(j)), end.get(j), end.get(next(j)), seams.get(j), seams.get(next(j)))
|
||||
else {
|
||||
return Err(fail("edges"));
|
||||
};
|
||||
let e0 = mt::Edge::try_new(a0, b0, mt::Curve::BSplineCurve(c0.clone())).map_err(|e| fail(&e.to_string()))?;
|
||||
let e1 = mt::Edge::try_new(a1, b1, mt::Curve::BSplineCurve(c1.clone())).map_err(|e| fail(&e.to_string()))?;
|
||||
let w: mt::Wire = vec![sa.clone(), e1, sb.inverse(), e0.inverse()].into();
|
||||
walls.push(mt::Face::try_new(vec![w], mt::Surface::BSplineSurface(surf.clone())).map_err(|e| fail(&e.to_string()))?);
|
||||
}
|
||||
Ok(walls)
|
||||
})?;
|
||||
faces.extend(walls);
|
||||
}
|
||||
Ok(Some(faces))
|
||||
}
|
||||
|
||||
/// Ruled walls between stations at the path's points (mitred at corners), every loop of the
|
||||
/// (oriented) profile.
|
||||
fn ruled(plane: &Plane, profile: &NumberedRegion, pts: &[Vec3]) -> Result<Body> {
|
||||
let (pts, st) = ruled_stations(pts)?;
|
||||
let s0 = pts.first().copied().unwrap_or_default();
|
||||
let loops: Vec<&NumberedLoop> = profile.loops().collect();
|
||||
let mut wires: Vec<Vec<mt::Wire>> = Vec::with_capacity(st.len());
|
||||
|
||||
Reference in new issue
Block a user