use ogeom_algo::{
Built, History, edge_vertices, make_cone, make_cylinder, make_edge, make_edge_between,
make_face_with_pcurves, make_solid, make_torus, make_vertex, sew,
};
use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
use ogeom_geom::Curve3d as _;
use ogeom_geom::Transformable as _;
use ogeom_geom::{
CircleCurve, Curve, Line2d, LineCurve, PlaneSurface, SurfaceGeometry, TorusSurface,
};
use ogeom_math::{Circle, Direction, Frame, Plane, Point, Point2, Torus, Transform, Vector};
use ogeom_topo::{EdgeData, EdgeRepr, Filter, Model, Shape, ShapeType, VertexData, explore};
pub fn make_pipe(
model: &mut Model,
spine: &Shape,
radius: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
if !radius.is_finite() || radius <= tol.confusion() {
ogeom_bail!(Construction, "a pipe of radius {radius} holds nothing");
}
let (curve, range) = {
let Some(data) = model.node(spine).and_then(|n| n.data().as_edge()) else {
ogeom_bail!(Construction, "a pipe runs along an edge");
};
let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "the spine has no curve");
};
let Some(geometry) = model.geometry().curve(*curve) else {
ogeom_bail!(Dangling, "curve is not in this model");
};
(geometry.clone(), *range)
};
let mut built = match &curve {
Curve::Line(line) => {
let start = curve.point_at(range.0, tol)?;
let length = range.1 - range.0;
let frame = Frame::about(start, line.axis().direction);
make_cylinder(model, frame, radius, length, tol)?
}
Curve::Circle(c) => {
let circle = c.circle();
if radius >= circle.radius() - tol.confusion() {
ogeom_bail!(
Construction,
"a tube of radius {radius} swallows its spine of radius {}",
circle.radius()
);
}
let closed = curve
.point_at(range.0, tol)?
.distance(curve.point_at(range.1, tol)?)
<= tol.confusion();
if closed {
make_torus(model, circle.frame(), circle.radius(), radius, tol)?
} else {
pipe_segment(model, circle, range, radius, tol)?
}
}
_ => ogeom_bail!(
Construction,
"a pipe along a free-form spine needs the sweep-surface \
machinery — docs/PARITY.md, offset.sweeps"
),
};
built.history.generate(spine, built.shape.clone());
Ok(built)
}
fn pipe_segment(
model: &mut Model,
spine: Circle,
range: (f64, f64),
radius: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
let frame = spine.frame();
let (x, y, z) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
let major = spine.radius();
let radial = |u: f64| x * u.cos() + y * u.sin();
let tangent = |u: f64| x * -u.sin() + y * u.cos();
let tube_point = |u: f64, v: f64| {
frame.origin() + radial(u) * radius.mul_add(v.cos(), major) + z * (radius * v.sin())
};
let pi = core::f64::consts::PI;
let tau = core::f64::consts::TAU;
let torus: SurfaceGeometry = TorusSurface::new(Torus::new(frame, major, radius, tol)?).into();
let surface_id = model.geometry_mut().add_surface(torus);
let ends = [range.0, range.1];
let mut verts: Vec<Vec<Shape>> = Vec::new();
for &u in &ends {
verts.push(vec![
make_vertex(model, tube_point(u, 0.0)).shape,
make_vertex(model, tube_point(u, pi)).shape,
]);
}
let mut tube_arcs: Vec<Vec<Shape>> = Vec::new();
for (k, &u) in ends.iter().enumerate() {
let centre = frame.origin() + radial(u) * major;
let circle = Circle::new(
Frame::new(
centre,
Direction::new(-tangent(u), tol)?,
Direction::new(radial(u), tol)?,
tol,
)?,
radius,
tol,
)?;
let curve = Curve::Circle(CircleCurve::new(circle));
let arcs = vec![
make_edge_between(
model,
curve.clone(),
(0.0, pi),
&verts[k][0],
&verts[k][1],
tol,
)?
.shape,
make_edge_between(model, curve, (pi, tau), &verts[k][1], &verts[k][0], tol)?.shape,
];
let column = Line2d::over(
ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
0.0,
tau,
)?;
ogeom_algo::attach_pcurve(
model,
&arcs[0],
column.into(),
surface_id,
ogeom_topo::Location::identity(),
(0.0, pi),
)?;
ogeom_algo::attach_pcurve(
model,
&arcs[1],
column.into(),
surface_id,
ogeom_topo::Location::identity(),
(pi, tau),
)?;
tube_arcs.push(arcs);
}
let parallel = |model: &mut Model, v: f64, from: &Shape, to: &Shape| -> OgeomResult<Shape> {
let height = radius * v.sin();
let ring = radius.mul_add(v.cos(), major);
let circle = Circle::new(
Frame::new(frame.origin() + z * height, frame.z(), frame.x(), tol)?,
ring,
tol,
)?;
let curve = Curve::Circle(CircleCurve::new(circle));
Ok(make_edge_between(model, curve, range, from, to, tol)?.shape)
};
let row = |v: f64| -> OgeomResult<Line2d> {
Line2d::over(
ogeom_math::Axis2::new(Point2::new(0.0, v), ogeom_math::Direction2::X),
range.0 - 1.0,
range.1 + 1.0,
)
};
let inner = parallel(model, pi, &verts[0][1], &verts[1][1])?;
ogeom_algo::attach_pcurve(
model,
&inner,
row(pi)?.into(),
surface_id,
ogeom_topo::Location::identity(),
range,
)?;
let outer = parallel(model, 0.0, &verts[0][0], &verts[1][0])?;
ogeom_algo::attach_seam(
model,
&outer,
row(tau)?.into(),
row(0.0)?.into(),
surface_id,
ogeom_topo::Location::identity(),
range,
)?;
let lower = {
let wire = ogeom_algo::make_wire(
model,
&[
tube_arcs[0][0].clone(),
inner.clone(),
tube_arcs[1][0].reversed(),
outer.reversed(),
],
tol,
)?
.shape;
ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape
};
let upper = {
let wire = ogeom_algo::make_wire(
model,
&[
tube_arcs[0][1].clone(),
outer.clone(),
tube_arcs[1][1].reversed(),
inner.reversed(),
],
tol,
)?
.shape;
ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape
};
let mut caps: Vec<Shape> = Vec::new();
for (k, &u) in ends.iter().enumerate() {
let outward = if k == 0 { -tangent(u) } else { tangent(u) };
let centre = frame.origin() + radial(u) * major;
let plane = Plane::through(centre, Direction::new(outward, tol)?);
let reach = (major + radius) * 2.0;
let surface: SurfaceGeometry =
PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
caps.push(
make_face_with_pcurves(
model,
surface,
&[vec![tube_arcs[k][0].clone(), tube_arcs[k][1].clone()]],
tol,
)?
.shape,
);
}
let faces = [lower, upper, caps[0].clone(), caps[1].clone()];
let sewn = sew(model, &faces, tol)?;
if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
ogeom_bail!(Construction, "the pipe segment did not close");
}
make_solid(model, std::slice::from_ref(&sewn.shells[0]))
}
pub fn make_loft(
model: &mut Model,
bottom: &Shape,
top: &Shape,
tol: Tolerances,
) -> OgeomResult<Built> {
match (model.kind_of(bottom)?, model.kind_of(top)?) {
(ShapeType::Wire, ShapeType::Vertex) => {
return loft_to_point(model, bottom, top, tol);
}
(ShapeType::Vertex, ShapeType::Wire) => {
let mut built = loft_to_point(model, top, bottom, tol)?;
built.history.generate(bottom, built.shape.clone());
return Ok(built);
}
_ => {}
}
for wire in [bottom, top] {
if model.kind_of(wire)? != ShapeType::Wire {
ogeom_bail!(Construction, "a loft runs between wires");
}
if !ogeom_algo::is_wire_closed(model, wire, tol)? {
ogeom_bail!(Construction, "a loft section must be closed");
}
}
let circle_of = |model: &Model, wire: &Shape| -> OgeomResult<Option<Circle>> {
let edges = explore(model, wire, Filter::OfType(ShapeType::Edge))?;
if edges.len() != 1 {
return Ok(None);
}
let Some(data) = model.node(&edges[0]).and_then(|n| n.data().as_edge()) else {
return Ok(None);
};
let Some(EdgeRepr::Curve3d { curve, .. }) = data.curve3d() else {
return Ok(None);
};
match model.geometry().curve(*curve) {
Some(Curve::Circle(c)) => Ok(Some(c.circle())),
_ => Ok(None),
}
};
if let (Some(lower), Some(upper)) = (circle_of(model, bottom)?, circle_of(model, top)?) {
let axis = lower.frame().z().vector();
let rise = upper.centre() - lower.centre();
let height = rise.dot(axis);
if rise.cross(axis).magnitude() > tol.confusion() * 10.0 || height.abs() <= tol.confusion()
{
ogeom_bail!(
Construction,
"lofted circles must be coaxial on parallel planes; the \
oblique loft needs the sweep machinery — see the deferred \
table"
);
}
let frame = if height > 0.0 {
Frame::new(lower.centre(), lower.frame().z(), lower.frame().x(), tol)?
} else {
Frame::new(
lower.centre(),
lower.frame().z().reversed(),
lower.frame().x(),
tol,
)?
};
let mut built = if (lower.radius() - upper.radius()).abs() <= tol.confusion() {
make_cylinder(model, frame, lower.radius(), height.abs(), tol)?
} else {
make_cone(
model,
frame,
lower.radius(),
upper.radius(),
height.abs(),
tol,
)?
};
built.history.generate(bottom, built.shape.clone());
built.history.generate(top, built.shape.clone());
return Ok(built);
}
let corners_of = |model: &Model, wire: &Shape| -> OgeomResult<Vec<Point>> {
let mut out = Vec::new();
for edge in explore(model, wire, Filter::OfType(ShapeType::Edge))? {
let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
ogeom_bail!(Construction, "a section edge holds no data");
};
let Some(EdgeRepr::Curve3d { curve, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "a section edge has no curve");
};
let Some(Curve::Line(_)) = model.geometry().curve(*curve) else {
ogeom_bail!(
Construction,
"a mixed or curved section needs the skinning machinery — \
docs/PARITY.md, offset.sweeps"
);
};
let Some((sv, _)) = edge_vertices(model, &edge)? else {
ogeom_bail!(Construction, "a section edge has no vertices");
};
let Some(data) = model.node(&sv).and_then(|n| n.data().as_vertex()) else {
ogeom_bail!(Construction, "a section vertex holds no point");
};
out.push(sv.transform(model.datums())?.apply(data.point));
}
Ok(out)
};
let low = corners_of(model, bottom)?;
let high = corners_of(model, top)?;
if low.len() != high.len() {
ogeom_bail!(
Construction,
"lofted sections must have the same corner count, found {} and {}",
low.len(),
high.len()
);
}
let n = low.len();
let centroid = {
let mut c = Vector::new(0.0, 0.0, 0.0);
for p in low.iter().chain(high.iter()) {
c += p.to_vector();
}
#[allow(clippy::cast_precision_loss)]
let count = 2.0 * n as f64;
Point::from_vector(c / count)
};
let vl: Vec<Shape> = low.iter().map(|p| make_vertex(model, *p).shape).collect();
let vh: Vec<Shape> = high.iter().map(|p| make_vertex(model, *p).shape).collect();
let seg = |model: &mut Model, a: (&Shape, Point), b: (&Shape, Point)| -> OgeomResult<Shape> {
let line = LineCurve::segment(a.1, b.1, tol)?;
let curve = Curve::Line(line);
let domain = curve.domain();
Ok(make_edge_between(model, curve, domain, a.0, b.0, tol)?.shape)
};
let mut low_edges = Vec::with_capacity(n);
let mut high_edges = Vec::with_capacity(n);
let mut rails = Vec::with_capacity(n);
for i in 0..n {
let j = (i + 1) % n;
low_edges.push(seg(model, (&vl[i], low[i]), (&vl[j], low[j]))?);
high_edges.push(seg(model, (&vh[i], high[i]), (&vh[j], high[j]))?);
rails.push(seg(model, (&vl[i], low[i]), (&vh[i], high[i]))?);
}
let planar = |model: &mut Model, corners: &[Point], edges: Vec<Shape>| -> OgeomResult<Shape> {
let normal = {
let mut n = (corners[1] - corners[0]).cross(corners[2] - corners[0]);
let m = n.magnitude();
if m <= tol.confusion() {
ogeom_bail!(Construction, "a loft wall is degenerate");
}
n /= m;
if n.dot(corners[0] - centroid) < 0.0 {
-n
} else {
n
}
};
let skew = corners.iter().any(|p| {
Plane::through(
corners[0],
Direction::new(normal, tol).unwrap_or(Direction::Z),
)
.distance_to(*p)
> tol.confusion() * 10.0
});
if skew {
if corners.len() != 4 || edges.len() != 4 {
ogeom_bail!(Construction, "a skew ruled wall has four corners");
}
return bilinear_wall(model, corners, &edges, centroid, tol);
}
let plane = Plane::through(corners[0], Direction::new(normal, tol)?);
let mut reach = 1.0_f64;
for p in corners {
reach = reach.max(p.distance(corners[0]) * 2.0);
}
let surface: SurfaceGeometry =
PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
Ok(make_face_with_pcurves(model, surface, &[edges], tol)?.shape)
};
let mut faces: Vec<Shape> = Vec::with_capacity(n + 2);
for i in 0..n {
let j = (i + 1) % n;
faces.push(planar(
model,
&[low[i], low[j], high[j], high[i]],
vec![
low_edges[i].clone(),
rails[j].clone(),
high_edges[i].reversed(),
rails[i].reversed(),
],
)?);
}
faces.push(planar(model, &low, low_edges.clone())?);
faces.push(planar(model, &high, high_edges.clone())?);
let sewn = sew(model, &faces, tol)?;
if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
ogeom_bail!(Construction, "the loft did not close");
}
let mut built = make_solid(model, std::slice::from_ref(&sewn.shells[0]))?;
built.history.generate(bottom, built.shape.clone());
built.history.generate(top, built.shape.clone());
Ok(built)
}
fn bilinear_wall(
model: &mut Model,
corners: &[Point],
edges: &[Shape],
centroid: Point,
tol: Tolerances,
) -> OgeomResult<Shape> {
use ogeom_geom::Surface as _;
let (p00, p10, p11, p01) = (corners[0], corners[1], corners[2], corners[3]);
let grid = ogeom_math::ControlGrid::new(vec![p00, p01, p10, p11], 2, 2)?;
let line = ogeom_math::KnotVector::clamped_uniform(1, 2)?;
let patch = ogeom_geom::BSplineSurface::new(line.clone(), line, &grid, tol)?;
let outward = {
let (du, dv) = patch.d1_at(0.5, 0.5, tol)?;
let centre = patch.point_at(0.5, 0.5, tol)?;
du.cross(dv).dot(centre - centroid) >= 0.0
};
let surface_id = model
.geometry_mut()
.add_surface(SurfaceGeometry::BSpline(patch));
let sides: [(Point2, Point2); 4] = [
(Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)),
(Point2::new(1.0, 0.0), Point2::new(1.0, 1.0)),
(Point2::new(0.0, 1.0), Point2::new(1.0, 1.0)),
(Point2::new(0.0, 0.0), Point2::new(0.0, 1.0)),
];
for (edge, (from, to)) in edges.iter().zip(sides) {
let range = {
let Some(node) = model.node(edge) else {
ogeom_bail!(Dangling, "a loft edge is not in this model");
};
let Some(data) = node.data().as_edge() else {
ogeom_bail!(Construction, "a loft edge holds no edge data");
};
let Some(EdgeRepr::Curve3d { range, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "a loft edge has no curve");
};
*range
};
let knots = ogeom_math::KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
let pcurve = ogeom_geom::BSpline2d::new(knots, vec![from, to], tol)?;
ogeom_algo::attach_pcurve(
model,
edge,
pcurve.into(),
surface_id,
ogeom_topo::Location::identity(),
range,
)?;
}
let wire = ogeom_algo::make_wire(model, edges, tol)?.shape;
let face = ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
Ok(if outward { face } else { face.reversed() })
}
struct SkinnedWall {
face: Shape,
ring0: Shape,
ring1: Shape,
curve0: ogeom_geom::Curve,
curve1: ogeom_geom::Curve,
u_dom: (f64, f64),
}
fn skinned_wall(
model: &mut Model,
rows: &[Vec<Point>],
shared: (Option<&Shape>, Option<&Shape>),
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<SkinnedWall> {
use ogeom_geom::Surface as _;
let mut closed_rows: Vec<Vec<Point>> = Vec::with_capacity(rows.len());
for row in rows {
let mut r = row.clone();
r.push(row[0]);
closed_rows.push(r);
}
let fitted = ogeom_geom::fit::fit_surface_grid(&closed_rows, 3, tolerance, tol)?;
if !fitted.met {
ogeom_bail!(
NotDone,
"the skin reached {} against a target of {tolerance}",
fitted.error
);
}
let surface = fitted.curve;
let (u_knots, v_knots) = (surface.u_knots().clone(), surface.v_knots().clone());
let (k, l, net) = {
let grid = surface.grid();
let net: Vec<Point> = grid.points().iter().map(|w| (*w).point()).collect();
(grid.u_count(), grid.v_count(), net)
};
let point_at = |i: usize, j: usize| -> Point { net[i * l + j] };
let (u_dom, v_dom) = surface.domain();
let border_v = |j: usize| -> OgeomResult<ogeom_geom::Curve> {
let control: Vec<Point> = (0..k).map(|i| point_at(i, j)).collect();
Ok(ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(
u_knots.clone(),
control,
tol,
)?))
};
let seam_curve = {
let control: Vec<Point> = (0..l).map(|j| point_at(0, j)).collect();
ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(
v_knots.clone(),
control,
tol,
)?)
};
let surface_geo: SurfaceGeometry = surface.into();
let surface_id = model.geometry_mut().add_surface(surface_geo.clone());
let slack = fitted.error + tol.confusion();
let ring_of = |model: &mut Model,
j: usize,
given: Option<&Shape>|
-> OgeomResult<(Shape, ogeom_geom::Curve)> {
match given {
Some(edge) => {
adopt_border(model, edge, &surface_geo, slack, tol)?;
Ok((edge.clone(), spine_curve_of(model, edge)?.0))
}
None => {
let curve = border_v(j)?;
Ok((make_edge(model, curve.clone(), u_dom, tol)?.shape, curve))
}
}
};
let (ring0, curve0) = ring_of(model, 0, shared.0)?;
let (ring1, curve1) = ring_of(model, l - 1, shared.1)?;
let anchor0 = ogeom_algo::edge_vertices(model, &ring0)?
.map(|(a, _)| a)
.ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a skinned ring has no vertex"))?;
let anchor1 = ogeom_algo::edge_vertices(model, &ring1)?
.map(|(a, _)| a)
.ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a skinned ring has no vertex"))?;
let seam = make_edge_between(model, seam_curve, v_dom, &anchor0, &anchor1, tol)?.shape;
let row_line = |v: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(0.0, v), ogeom_math::Direction2::X),
u_dom.0 - 1.0,
u_dom.1 + 1.0,
)?
.into())
};
let column_line = |u: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
v_dom.0 - 1.0,
v_dom.1 + 1.0,
)?
.into())
};
ogeom_algo::attach_pcurve(
model,
&ring0,
row_line(v_dom.0)?,
surface_id,
ogeom_topo::Location::identity(),
u_dom,
)?;
ogeom_algo::attach_pcurve(
model,
&ring1,
row_line(v_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
u_dom,
)?;
ogeom_algo::attach_seam(
model,
&seam,
column_line(u_dom.0)?,
column_line(u_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
v_dom,
)?;
let wall = {
let wire = ogeom_algo::make_wire(
model,
&[
ring0.clone(),
seam.clone(),
ring1.reversed(),
seam.reversed(),
],
tol,
)?
.shape;
let face =
ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
let mid_u = f64::midpoint(u_dom.0, u_dom.1);
let mid_v = f64::midpoint(v_dom.0, v_dom.1);
let s_mid = surface_geo.point_at(mid_u, mid_v, tol)?;
let (du, dv) = surface_geo.d1_at(mid_u, mid_v, tol)?;
let centroid = {
let mut c = Vector::new(0.0, 0.0, 0.0);
let mut n = 0.0;
for row in rows {
for p in row {
c += p.to_vector();
n += 1.0;
}
}
Point::from_vector(c / n)
};
if du.cross(dv).dot(s_mid - centroid) >= 0.0 {
face
} else {
face.reversed()
}
};
Ok(SkinnedWall {
face: wall,
ring0,
ring1,
curve0,
curve1,
u_dom,
})
}
fn skinned_ring_strip(
model: &mut Model,
rows: &[Vec<Point>],
outward_hint: Point,
shared: [Option<&Shape>; 2],
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<(Shape, Shape, Shape)> {
use ogeom_geom::Surface as _;
let mut looped: Vec<Vec<Point>> = rows.to_vec();
looped.push(rows[0].clone());
let fitted = ogeom_geom::fit::fit_surface_grid_closed_v(&looped, 3, tolerance, tol)?;
if !fitted.met {
ogeom_bail!(
NotDone,
"the ring strip reached {} against a target of {tolerance}",
fitted.error
);
}
let surface = fitted.curve;
let (u_knots, v_knots) = (surface.u_knots().clone(), surface.v_knots().clone());
let (k, l, net) = {
let grid = surface.grid();
let net: Vec<Point> = grid.points().iter().map(|w| (*w).point()).collect();
(grid.u_count(), grid.v_count(), net)
};
let point_at = |i: usize, j: usize| -> Point { net[i * l + j] };
let (u_dom, v_dom) = surface.domain();
let rail_curve = |i: usize| -> OgeomResult<ogeom_geom::Curve> {
let control: Vec<Point> = (0..l).map(|j| point_at(i, j)).collect();
Ok(ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(
v_knots.clone(),
control,
tol,
)?))
};
let seam_curve = {
let control: Vec<Point> = (0..k).map(|i| point_at(i, 0)).collect();
ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(
u_knots.clone(),
control,
tol,
)?)
};
let surface_geo: SurfaceGeometry = surface.into();
let surface_id = model.geometry_mut().add_surface(surface_geo.clone());
let slack = fitted.error + tol.confusion();
let rail_of = |model: &mut Model, i: usize, given: Option<&Shape>| -> OgeomResult<Shape> {
let Some(edge) = given else {
let edge = make_edge(model, rail_curve(i)?, v_dom, tol)?.shape;
model.widen(&edge, ogeom_core::Tolerance::new(slack)?)?;
return Ok(edge);
};
let (curve, range) = spine_curve_of(model, edge)?;
let mut off: f64 = 0.0;
for step in 0..=32 {
#[allow(clippy::cast_precision_loss)]
let t = range.0 + (range.1 - range.0) * (step as f64) / 32.0;
let p = curve.point_at(t, tol)?;
off = off.max(ogeom_algo::project_on_surface(&surface_geo, p, 16, tol)?.distance);
}
model.widen(edge, ogeom_core::Tolerance::new(off + slack)?)?;
if let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? {
for v in [&a, &b] {
model.widen(v, ogeom_core::Tolerance::new(off + slack)?)?;
}
}
Ok(edge.clone())
};
let rail0 = rail_of(model, 0, shared[0])?;
let rail1 = rail_of(model, k - 1, shared[1])?;
let anchor0 = ogeom_algo::edge_vertices(model, &rail0)?
.map(|(a, _)| a)
.ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a strip rail has no vertex"))?;
let anchor1 = ogeom_algo::edge_vertices(model, &rail1)?
.map(|(a, _)| a)
.ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a strip rail has no vertex"))?;
let seam = make_edge_between(model, seam_curve, u_dom, &anchor0, &anchor1, tol)?.shape;
let row_line = |v: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(0.0, v), ogeom_math::Direction2::X),
u_dom.0 - 1.0,
u_dom.1 + 1.0,
)?
.into())
};
let column_line = |u: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
v_dom.0 - 1.0,
v_dom.1 + 1.0,
)?
.into())
};
ogeom_algo::attach_pcurve(
model,
&rail0,
column_line(u_dom.0)?,
surface_id,
ogeom_topo::Location::identity(),
v_dom,
)?;
ogeom_algo::attach_pcurve(
model,
&rail1,
column_line(u_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
v_dom,
)?;
ogeom_algo::attach_seam(
model,
&seam,
row_line(v_dom.0)?,
row_line(v_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
u_dom,
)?;
let wire = ogeom_algo::make_wire(
model,
&[
rail0.clone(),
seam.clone(),
rail1.reversed(),
seam.reversed(),
],
tol,
)?
.shape;
let face = ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
let mid_u = f64::midpoint(u_dom.0, u_dom.1);
let mid_v = f64::midpoint(v_dom.0, v_dom.1);
let s_mid = surface_geo.point_at(mid_u, mid_v, tol)?;
let (du, dv) = surface_geo.d1_at(mid_u, mid_v, tol)?;
let face = if du.cross(dv).dot(s_mid - outward_hint) >= 0.0 {
face
} else {
face.reversed()
};
Ok((face, rail0, rail1))
}
fn adopt_border(
model: &mut Model,
edge: &Shape,
surface: &SurfaceGeometry,
slack: f64,
tol: Tolerances,
) -> OgeomResult<()> {
let (curve, range) = spine_curve_of(model, edge)?;
let mut off: f64 = 0.0;
for step in 0..=32 {
#[allow(clippy::cast_precision_loss)]
let t = range.0 + (range.1 - range.0) * (step as f64) / 32.0;
let p = curve.point_at(t, tol)?;
off = off.max(ogeom_algo::project_on_surface(surface, p, 16, tol)?.distance);
}
model.widen(edge, ogeom_core::Tolerance::new(off + slack)?)?;
if let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? {
for v in [&a, &b] {
model.widen(v, ogeom_core::Tolerance::new(off + slack)?)?;
}
}
Ok(())
}
fn skinned_solid(
model: &mut Model,
rows: &[Vec<Point>],
cap_outward: (Vector, Vector),
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
let wall = skinned_wall(model, rows, (None, None), tolerance, tol)?;
let u_dom = wall.u_dom;
let cap = |model: &mut Model,
ring: &Shape,
curve: ogeom_geom::Curve,
outward: Vector|
-> OgeomResult<Shape> {
let at = curve.point_at(u_dom.0, tol)?;
let plane = Plane::through(at, Direction::new(outward, tol)?);
let mut reach = 1.0_f64;
for t in 0..8 {
let p = curve.point_at(u_dom.0 + (u_dom.1 - u_dom.0) * f64::from(t) / 8.0, tol)?;
reach = reach.max(p.distance(at) * 2.0);
}
let cap_surface: SurfaceGeometry =
PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
let wire = ogeom_algo::make_wire(model, std::slice::from_ref(ring), tol)?.shape;
let face =
ogeom_algo::make_face(model, cap_surface.clone(), std::slice::from_ref(&wire), tol)?
.shape;
let id = {
let Some(node) = model.node(&face) else {
ogeom_bail!(Dangling, "the cap just built is not in this model");
};
let ogeom_topo::NodeData::Face(data) = node.data() else {
ogeom_bail!(Construction, "the cap holds no face data");
};
data.surface
};
let Some(pcurve) = ogeom_intersect::exact_pcurve_of(&curve, &cap_surface, tol) else {
ogeom_bail!(Construction, "a cap edge has no closed-form pcurve");
};
ogeom_algo::attach_pcurve(
model,
ring,
pcurve,
id,
ogeom_topo::Location::identity(),
u_dom,
)?;
Ok(face)
};
let cap0 = cap(model, &wall.ring0, wall.curve0.clone(), cap_outward.0)?;
let cap1 = cap(model, &wall.ring1, wall.curve1.clone(), cap_outward.1)?;
let faces = [wall.face, cap0, cap1];
let sewn = sew(model, &faces, tol)?;
if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
ogeom_bail!(Construction, "the skinned solid did not close");
}
make_solid(model, std::slice::from_ref(&sewn.shells[0]))
}
fn skinned_solid_to_apex(
model: &mut Model,
rows: &[Vec<Point>],
cap_outward: Vector,
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
use ogeom_geom::Surface as _;
let mut closed_rows: Vec<Vec<Point>> = Vec::with_capacity(rows.len());
for row in rows {
let mut r = row.clone();
r.push(row[0]);
closed_rows.push(r);
}
let fitted = ogeom_geom::fit::fit_surface_grid(&closed_rows, 3, tolerance, tol)?;
if !fitted.met {
ogeom_bail!(
NotDone,
"the skin reached {} against a target of {tolerance}",
fitted.error
);
}
let surface = fitted.curve;
let (u_knots, v_knots) = (surface.u_knots().clone(), surface.v_knots().clone());
let (k, l, net) = {
let grid = surface.grid();
let net: Vec<Point> = grid.points().iter().map(|w| (*w).point()).collect();
(grid.u_count(), grid.v_count(), net)
};
let point_at = |i: usize, j: usize| -> Point { net[i * l + j] };
let (u_dom, v_dom) = surface.domain();
let apex = rows[rows.len() - 1][0];
let ring_curve = {
let control: Vec<Point> = (0..k).map(|i| point_at(i, 0)).collect();
ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(
u_knots.clone(),
control,
tol,
)?)
};
let seam_curve = {
let control: Vec<Point> = (0..l).map(|j| point_at(0, j)).collect();
ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(
v_knots.clone(),
control,
tol,
)?)
};
let surface_geo: SurfaceGeometry = surface.into();
let surface_id = model.geometry_mut().add_surface(surface_geo.clone());
let ring0 = make_edge(model, ring_curve.clone(), u_dom, tol)?.shape;
let anchor0 = ogeom_algo::edge_vertices(model, &ring0)?
.map(|(a, _)| a)
.ok_or_else(|| ogeom_core::ogeom_err!(Construction, "a skinned ring has no vertex"))?;
let apex_vertex = model.add_vertex(VertexData::new(apex));
let apex_edge = {
let mut data = EdgeData::new();
data.degenerate = true;
model.add_edge(data, &[apex_vertex.clone(), apex_vertex.clone()])?
};
let seam = make_edge_between(model, seam_curve, v_dom, &anchor0, &apex_vertex, tol)?.shape;
let row_line = |v: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(0.0, v), ogeom_math::Direction2::X),
u_dom.0 - 1.0,
u_dom.1 + 1.0,
)?
.into())
};
let column_line = |u: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
v_dom.0 - 1.0,
v_dom.1 + 1.0,
)?
.into())
};
ogeom_algo::attach_pcurve(
model,
&ring0,
row_line(v_dom.0)?,
surface_id,
ogeom_topo::Location::identity(),
u_dom,
)?;
ogeom_algo::attach_pcurve(
model,
&apex_edge,
row_line(v_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
u_dom,
)?;
ogeom_algo::attach_seam(
model,
&seam,
column_line(u_dom.0)?,
column_line(u_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
v_dom,
)?;
let wall = {
let wire = ogeom_algo::make_wire(
model,
&[
ring0.clone(),
seam.clone(),
apex_edge.reversed(),
seam.reversed(),
],
tol,
)?
.shape;
let face =
ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
let mid_u = f64::midpoint(u_dom.0, u_dom.1);
let mid_v = f64::midpoint(v_dom.0, v_dom.1);
let s_mid = surface_geo.point_at(mid_u, mid_v, tol)?;
let (du, dv) = surface_geo.d1_at(mid_u, mid_v, tol)?;
let centroid = {
let mut c = Vector::new(0.0, 0.0, 0.0);
let mut n = 0.0;
for row in rows {
for p in row {
c += p.to_vector();
n += 1.0;
}
}
Point::from_vector(c / n)
};
if du.cross(dv).dot(s_mid - centroid) >= 0.0 {
face
} else {
face.reversed()
}
};
let cap = {
let at = ring_curve.point_at(u_dom.0, tol)?;
let plane = Plane::through(at, Direction::new(cap_outward, tol)?);
let mut reach = 1.0_f64;
for t in 0..8 {
let p = ring_curve.point_at(u_dom.0 + (u_dom.1 - u_dom.0) * f64::from(t) / 8.0, tol)?;
reach = reach.max(p.distance(at) * 2.0);
}
let cap_surface: SurfaceGeometry =
PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
let wire = ogeom_algo::make_wire(model, std::slice::from_ref(&ring0), tol)?.shape;
let face =
ogeom_algo::make_face(model, cap_surface.clone(), std::slice::from_ref(&wire), tol)?
.shape;
let id = {
let Some(node) = model.node(&face) else {
ogeom_bail!(Dangling, "the cap just built is not in this model");
};
let ogeom_topo::NodeData::Face(data) = node.data() else {
ogeom_bail!(Construction, "the cap holds no face data");
};
data.surface
};
let Some(pcurve) = ogeom_intersect::exact_pcurve_of(&ring_curve, &cap_surface, tol) else {
ogeom_bail!(Construction, "a cap edge has no closed-form pcurve");
};
ogeom_algo::attach_pcurve(
model,
&ring0,
pcurve,
id,
ogeom_topo::Location::identity(),
u_dom,
)?;
face
};
let faces = [wall, cap];
let sewn = sew(model, &faces, tol)?;
if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
ogeom_bail!(Construction, "the skinned apex solid did not close");
}
make_solid(model, std::slice::from_ref(&sewn.shells[0]))
}
fn closed_skinned_solid(
model: &mut Model,
rows: &[Vec<Point>],
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
let shell = closed_skinned_shell(model, rows, tolerance, tol)?;
make_solid(model, std::slice::from_ref(&shell))
}
fn closed_skinned_shell(
model: &mut Model,
rows: &[Vec<Point>],
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Shape> {
use ogeom_geom::Surface as _;
let mut closed_rows: Vec<Vec<Point>> = Vec::with_capacity(rows.len() + 1);
for row in rows {
let mut r = row.clone();
r.push(row[0]);
closed_rows.push(r);
}
closed_rows.push(closed_rows[0].clone());
let fitted = ogeom_geom::fit::fit_surface_grid_closed_v(&closed_rows, 3, tolerance, tol)?;
if !fitted.met {
ogeom_bail!(
NotDone,
"the closed skin reached {} against a target of {tolerance}",
fitted.error
);
}
let surface = fitted.curve;
let (u_knots, v_knots) = (surface.u_knots().clone(), surface.v_knots().clone());
let (k, l, net) = {
let grid = surface.grid();
let net: Vec<Point> = grid.points().iter().map(|w| (*w).point()).collect();
(grid.u_count(), grid.v_count(), net)
};
let point_at = |i: usize, j: usize| -> Point { net[i * l + j] };
let (u_dom, v_dom) = surface.domain();
let along_u = {
let control: Vec<Point> = (0..k).map(|i| point_at(i, 0)).collect();
ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(u_knots, control, tol)?)
};
let along_v = {
let control: Vec<Point> = (0..l).map(|j| point_at(0, j)).collect();
ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(v_knots, control, tol)?)
};
let surface_geo: SurfaceGeometry = surface.into();
let surface_id = model.geometry_mut().add_surface(surface_geo.clone());
let u_edge = make_edge(model, along_u, u_dom, tol)?.shape;
let Some((corner, _)) = ogeom_algo::edge_vertices(model, &u_edge)? else {
ogeom_bail!(Construction, "the closed skin's seam has no vertex");
};
let v_edge = make_edge_between(model, along_v, v_dom, &corner, &corner, tol)?.shape;
let row_line = |v: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(0.0, v), ogeom_math::Direction2::X),
u_dom.0 - 1.0,
u_dom.1 + 1.0,
)?
.into())
};
let column_line = |u: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
v_dom.0 - 1.0,
v_dom.1 + 1.0,
)?
.into())
};
ogeom_algo::attach_seam(
model,
&u_edge,
row_line(v_dom.0)?,
row_line(v_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
u_dom,
)?;
ogeom_algo::attach_seam(
model,
&v_edge,
column_line(u_dom.1)?,
column_line(u_dom.0)?,
surface_id,
ogeom_topo::Location::identity(),
v_dom,
)?;
let wire = ogeom_algo::make_wire(
model,
&[
u_edge.clone(),
v_edge.clone(),
u_edge.reversed(),
v_edge.reversed(),
],
tol,
)?
.shape;
let face = ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
let centroid = {
let mut c = Vector::new(0.0, 0.0, 0.0);
let mut n = 0.0;
for row in rows {
for p in row {
c += p.to_vector();
n += 1.0;
}
}
Point::from_vector(c / n)
};
let mid_u = f64::midpoint(u_dom.0, u_dom.1);
let mid_v = f64::midpoint(v_dom.0, v_dom.1);
let s_mid = surface_geo.point_at(mid_u, mid_v, tol)?;
let (du, dv) = surface_geo.d1_at(mid_u, mid_v, tol)?;
let face = if du.cross(dv).dot(s_mid - centroid) >= 0.0 {
face
} else {
face.reversed()
};
let sewn = sew(model, std::slice::from_ref(&face), tol)?;
if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
ogeom_bail!(Construction, "the closed skin did not close");
}
Ok(sewn.shells[0].clone())
}
fn loft_to_point(
model: &mut Model,
section: &Shape,
apex: &Shape,
tol: Tolerances,
) -> OgeomResult<Built> {
if !ogeom_algo::is_wire_closed(model, section, tol)? {
ogeom_bail!(Construction, "a loft section must be closed");
}
let apex_point = {
let Some(data) = model.node(apex).and_then(|n| n.data().as_vertex()) else {
ogeom_bail!(Construction, "the apex vertex holds no data");
};
data.point
};
let edges = explore(model, section, Filter::OfType(ShapeType::Edge))?;
if edges.len() == 1
&& let Some(data) = model.node(&edges[0]).and_then(|n| n.data().as_edge())
&& let Some(EdgeRepr::Curve3d { curve, .. }) = data.curve3d()
&& let Some(Curve::Circle(c)) = model.geometry().curve(*curve)
{
let circle = c.circle();
let axis = circle.frame().z().vector();
let rise = apex_point - circle.centre();
let height = rise.dot(axis);
if rise.cross(axis).magnitude() > tol.confusion() * 10.0 {
ogeom_bail!(
Construction,
"a circle lofts to a point on its own axis; the oblique cone \
needs the skinned machinery — docs/PARITY.md, offset.loft"
);
}
if height.abs() <= tol.confusion() {
ogeom_bail!(Construction, "the apex sits in the section's own plane");
}
let base = if height > 0.0 {
circle.frame()
} else {
Frame::new(
circle.centre(),
-circle.frame().z(),
circle.frame().x(),
tol,
)?
};
let mut built =
ogeom_algo::make_cone(model, base, circle.radius(), 0.0, height.abs(), tol)?;
built.history.generate(section, built.shape.clone());
built.history.generate(apex, built.shape.clone());
return Ok(built);
}
let mut corners: Vec<Point> = Vec::new();
for edge in model.ordered_children_of(section)? {
let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
ogeom_bail!(Construction, "a section edge holds no data");
};
let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "a section edge has no curve");
};
let Some(Curve::Line(line)) = model.geometry().curve(*curve).cloned() else {
ogeom_bail!(
Construction,
"a mixed or curved section lofts to a point through the \
skinned machinery — docs/PARITY.md, offset.loft"
);
};
let t = if edge.orientation() == ogeom_topo::Orientation::Reversed {
range.1
} else {
range.0
};
corners.push(ogeom_geom::Curve::Line(line).point_at(t, tol)?);
}
if corners.len() < 3 {
ogeom_bail!(Construction, "a pyramid needs at least three base corners");
}
let apex_vertex = ogeom_algo::make_vertex(model, apex_point).shape;
let base_vertices: Vec<Shape> = corners
.iter()
.map(|p| ogeom_algo::make_vertex(model, *p).shape)
.collect();
let segment =
|model: &mut Model, from: (&Shape, Point), to: (&Shape, Point)| -> OgeomResult<Shape> {
let line = ogeom_geom::LineCurve::segment(from.1, to.1, tol)?;
let curve: Curve = line.into();
let domain = curve.domain();
Ok(make_edge_between(model, curve, domain, from.0, to.0, tol)?.shape)
};
let count = corners.len();
let mut base_edges = Vec::with_capacity(count);
let mut rails = Vec::with_capacity(count);
for i in 0..count {
let next = (i + 1) % count;
base_edges.push(segment(
model,
(&base_vertices[i], corners[i]),
(&base_vertices[next], corners[next]),
)?);
rails.push(segment(
model,
(&base_vertices[i], corners[i]),
(&apex_vertex, apex_point),
)?);
}
let centroid = {
let mut c = Vector::new(0.0, 0.0, 0.0);
for p in &corners {
c += p.to_vector();
}
#[allow(clippy::cast_precision_loss)]
Point::from_vector(c / count as f64 / 4.0 * 3.0 + apex_point.to_vector() / 4.0)
};
let planar = |model: &mut Model, pts: [Point; 3], walk: Vec<Shape>| -> OgeomResult<Shape> {
let n = (pts[1] - pts[0]).cross(pts[2] - pts[0]);
let m = n.magnitude();
if m <= tol.confusion() {
ogeom_bail!(Construction, "a wall of the pyramid is degenerate");
}
let mut outward = n / m;
if outward.dot(pts[0] - centroid) < 0.0 {
outward = -outward;
}
let plane = ogeom_math::Plane::through(pts[0], Direction::new(outward, tol)?);
let mut reach = 1.0_f64;
for p in pts {
reach = reach.max(p.distance(pts[0]) * 2.0);
}
let surface: SurfaceGeometry =
PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
let id = model.geometry_mut().add_surface(surface.clone());
let signed = {
let (du, dv) = {
use ogeom_geom::Surface as _;
surface.d1_at(0.0, 0.0, tol)?
};
du.cross(dv).dot(outward) >= 0.0
};
let mut wired = Vec::with_capacity(walk.len());
for used in &walk {
let (curve, range) = spine_curve_of(model, used)?;
let Some(pcurve) = ogeom_intersect::exact_pcurve_of(&curve, &surface, tol) else {
ogeom_bail!(Construction, "a wall edge has no closed-form pcurve");
};
ogeom_algo::attach_pcurve(
model,
used,
pcurve,
id,
ogeom_topo::Location::identity(),
range,
)?;
wired.push(used.clone());
}
let wire = ogeom_algo::make_wire(model, &wired, tol)?.shape;
let face = ogeom_algo::make_face_on(model, id, std::slice::from_ref(&wire), tol)?.shape;
Ok(if signed { face } else { face.reversed() })
};
let mut faces = Vec::with_capacity(count + 1);
for i in 0..count {
let next = (i + 1) % count;
faces.push(planar(
model,
[corners[i], corners[next], apex_point],
vec![
base_edges[i].clone(),
rails[next].clone(),
rails[i].reversed(),
],
)?);
}
let base_walk: Vec<Shape> = (0..count).rev().map(|i| base_edges[i].reversed()).collect();
faces.push({
let n = (corners[1] - corners[0]).cross(corners[2] - corners[0]);
let mut outward = n / n.magnitude();
if outward.dot(corners[0] - centroid) < 0.0 {
outward = -outward;
}
let plane = ogeom_math::Plane::through(corners[0], Direction::new(outward, tol)?);
let mut reach = 1.0_f64;
for p in &corners {
reach = reach.max(p.distance(corners[0]) * 2.0);
}
let surface: SurfaceGeometry =
PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
let id = model.geometry_mut().add_surface(surface.clone());
for used in &base_walk {
let (curve, range) = spine_curve_of(model, used)?;
let Some(pcurve) = ogeom_intersect::exact_pcurve_of(&curve, &surface, tol) else {
ogeom_bail!(Construction, "a base edge has no closed-form pcurve");
};
ogeom_algo::attach_pcurve(
model,
used,
pcurve,
id,
ogeom_topo::Location::identity(),
range,
)?;
}
let wire = ogeom_algo::make_wire(model, &base_walk, tol)?.shape;
let face = ogeom_algo::make_face_on(model, id, std::slice::from_ref(&wire), tol)?.shape;
let signed = {
use ogeom_geom::Surface as _;
let (du, dv) = surface.d1_at(0.0, 0.0, tol)?;
du.cross(dv).dot(outward) >= 0.0
};
if signed { face } else { face.reversed() }
});
let sewn = sew(model, &faces, tol)?;
if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
ogeom_bail!(Construction, "the pyramid did not close");
}
let mut built = make_solid(model, std::slice::from_ref(&sewn.shells[0]))?;
built.history.generate(section, built.shape.clone());
built.history.generate(apex, built.shape.clone());
Ok(built)
}
pub fn make_loft_skinned_aligned(
model: &mut Model,
sections: &[Shape],
hints: &[Point],
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
if hints.len() != sections.len() {
ogeom_bail!(
Construction,
"{} hints against {} sections; each section names its own start",
hints.len(),
sections.len()
);
}
if sections.len() < 2 {
ogeom_bail!(Construction, "a loft needs at least two sections");
}
const AROUND: usize = 48;
let mut rows: Vec<Vec<Point>> = Vec::with_capacity(sections.len());
let mut planes: Vec<Plane> = Vec::with_capacity(sections.len());
for (wire, hint) in sections.iter().zip(hints) {
if model.kind_of(wire)? != ShapeType::Wire {
ogeom_bail!(Construction, "a loft section is a closed wire");
}
if !ogeom_algo::is_wire_closed(model, wire, tol)? {
ogeom_bail!(Construction, "a loft section must be closed");
}
let Some(plane) = ogeom_algo::find_plane(model, wire, tol)? else {
ogeom_bail!(Construction, "a loft section must be planar");
};
planes.push(plane);
rows.push(sample_wire_from(model, wire, AROUND, Some(*hint), tol)?);
}
let outward0 = {
let towards = rows[1][0] - rows[0][0];
let n = planes[0].normal().vector();
if n.dot(towards) > 0.0 { -n } else { n }
};
let outward1 = {
let towards = rows[rows.len() - 2][0] - rows[rows.len() - 1][0];
let n = planes[planes.len() - 1].normal().vector();
if n.dot(towards) > 0.0 { -n } else { n }
};
let mut built = skinned_solid(model, &rows, (outward0, outward1), tolerance, tol)?;
for section in sections {
built.history.generate(section, built.shape.clone());
}
Ok(built)
}
pub fn make_loft_skinned_closed(
model: &mut Model,
sections: &[Shape],
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
if sections.len() < 3 {
ogeom_bail!(Construction, "a closed loft needs at least three sections");
}
const AROUND: usize = 48;
let mut rows: Vec<Vec<Point>> = Vec::with_capacity(sections.len());
for wire in sections {
if model.kind_of(wire)? != ShapeType::Wire {
ogeom_bail!(Construction, "a loft section is a closed wire");
}
if !ogeom_algo::is_wire_closed(model, wire, tol)? {
ogeom_bail!(Construction, "a loft section must be closed");
}
if ogeom_algo::find_plane(model, wire, tol)?.is_none() {
ogeom_bail!(Construction, "a loft section must be planar");
}
rows.push(sample_wire(model, wire, AROUND, tol)?);
}
let mut built = closed_skinned_solid(model, &rows, tolerance, tol)?;
for section in sections {
built.history.generate(section, built.shape.clone());
}
Ok(built)
}
struct SkinnedStrip {
face: Shape,
bottom: Shape,
top: Shape,
rail0: Shape,
rail1: Shape,
}
#[allow(clippy::too_many_arguments, reason = "one strip, spelled out")]
fn skinned_strip(
model: &mut Model,
rows: &[Vec<Point>],
corners: (&Shape, &Shape, &Shape, &Shape),
shared: [Option<&Shape>; 4],
outward_hint: Point,
hole: bool,
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<SkinnedStrip> {
use ogeom_geom::Surface as _;
let fitted = ogeom_geom::fit::fit_surface_grid(rows, 3, tolerance, tol)?;
if !fitted.met {
ogeom_bail!(
NotDone,
"the strip reached {} against a target of {tolerance}",
fitted.error
);
}
let error = fitted.error.max(tol.confusion());
let surface = fitted.curve;
let (u_knots, v_knots) = (surface.u_knots().clone(), surface.v_knots().clone());
let (k, l, net) = {
let grid = surface.grid();
let net: Vec<Point> = grid.points().iter().map(|w| (*w).point()).collect();
(grid.u_count(), grid.v_count(), net)
};
let point_at = |i: usize, j: usize| -> Point { net[i * l + j] };
let (u_dom, v_dom) = surface.domain();
let u_curve = |j: usize| -> OgeomResult<ogeom_geom::Curve> {
let control: Vec<Point> = (0..k).map(|i| point_at(i, j)).collect();
Ok(ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(
u_knots.clone(),
control,
tol,
)?))
};
let v_curve = |i: usize| -> OgeomResult<ogeom_geom::Curve> {
let control: Vec<Point> = (0..l).map(|j| point_at(i, j)).collect();
Ok(ogeom_geom::Curve::BSpline(ogeom_geom::BSplineCurve::new(
v_knots.clone(),
control,
tol,
)?))
};
let surface_geo: SurfaceGeometry = surface.into();
let surface_id = model.geometry_mut().add_surface(surface_geo.clone());
let (c00, c10, c01, c11) = corners;
let border = |model: &mut Model,
given: Option<&Shape>,
curve: ogeom_geom::Curve,
range: (f64, f64),
from: &Shape,
to: &Shape|
-> OgeomResult<Shape> {
match given {
Some(edge) => {
adopt_border(model, edge, &surface_geo, error + tol.confusion(), tol)?;
Ok(edge.clone())
}
None => Ok(make_edge_between(model, curve, range, from, to, tol)?.shape),
}
};
let bottom = border(model, shared[0], u_curve(0)?, u_dom, c00, c10)?;
let top = border(model, shared[1], u_curve(l - 1)?, u_dom, c01, c11)?;
let rail0 = border(model, shared[2], v_curve(0)?, v_dom, c00, c01)?;
let rail1 = border(model, shared[3], v_curve(k - 1)?, v_dom, c10, c11)?;
let row_line = |v: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(0.0, v), ogeom_math::Direction2::X),
u_dom.0 - 1.0,
u_dom.1 + 1.0,
)?
.into())
};
let column_line = |u: f64| -> OgeomResult<ogeom_geom::PlanarCurve> {
Ok(Line2d::over(
ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
v_dom.0 - 1.0,
v_dom.1 + 1.0,
)?
.into())
};
ogeom_algo::attach_pcurve(
model,
&bottom,
row_line(v_dom.0)?,
surface_id,
ogeom_topo::Location::identity(),
u_dom,
)?;
ogeom_algo::attach_pcurve(
model,
&top,
row_line(v_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
u_dom,
)?;
ogeom_algo::attach_pcurve(
model,
&rail0,
column_line(u_dom.0)?,
surface_id,
ogeom_topo::Location::identity(),
v_dom,
)?;
ogeom_algo::attach_pcurve(
model,
&rail1,
column_line(u_dom.1)?,
surface_id,
ogeom_topo::Location::identity(),
v_dom,
)?;
for edge in [&bottom, &top, &rail0, &rail1] {
model.widen(edge, ogeom_core::Tolerance::new(error)?)?;
}
let wire = ogeom_algo::make_wire(
model,
&[
bottom.clone(),
rail1.clone(),
top.reversed(),
rail0.reversed(),
],
tol,
)?
.shape;
let face = ogeom_algo::make_face_on(model, surface_id, std::slice::from_ref(&wire), tol)?.shape;
let mid_u = f64::midpoint(u_dom.0, u_dom.1);
let mid_v = f64::midpoint(v_dom.0, v_dom.1);
let s_mid = surface_geo.point_at(mid_u, mid_v, tol)?;
let (du, dv) = surface_geo.d1_at(mid_u, mid_v, tol)?;
let natural_out = du.cross(dv).dot(s_mid - outward_hint) >= 0.0;
let face = if natural_out == !hole {
face
} else {
face.reversed()
};
Ok(SkinnedStrip {
face,
bottom,
top,
rail0,
rail1,
})
}
pub fn make_loft_skinned(
model: &mut Model,
sections: &[Shape],
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
if sections.len() < 2 {
ogeom_bail!(Construction, "a loft needs at least two sections");
}
const AROUND: usize = 48;
let apex = match model.kind_of(§ions[sections.len() - 1])? {
ShapeType::Vertex => {
if sections.len() < 2 {
ogeom_bail!(
Construction,
"a loft to a point needs a section to start from"
);
}
let Some(data) = model
.node(§ions[sections.len() - 1])
.and_then(|n| n.data().as_vertex())
else {
ogeom_bail!(Construction, "the apex vertex holds no point");
};
Some(data.point)
}
_ => None,
};
let wires = §ions[..sections.len() - usize::from(apex.is_some())];
let mut rows: Vec<Vec<Point>> = Vec::with_capacity(sections.len());
let mut cap_planes: Vec<Option<Plane>> = Vec::with_capacity(wires.len());
for wire in wires {
if model.kind_of(wire)? != ShapeType::Wire {
ogeom_bail!(Construction, "a loft section is a wire");
}
if !ogeom_algo::is_wire_closed(model, wire, tol)? {
ogeom_bail!(Construction, "a loft section must be closed");
}
cap_planes.push(ogeom_algo::find_plane(model, wire, tol)?);
rows.push(sample_wire(model, wire, AROUND, tol)?);
}
let outward_at =
|rows: &[Vec<Point>], planes: &[Option<Plane>], end: bool| -> OgeomResult<Vector> {
let (i, j) = if end {
(rows.len() - 1, rows.len() - 2)
} else {
(0, 1)
};
let Some(plane) = &planes[i] else {
ogeom_bail!(
Construction,
"a loft's end section must be planar; a cap stands on it"
);
};
let towards = rows[j][0] - rows[i][0];
let n = plane.normal().vector();
Ok(if n.dot(towards) > 0.0 { -n } else { n })
};
let mut built = if let Some(apex) = apex {
if rows.len() < 2 {
let half: Vec<Point> = rows[0]
.iter()
.map(|p| Point::from_vector((p.to_vector() + apex.to_vector()) * 0.5))
.collect();
rows.push(half);
}
let outward0 = outward_at(&rows, &cap_planes, false)?;
rows.push(vec![apex; AROUND]);
skinned_solid_to_apex(model, &rows, outward0, tolerance, tol)?
} else {
let outward0 = outward_at(&rows, &cap_planes, false)?;
let outward1 = outward_at(&rows, &cap_planes, true)?;
skinned_solid(model, &rows, (outward0, outward1), tolerance, tol)?
};
for section in sections {
built.history.generate(section, built.shape.clone());
}
Ok(built)
}
fn sample_wire(
model: &Model,
wire: &Shape,
count: usize,
tol: Tolerances,
) -> OgeomResult<Vec<Point>> {
sample_wire_from(model, wire, count, None, tol)
}
fn sample_wire_from(
model: &Model,
wire: &Shape,
count: usize,
start_hint: Option<Point>,
tol: Tolerances,
) -> OgeomResult<Vec<Point>> {
let mut dense: Vec<Point> = Vec::new();
for edge in explore(model, wire, Filter::OfType(ShapeType::Edge))? {
let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
ogeom_bail!(Construction, "a section edge holds no data");
};
let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "a section edge has no curve");
};
let Some(geometry) = model.geometry().curve(*curve) else {
ogeom_bail!(Dangling, "curve is not in this model");
};
let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
for i in 0..64 {
let f = f64::from(i) / 64.0;
let t = if reversed {
range.1 - (range.1 - range.0) * f
} else {
range.0 + (range.1 - range.0) * f
};
dense.push(geometry.point_at(t, tol)?);
}
}
if let Some(hint) = start_hint {
let mut best = 0usize;
let mut held = f64::INFINITY;
for (i, p) in dense.iter().enumerate() {
let d = p.distance(hint);
if d < held {
held = d;
best = i;
}
}
dense.rotate_left(best);
}
let mut lengths = vec![0.0];
for w in dense.windows(2) {
let last = lengths[lengths.len() - 1];
lengths.push(last + w[0].distance(w[1]));
}
let closing = dense[dense.len() - 1].distance(dense[0]);
let total = lengths[lengths.len() - 1] + closing;
let mut out = Vec::with_capacity(count);
let mut cursor = 0usize;
for s in 0..count {
#[allow(clippy::cast_precision_loss)]
let target = total * (s as f64) / (count as f64);
while cursor + 1 < lengths.len() && lengths[cursor + 1] < target {
cursor += 1;
}
let (a, b) = (dense[cursor], dense[(cursor + 1) % dense.len()]);
let la = lengths[cursor];
let lb = if cursor + 1 < lengths.len() {
lengths[cursor + 1]
} else {
total
};
let f = if lb > la {
(target - la) / (lb - la)
} else {
0.0
};
out.push(a + (b - a) * f.clamp(0.0, 1.0));
}
Ok(out)
}
pub fn make_pipe_skinned(
model: &mut Model,
spine: &Shape,
radius: f64,
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
if !radius.is_finite() || radius <= tol.confusion() {
ogeom_bail!(Construction, "a pipe of radius {radius} holds nothing");
}
let (curve, range) = {
let Some(data) = model.node(spine).and_then(|n| n.data().as_edge()) else {
ogeom_bail!(Construction, "a pipe runs along an edge");
};
let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "the spine has no curve");
};
let Some(geometry) = model.geometry().curve(*curve) else {
ogeom_bail!(Dangling, "curve is not in this model");
};
(geometry.clone(), *range)
};
const STATIONS: usize = 33;
const AROUND: usize = 40;
let mut stations: Vec<SpineStation> = Vec::with_capacity(STATIONS);
for i in 0..STATIONS {
#[allow(clippy::cast_precision_loss)]
let t = range.0 + (range.1 - range.0) * (i as f64) / ((STATIONS - 1) as f64);
let p = curve.point_at(t, tol)?;
let d = curve.d1_at(t, tol)?;
let m = d.magnitude();
if m <= tol.confusion() {
ogeom_bail!(Construction, "the spine is degenerate at {t}");
}
stations.push(SpineStation {
at: p,
tangent: d / m,
edge: 0,
t,
});
}
let normals = rmf_normals(&stations);
let mut rows: Vec<Vec<Point>> = Vec::with_capacity(STATIONS);
for (i, station) in stations.iter().enumerate() {
let x = normals[i];
let y = station.tangent.cross(x);
let mut row = Vec::with_capacity(AROUND);
for a in 0..AROUND {
#[allow(clippy::cast_precision_loss)]
let ang = core::f64::consts::TAU * (a as f64) / (AROUND as f64);
row.push(station.at + (x * ang.cos() + y * ang.sin()) * radius);
}
rows.push(row);
}
let mut built = skinned_solid(
model,
&rows,
(-stations[0].tangent, stations[STATIONS - 1].tangent),
tolerance,
tol,
)?;
built.history.generate(spine, built.shape.clone());
Ok(built)
}
#[derive(Clone, Copy)]
struct SpineStation {
at: Point,
tangent: Vector,
edge: usize,
t: f64,
}
#[allow(clippy::too_many_arguments, reason = "one frame, spelled out")]
fn closed_loop_shell(
model: &mut Model,
profile_loop: &Shape,
edges: &[Shape],
smooth: bool,
stations: &[SpineStation],
normals: &[Vector],
frame0: (Point, Vector),
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Shape> {
const AROUND: usize = 40;
let (origin, x0) = frame0;
let t0 = stations[0].tangent;
let y0 = t0.cross(x0);
if !smooth {
const ALONG_EDGE: usize = 8;
let mid_station = stations[stations.len() / 2].at;
let mut faces = Vec::with_capacity(edges.len());
let mut rails: Vec<Option<Shape>> = vec![None; edges.len()];
for (index, edge) in edges.iter().enumerate() {
let (curve, range) = spine_curve_of(model, edge)?;
let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
let mut flat_row: Vec<(f64, f64)> = Vec::with_capacity(ALONG_EDGE + 1);
for kk in 0..=ALONG_EDGE {
#[allow(clippy::cast_precision_loss)]
let f = (kk as f64) / (ALONG_EDGE as f64);
let t = if reversed {
range.1 - (range.1 - range.0) * f
} else {
range.0 + (range.1 - range.0) * f
};
let p = curve.point_at(t, tol)?;
flat_row.push(((p - origin).dot(x0), (p - origin).dot(y0)));
}
let rows: Vec<Vec<Point>> = stations
.iter()
.enumerate()
.map(|(i, station)| {
let x = normals[i];
let y = station.tangent.cross(x);
flat_row
.iter()
.map(|(a, b)| station.at + x * *a + y * *b)
.collect()
})
.collect();
let next = (index + 1) % edges.len();
let shared = [rails[index].clone(), rails[next].clone()];
let (face, rail0, rail1) = skinned_ring_strip(
model,
&rows,
mid_station,
[shared[0].as_ref(), shared[1].as_ref()],
tolerance,
tol,
)?;
rails[index] = Some(rail0);
rails[next] = Some(rail1);
faces.push(face);
}
let sewn = sew(model, &faces, tol)?;
if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
if std::env::var_os("OGEOM_DEBUG_RING").is_some() {
use ogeom_geom::Curve3d as _;
eprintln!(
"RING: {} shells from {} strips",
sewn.shells.len(),
faces.len()
);
for shell in &sewn.shells {
for edge in ogeom_topo::explore_unique(model, shell, ShapeType::Edge)? {
let mut uses = 0;
for f in explore(model, shell, Filter::OfType(ShapeType::Face))? {
for w in model.children_of(&f)? {
for e in model.children_of(&w)? {
if e.node() == edge.node() {
uses += 1;
}
}
}
}
if uses == 1
&& let Some(d) = model.node(&edge).and_then(|n| n.data().as_edge())
&& let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) =
d.curve3d()
&& let Some(g) = model.geometry().curve(*curve)
{
eprintln!(
"RING open edge tol {:.2e}: {:?} -> {:?}",
d.tolerance.get(),
g.point_at(range.0, tol)?,
g.point_at(range.1, tol)?
);
}
}
}
}
ogeom_bail!(Construction, "the faceted ring did not close");
}
return Ok(sewn.shells[0].clone());
}
let samples = sample_wire(model, profile_loop, AROUND, tol)?;
let flat: Vec<(f64, f64)> = samples
.iter()
.map(|p| ((*p - origin).dot(x0), (*p - origin).dot(y0)))
.collect();
let rows: Vec<Vec<Point>> = stations
.iter()
.enumerate()
.map(|(i, station)| {
let x = normals[i];
let y = station.tangent.cross(x);
flat.iter()
.map(|(a, b)| station.at + x * *a + y * *b)
.collect()
})
.collect();
closed_skinned_shell(model, &rows, tolerance, tol)
}
fn rmf_normals(stations: &[SpineStation]) -> Vec<Vector> {
let mut normals: Vec<Vector> = Vec::with_capacity(stations.len());
let t0 = stations[0].tangent;
let seed = if t0.cross(ogeom_math::Vector::Z).magnitude() > 0.5 {
ogeom_math::Vector::Z
} else {
ogeom_math::Vector::X
};
let n0 = {
let v = seed - t0 * seed.dot(t0);
v / v.magnitude()
};
normals.push(n0);
for i in 1..stations.len() {
let (p0, t0) = (stations[i - 1].at, stations[i - 1].tangent);
let (p1, t1) = (stations[i].at, stations[i].tangent);
let n = normals[i - 1];
let v1 = p1 - p0;
let c1 = v1.dot(v1);
if c1 <= 1e-20 {
let bisector = t0 + t1;
let m = bisector.magnitude();
if m <= 1e-12 {
normals.push(n);
continue;
}
let b = bisector / m;
normals.push(n - b * (2.0 * n.dot(b)));
continue;
}
normals.push(rmf_step(p0, t0, n, p1, t1));
}
normals
}
fn rmf_step(p0: Point, t0: Vector, n0: Vector, p1: Point, t1: Vector) -> Vector {
let v1 = p1 - p0;
let c1 = v1.dot(v1);
if c1 <= 1e-20 {
return n0;
}
let nl = n0 - v1 * (2.0 / c1 * v1.dot(n0));
let tl = t0 - v1 * (2.0 / c1 * v1.dot(t0));
let v2 = t1 - tl;
let c2 = v2.dot(v2);
let next = if c2 > 1e-20 {
nl - v2 * (2.0 / c2 * v2.dot(nl))
} else {
nl
};
next / next.magnitude()
}
struct SpineWalk<'a> {
curves: Vec<(ogeom_geom::Curve, (f64, f64), bool)>,
stations: &'a [SpineStation],
normals: &'a [Vector],
}
struct CornerJoin {
at: Point,
s1: f64,
s2: f64,
gap: f64,
}
impl SpineWalk<'_> {
fn frame_at(
&self,
s: f64,
(rs, re): (usize, usize),
tol: Tolerances,
) -> OgeomResult<(Point, Vector, Vector)> {
let st = self.stations;
let at = |i: usize| (st[i].at, st[i].tangent, self.normals[i]);
#[allow(clippy::cast_precision_loss)]
let (rsf, ref_) = (rs as f64, re as f64);
if s <= rsf {
let (p, t, n) = at(rs);
let h = st[rs].at.distance(st[(rs + 1).min(re)].at);
return Ok((p + t * ((s - rsf) * h), t, n));
}
if s >= ref_ {
let (p, t, n) = at(re);
let h = st[re].at.distance(st[re.saturating_sub(1).max(rs)].at);
return Ok((p + t * ((s - ref_) * h), t, n));
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let j = s.floor() as usize;
#[allow(clippy::cast_precision_loss)]
let f = s - j as f64;
if f <= 0.0 {
return Ok(at(j));
}
let (curve, range, reversed) = &self.curves[st[j + 1].edge];
let t_from = if st[j].edge == st[j + 1].edge {
st[j].t
} else if *reversed {
range.1
} else {
range.0
};
let t = t_from + (st[j + 1].t - t_from) * f;
let p = curve.point_at(t, tol)?;
let d = curve.d1_at(t, tol)?;
let m = d.magnitude();
if m <= tol.confusion() {
ogeom_bail!(Construction, "the spine is degenerate at {t}");
}
let tangent = if *reversed { -(d / m) } else { d / m };
let n = rmf_step(st[j].at, st[j].tangent, self.normals[j], p, tangent);
Ok((p, tangent, n))
}
fn generator(
&self,
s: f64,
run: (usize, usize),
(a, b): (f64, f64),
tol: Tolerances,
) -> OgeomResult<Point> {
let (p, t, x) = self.frame_at(s, run, tol)?;
let y = t.cross(x);
Ok(p + x * a + y * b)
}
fn join(
&self,
before: (usize, usize),
after: (usize, usize),
ab: (f64, f64),
tol: Tolerances,
) -> OgeomResult<CornerJoin> {
const STEP: f64 = 1e-4;
#[allow(clippy::cast_precision_loss)]
let (mut s1, mut s2) = (before.1 as f64, after.0 as f64);
for _ in 0..60 {
let g1 = self.generator(s1, before, ab, tol)?;
let g2 = self.generator(s2, after, ab, tol)?;
let f = g1 - g2;
let d1 = (self.generator(s1 + STEP, before, ab, tol)?
- self.generator(s1 - STEP, before, ab, tol)?)
/ (2.0 * STEP);
let d2 = (self.generator(s2 + STEP, after, ab, tol)?
- self.generator(s2 - STEP, after, ab, tol)?)
/ (2.0 * STEP);
let (a11, a12, a22) = (d1.dot(d1), -d1.dot(d2), d2.dot(d2));
let (b1, b2) = (-f.dot(d1), f.dot(d2));
let det = a11 * a22 - a12 * a12;
if det.abs() <= 1e-30 {
break;
}
let e1 = (b1 * a22 - a12 * b2) / det;
let e2 = (a11 * b2 - a12 * b1) / det;
s1 += e1;
s2 += e2;
if e1.abs().max(e2.abs()) <= 1e-12 {
break;
}
}
let g1 = self.generator(s1, before, ab, tol)?;
let g2 = self.generator(s2, after, ab, tol)?;
Ok(CornerJoin {
at: g1.midpoint(g2),
s1,
s2,
gap: g1.distance(g2),
})
}
}
pub fn make_pipe_shell(
model: &mut Model,
profile: &Shape,
spine: &Shape,
frenet: bool,
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
const AROUND: usize = 40;
let stations = shell_stations(model, spine, tol)?;
let kinks: Vec<usize> = (0..stations.len() - 1)
.filter(|&i| {
stations[i].at.distance(stations[i + 1].at) <= tol.confusion()
&& (stations[i]
.tangent
.cross(stations[i + 1].tangent)
.magnitude()
> tol.angular()
|| stations[i].tangent.dot(stations[i + 1].tangent) < 0.0)
})
.collect();
let ring = stations[0].at.distance(stations[stations.len() - 1].at) <= tol.confusion() * 10.0;
if ring && kinks.is_empty() {
return closed_pipe_shell(model, profile, spine, stations, frenet, tolerance, tol);
}
if frenet && !kinks.is_empty() {
ogeom_bail!(
Construction,
"a Frenet frame has no direction at a corner; sweep a cornered \
spine with the rotation-minimizing frame"
);
}
let normals = if frenet {
frenet_normals(&stations, tol)?
} else {
rmf_normals(&stations)
};
let normals = if ring {
let mut extended = stations.clone();
extended.push(stations[0]);
let carried = rmf_normals(&extended);
let (n0, n_home) = (carried[0], carried[carried.len() - 1]);
let t0 = stations[0].tangent;
let twist = (n0.cross(n_home).dot(t0)).atan2(n0.dot(n_home));
let mut lengths = vec![0.0_f64];
for pair in extended.windows(2) {
let held = lengths[lengths.len() - 1];
lengths.push(held + pair[0].at.distance(pair[1].at));
}
let total = lengths[lengths.len() - 1];
carried
.iter()
.take(stations.len())
.enumerate()
.map(|(i, n)| {
let phi = -twist * lengths[i] / total;
let t = extended[i].tangent;
let v = *n * phi.cos() + t.cross(*n) * phi.sin();
let v = v - t * v.dot(t);
v / v.magnitude()
})
.collect()
} else {
normals
};
let mitre: Vec<Option<(Point, Vector)>> = {
let mut out: Vec<Option<(Point, Vector)>> = vec![None; stations.len()];
for &k in &kinks {
let n = stations[k].tangent + stations[k + 1].tangent;
if n.magnitude() <= tol.angular() {
ogeom_bail!(
Construction,
"the spine doubles straight back on itself; no mitre \
plane divides that corner"
);
}
out[k] = Some((stations[k].at, n));
out[k + 1] = Some((stations[k + 1].at, n));
}
if ring {
let wrap = stations.len() - 1;
let n = stations[wrap].tangent + stations[0].tangent;
if n.magnitude() <= tol.angular() {
ogeom_bail!(
Construction,
"the spine doubles straight back on itself; no mitre \
plane divides that corner"
);
}
out[wrap] = Some((stations[wrap].at, n));
out[0] = Some((stations[0].at, n));
}
out
};
let runs: Vec<(usize, usize)> = {
let mut out = Vec::with_capacity(kinks.len() + 1);
let mut start = 0;
for &k in &kinks {
out.push((start, k));
start = k + 1;
}
out.push((start, stations.len() - 1));
out
};
let straight = |rs: usize, re: usize| -> bool {
let t0 = stations[rs].tangent;
(rs..=re).all(|i| stations[i].tangent.cross(t0).magnitude() <= tol.angular())
};
struct CornerPair {
before: (usize, usize),
after: (usize, usize),
curved: bool,
}
let corner_pairs: Vec<CornerPair> = {
let mut out = Vec::new();
for pair in runs.windows(2) {
out.push(CornerPair {
before: pair[0],
after: pair[1],
curved: !straight(pair[0].0, pair[0].1) || !straight(pair[1].0, pair[1].1),
});
}
if ring && runs.len() > 1 {
let (before, after) = (runs[runs.len() - 1], runs[0]);
out.push(CornerPair {
before,
after,
curved: !straight(before.0, before.1) || !straight(after.0, after.1),
});
}
out
};
let curves: Vec<(ogeom_geom::Curve, (f64, f64), bool)> = {
let edges: Vec<Shape> = match model.kind_of(spine)? {
ShapeType::Edge => vec![spine.clone()],
_ => model.ordered_children_of(spine)?,
};
let mut out = Vec::with_capacity(edges.len());
for edge in &edges {
let (curve, range) = spine_curve_of(model, edge)?;
out.push((
curve,
range,
edge.orientation() == ogeom_topo::Orientation::Reversed,
));
}
out
};
let walk = SpineWalk {
curves,
stations: &stations,
normals: &normals,
};
let join_reach = tolerance.max(tol.confusion() * 100.0);
let curved_join = |pair: &CornerPair, ab: (f64, f64)| -> OgeomResult<CornerJoin> {
let join = walk.join(pair.before, pair.after, ab, tol)?;
if join.gap > join_reach {
ogeom_bail!(
Construction,
"a skew corner against a curved leg is still owed its frame \
law: the legs' generators miss by {} — docs/PARITY.md, \
offset.sweeps",
join.gap
);
}
Ok(join)
};
let shares_start = |ri: usize| -> bool {
ri > 0
&& corner_pairs
.iter()
.any(|pair| pair.curved && pair.after == runs[ri])
};
let shares_end = |ri: usize| -> bool {
ring && ri + 1 == runs.len()
&& corner_pairs
.iter()
.any(|pair| pair.curved && pair.before == runs[ri] && pair.after == runs[0])
};
let curved_at = |i: usize| -> Option<&CornerPair> {
corner_pairs
.iter()
.find(|pair| pair.curved && (pair.before.1 == i || pair.after.0 == i))
};
let loops: Vec<Shape> = match model.kind_of(profile)? {
ShapeType::Face => explore(model, profile, Filter::OfType(ShapeType::Wire))?,
ShapeType::Wire => vec![profile.clone()],
other => ogeom_bail!(
Construction,
"a pipe shell sweeps a planar wire or face, not a {other:?}"
),
};
if loops.is_empty() {
ogeom_bail!(Construction, "the profile has no loop to sweep");
}
let Some(plane) = ogeom_algo::find_plane(model, profile, tol)? else {
ogeom_bail!(Construction, "a pipe shell sweeps a planar profile");
};
let t0 = stations[0].tangent;
if plane.normal().vector().cross(t0).magnitude() > 1e-9 {
ogeom_bail!(
Construction,
"the profile leans along its spine; a pipe shell runs square to \
the start"
);
}
if plane.distance_to(stations[0].at) > tol.confusion() * 100.0 {
ogeom_bail!(
Construction,
"the profile does not sit at the spine's start"
);
}
let x0 = normals[0];
let y0 = t0.cross(x0);
let origin = stations[0].at;
let flat = |p: Point| -> (f64, f64) { ((p - origin).dot(x0), (p - origin).dot(y0)) };
let place = |i: usize, (a, b): (f64, f64)| -> OgeomResult<Point> {
if let Some(pair) = curved_at(i) {
return Ok(curved_join(pair, (a, b))?.at);
}
let x = normals[i];
let y = stations[i].tangent.cross(x);
let p = stations[i].at + x * a + y * b;
Ok(match mitre[i] {
Some((corner, n)) => {
let t = stations[i].tangent;
p + t * ((corner - p).dot(n) / t.dot(n))
}
None => p,
})
};
let run_rows =
|(rs, re): (usize, usize), flat_row: &[(f64, f64)]| -> OgeomResult<Vec<Vec<Point>>> {
let run = (rs, re);
let joins_at = |at_start: bool| -> OgeomResult<Option<Vec<CornerJoin>>> {
let pair = corner_pairs.iter().find(|pair| {
pair.curved
&& if at_start {
pair.after == run
} else {
pair.before == run
}
});
let Some(pair) = pair else {
return Ok(None);
};
let mut out = Vec::with_capacity(flat_row.len());
for ab in flat_row {
out.push(curved_join(pair, *ab)?);
}
Ok(Some(out))
};
let (start, end) = (joins_at(true)?, joins_at(false)?);
#[allow(clippy::cast_precision_loss)]
let (rsf, ref_) = (rs as f64, re as f64);
let short = || {
ogeom_bail!(
Construction,
"a leg is shorter than its corner's reach; the mitre would \
run off its far end"
)
};
let mut rows: Vec<Vec<Point>> = Vec::new();
if straight(rs, re) {
if start
.as_ref()
.is_some_and(|js| js.iter().any(|j| j.s2 >= ref_ - 0.5))
|| end
.as_ref()
.is_some_and(|js| js.iter().any(|j| j.s1 <= rsf + 0.5))
{
return short();
}
for (at_start, joins) in [(true, &start), (false, &end)] {
rows.push(match joins {
Some(js) => js.iter().map(|j| j.at).collect(),
None => {
let i = if at_start { rs } else { re };
flat_row
.iter()
.map(|ab| place(i, *ab))
.collect::<OgeomResult<Vec<Point>>>()?
}
});
}
return Ok(rows);
}
if start.is_none() && end.is_none() {
for i in rs..=re {
rows.push(
flat_row
.iter()
.map(|ab| place(i, *ab))
.collect::<OgeomResult<Vec<Point>>>()?,
);
}
return Ok(rows);
}
if start
.as_ref()
.is_some_and(|js| js.iter().any(|j| j.s2 >= ref_ - 0.5))
|| end
.as_ref()
.is_some_and(|js| js.iter().any(|j| j.s1 <= rsf + 0.5))
{
return short();
}
let steps = re - rs;
let fine = steps * 8;
let mut columns: Vec<Vec<Point>> = Vec::with_capacity(flat_row.len());
for (k, ab) in flat_row.iter().enumerate() {
let lo = start.as_ref().map_or(rsf, |js| js[k].s2);
let hi = end.as_ref().map_or(ref_, |js| js[k].s1);
let mut along: Vec<(f64, f64)> = Vec::with_capacity(fine + 1);
let mut prev: Option<Point> = None;
let mut length = 0.0;
for i in 0..=fine {
#[allow(clippy::cast_precision_loss)]
let sp = lo + (hi - lo) * (i as f64) / (fine as f64);
let p = walk.generator(sp, run, *ab, tol)?;
if let Some(q) = prev {
length += q.distance(p);
}
along.push((length, sp));
prev = Some(p);
}
let mut column = Vec::with_capacity(steps + 1);
for i in 0..=steps {
#[allow(clippy::cast_precision_loss)]
let target = length * (i as f64) / (steps as f64);
let at = along.partition_point(|(l, _)| *l < target).clamp(1, fine);
let ((l0, s0), (l1, s1)) = (along[at - 1], along[at]);
let f = if l1 > l0 {
((target - l0) / (l1 - l0)).clamp(0.0, 1.0)
} else {
0.0
};
column.push(walk.generator(s0 + (s1 - s0) * f, run, *ab, tol)?);
}
columns.push(column);
}
for i in 0..=steps {
rows.push(columns.iter().map(|c| c[i]).collect());
}
Ok(rows)
};
let last = stations.len() - 1;
enum LoopWall {
Ring {
ring0: Shape,
ring1: Shape,
},
Chain {
bottoms: Vec<Shape>,
tops: Vec<Shape>,
},
}
let mut faces: Vec<Shape> = Vec::new();
let mut ends: Vec<LoopWall> = Vec::with_capacity(loops.len());
for (li, wire) in loops.iter().enumerate() {
let hole = li != 0;
let edges = model.ordered_children_of(wire)?;
let single_smooth = edges.len() == 1 && {
let (curve, _) = spine_curve_of(model, &edges[0])?;
!matches!(curve, ogeom_geom::Curve::Line(_))
&& ogeom_algo::edge_vertices(model, &edges[0])?.is_some_and(|(a, b)| a.is_same(&b))
};
if single_smooth {
let samples = sample_wire(model, wire, AROUND, tol)?;
let flat_row: Vec<(f64, f64)> = samples.iter().map(|p| flat(*p)).collect();
let mut ring0: Option<Shape> = None;
let mut ring1: Option<Shape> = None;
for (ri, &(rs, re)) in runs.iter().enumerate() {
let rows = run_rows((rs, re), &flat_row)?;
let shared_start = shares_start(ri).then_some(()).and(ring1.as_ref());
let shared_end = shares_end(ri).then_some(()).and(ring0.as_ref());
let wall = skinned_wall(model, &rows, (shared_start, shared_end), tolerance, tol)?;
faces.push(if hole {
wall.face.reversed()
} else {
wall.face.clone()
});
if ring0.is_none() {
ring0 = Some(wall.ring0);
}
ring1 = Some(wall.ring1);
}
let (Some(ring0), Some(ring1)) = (ring0, ring1) else {
ogeom_bail!(Construction, "the sweep produced no wall");
};
ends.push(LoopWall::Ring { ring0, ring1 });
} else {
let count = edges.len();
let mut corner_flat: Vec<(f64, f64)> = Vec::with_capacity(count);
for edge in &edges {
let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
ogeom_bail!(Construction, "a profile edge has no vertices");
};
let start = if edge.orientation() == ogeom_topo::Orientation::Reversed {
b
} else {
a
};
let Some(data) = model.node(&start).and_then(|n| n.data().as_vertex()) else {
ogeom_bail!(Construction, "a profile vertex holds no data");
};
corner_flat.push(flat(data.point));
}
let make_corners = |model: &mut Model, station: usize| -> OgeomResult<Vec<Shape>> {
let mut out = Vec::with_capacity(corner_flat.len());
for ab in &corner_flat {
out.push(ogeom_algo::make_vertex(model, place(station, *ab)?).shape);
}
Ok(out)
};
let mut corners_at: Vec<Option<Vec<Shape>>> = vec![None; stations.len()];
if ring {
let mut worst = 0.0_f64;
for ab in &corner_flat {
worst = worst.max(place(last, *ab)?.distance(place(0, *ab)?));
for &k in &kinks {
worst = worst.max(place(k, *ab)?.distance(place(k + 1, *ab)?));
}
}
if worst > tolerance.max(tol.confusion() * 100.0) {
ogeom_bail!(
Construction,
"a skew-cornered ring's sections do not meet on \
their mitres; the out-of-plane corner's frame law \
is still owed — docs/PARITY.md, offset.sweeps"
);
}
let set = make_corners(model, 0)?;
if worst > tol.confusion() {
for v in &set {
model.widen(v, ogeom_core::Tolerance::new(worst * 2.0)?)?;
}
}
corners_at[0] = Some(set.clone());
corners_at[last] = Some(set);
} else {
corners_at[0] = Some(make_corners(model, 0)?);
corners_at[last] = Some(make_corners(model, last)?);
}
for &k in &kinks {
let set = make_corners(model, k)?;
corners_at[k] = Some(set.clone());
corners_at[k + 1] = Some(set);
}
let hint_flat = {
let mut a = 0.0;
let mut b = 0.0;
for (fa, fb) in &corner_flat {
a += fa;
b += fb;
}
#[allow(clippy::cast_precision_loss)]
let n = count as f64;
(a / n, b / n)
};
let mut bottoms = Vec::with_capacity(count);
let mut tops = Vec::with_capacity(count);
let mut run_rails: Vec<(Option<Shape>, Option<Shape>)> = vec![(None, None); runs.len()];
for (ei, edge) in edges.iter().enumerate() {
let (curve, range) = spine_curve_of(model, edge)?;
let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
const ALONG_EDGE: usize = 8;
let mut flat_row: Vec<(f64, f64)> = Vec::with_capacity(ALONG_EDGE + 1);
for k in 0..=ALONG_EDGE {
#[allow(clippy::cast_precision_loss)]
let f = (k as f64) / (ALONG_EDGE as f64);
let t = if reversed {
range.1 - (range.1 - range.0) * f
} else {
range.0 + (range.1 - range.0) * f
};
flat_row.push(flat(curve.point_at(t, tol)?));
}
let next = (ei + 1) % count;
let mut bottom: Option<Shape> = None;
let mut top: Option<Shape> = None;
for (ri, &(rs, re)) in runs.iter().enumerate() {
let rows = run_rows((rs, re), &flat_row)?;
let shared_start = shares_start(ri).then_some(()).and(top.as_ref());
let shared_end = shares_end(ri).then_some(()).and(bottom.as_ref());
let mid_i = usize::midpoint(rs, re);
let hint = {
let x = normals[mid_i];
let y = stations[mid_i].tangent.cross(x);
stations[mid_i].at + x * hint_flat.0 + y * hint_flat.1
};
let (Some(from), Some(to)) = (&corners_at[rs], &corners_at[re]) else {
ogeom_bail!(Construction, "a run boundary has no corners");
};
let (first_rail0, prev_rail1) = run_rails[ri].clone();
let shared_rail0 = if ei > 0 { prev_rail1 } else { None };
let shared_rail1 = if ei + 1 == count && count > 1 {
first_rail0.clone()
} else {
None
};
let strip = skinned_strip(
model,
&rows,
(&from[ei], &from[next], &to[ei], &to[next]),
[
shared_start,
shared_end,
shared_rail0.as_ref(),
shared_rail1.as_ref(),
],
hint,
hole,
tolerance,
tol,
)?;
run_rails[ri] = (
if ei == 0 {
Some(strip.rail0.clone())
} else {
first_rail0
},
Some(strip.rail1.clone()),
);
faces.push(strip.face.clone());
if bottom.is_none() {
bottom = Some(strip.bottom);
}
top = Some(strip.top);
}
let (Some(bottom), Some(top)) = (bottom, top) else {
ogeom_bail!(Construction, "the sweep produced no strip");
};
bottoms.push(bottom);
tops.push(top);
}
ends.push(LoopWall::Chain { bottoms, tops });
}
}
for end in 0..if ring { 0 } else { 2 } {
let (at, outward) = if end == 0 {
(stations[0].at, -stations[0].tangent)
} else {
(stations[last].at, stations[last].tangent)
};
let cap_plane = Plane::through(at, Direction::new(outward, tol)?);
let mut loop_edges: Vec<Vec<Shape>> = Vec::with_capacity(ends.len());
for wall in &ends {
loop_edges.push(match wall {
LoopWall::Ring { ring0, ring1 } => {
vec![if end == 0 {
ring0.clone()
} else {
ring1.clone()
}]
}
LoopWall::Chain { bottoms, tops } => {
if end == 0 {
bottoms.clone()
} else {
tops.clone()
}
}
});
}
let mut reach = 1.0_f64;
for edges in &loop_edges {
for edge in edges {
let (curve, range) = spine_curve_of(model, edge)?;
for t in 0..8 {
let p =
curve.point_at(range.0 + (range.1 - range.0) * f64::from(t) / 8.0, tol)?;
reach = reach.max(p.distance(at) * 2.0);
}
}
}
let cap_surface: SurfaceGeometry =
PlaneSurface::over(cap_plane, (-reach, reach), (-reach, reach))?.into();
let mut wires: Vec<Shape> = Vec::with_capacity(loop_edges.len());
for edges in &loop_edges {
wires.push(ogeom_algo::make_wire(model, edges, tol)?.shape);
}
let face = ogeom_algo::make_face(model, cap_surface.clone(), &wires, tol)?.shape;
let cap_id = {
let Some(node) = model.node(&face) else {
ogeom_bail!(Dangling, "the cap just built is not in this model");
};
let ogeom_topo::NodeData::Face(data) = node.data() else {
ogeom_bail!(Construction, "the cap holds no face data");
};
data.surface
};
let frame = cap_plane.frame();
for edges in &loop_edges {
for edge in edges {
let (curve, range) = spine_curve_of(model, edge)?;
let ogeom_geom::Curve::BSpline(bs) = &curve else {
ogeom_bail!(Construction, "a swept ring is not a spline");
};
let control2: Vec<Point2> = bs
.control_points()
.iter()
.map(|w| {
let local = frame.to_local(w.point());
Point2::new(local.x, local.y)
})
.collect();
let pcurve: ogeom_geom::PlanarCurve =
ogeom_geom::BSpline2d::new(bs.knots().clone(), control2, tol)?.into();
ogeom_algo::attach_pcurve(
model,
edge,
pcurve,
cap_id,
ogeom_topo::Location::identity(),
range,
)?;
}
}
faces.push(face);
}
let sewn = sew(model, &faces, tol)?;
let mut built = if ring {
if sewn.shells.is_empty() {
ogeom_bail!(Construction, "the pipe shell did not close");
}
for shell in &sewn.shells {
if !ogeom_algo::is_shell_closed(model, shell)? {
ogeom_bail!(Construction, "the pipe shell did not close");
}
}
let mut ordered = sewn.shells.clone();
let mut sized: Vec<(f64, Shape)> = Vec::with_capacity(ordered.len());
for shell in ordered.drain(..) {
let bound = ogeom_algo::shape_bounds(model, &shell, tol)?;
sized.push((bound.diagonal(), shell));
}
sized.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(core::cmp::Ordering::Equal));
let shells: Vec<Shape> = sized.into_iter().map(|(_, s)| s).collect();
make_solid(model, &shells)?
} else {
if sewn.shells.len() != 1 || !ogeom_algo::is_shell_closed(model, &sewn.shells[0])? {
if std::env::var_os("OGEOM_DEBUG_SWEEP").is_some() {
eprintln!(
"SWEEP: {} shells, {} free edges from {} faces",
sewn.shells.len(),
sewn.free_edges.len(),
faces.len()
);
for edge in &sewn.free_edges {
let (curve, range) = spine_curve_of(model, edge)?;
let a = curve.point_at(range.0, tol)?;
let b = curve.point_at(range.1, tol)?;
let m = curve.point_at(f64::midpoint(range.0, range.1), tol)?;
let t = model.tolerance_of(edge)?.map_or(0.0, |t| t.get());
eprintln!(
" free ({:.3},{:.3},{:.3}) -> ({:.3},{:.3},{:.3}) via ({:.3},{:.3},{:.3}) tol {t:.2e}",
a.x, a.y, a.z, b.x, b.y, b.z, m.x, m.y, m.z
);
}
}
ogeom_bail!(Construction, "the pipe shell did not close");
}
make_solid(model, std::slice::from_ref(&sewn.shells[0]))?
};
built.history.generate(profile, built.shape.clone());
built.history.generate(spine, built.shape.clone());
Ok(built)
}
fn spine_curve_of(model: &Model, edge: &Shape) -> OgeomResult<(ogeom_geom::Curve, (f64, f64))> {
let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
ogeom_bail!(Construction, "an edge holds no data");
};
let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "an edge has no curve");
};
let Some(geometry) = model.geometry().curve(*curve) else {
ogeom_bail!(Dangling, "curve is not in this model");
};
Ok((geometry.clone(), *range))
}
fn closed_pipe_shell(
model: &mut Model,
profile: &Shape,
spine: &Shape,
mut stations: Vec<SpineStation>,
frenet: bool,
tolerance: f64,
tol: Tolerances,
) -> OgeomResult<Built> {
stations.pop();
if stations.len() < 3 {
ogeom_bail!(Construction, "a closed spine needs room to turn");
}
for i in 0..stations.len() {
let next = &stations[(i + 1) % stations.len()];
if stations[i].tangent.dot(next.tangent) < 0.9 {
ogeom_bail!(
Construction,
"a closed spine with a sharp corner needs the mitred strips, \
which are still owed — docs/PARITY.md, offset.sweeps; round \
the corner and the ring sweeps"
);
}
}
let loops: Vec<Shape> = match model.kind_of(profile)? {
ShapeType::Face => explore(model, profile, Filter::OfType(ShapeType::Wire))?,
ShapeType::Wire => vec![profile.clone()],
other => ogeom_bail!(
Construction,
"a pipe shell sweeps a planar wire or face, not a {other:?}"
),
};
let profile_loop = &loops[0];
let edges = model.ordered_children_of(profile_loop)?;
let smooth = edges.len() == 1
&& ogeom_algo::edge_vertices(model, &edges[0])?.is_some_and(|(a, b)| a.is_same(&b));
let Some(plane) = ogeom_algo::find_plane(model, profile, tol)? else {
ogeom_bail!(Construction, "a pipe shell sweeps a planar profile");
};
let t0 = stations[0].tangent;
if plane.normal().vector().cross(t0).magnitude() > 1e-9 {
ogeom_bail!(
Construction,
"the profile leans along its spine; a pipe shell runs square to \
the start"
);
}
if plane.distance_to(stations[0].at) > tol.confusion() * 100.0 {
ogeom_bail!(
Construction,
"the profile does not sit at the spine's start"
);
}
let mut normals: Vec<Vector> = if frenet {
frenet_normals_closed(&stations, tol)?
} else {
let mut extended = stations.clone();
extended.push(stations[0]);
let carried = rmf_normals(&extended);
let (n0, n_home) = (carried[0], carried[carried.len() - 1]);
let twist = (n0.cross(n_home).dot(t0)).atan2(n0.dot(n_home));
let mut lengths = vec![0.0_f64];
for pair in extended.windows(2) {
let last = lengths[lengths.len() - 1];
lengths.push(last + pair[0].at.distance(pair[1].at));
}
let total = lengths[lengths.len() - 1];
carried
.iter()
.take(stations.len())
.enumerate()
.map(|(i, n)| {
let phi = -twist * lengths[i] / total;
let t = extended[i].tangent;
*n * phi.cos() + t.cross(*n) * phi.sin()
})
.collect()
};
for (n, station) in normals.iter_mut().zip(&stations) {
let v = *n - station.tangent * n.dot(station.tangent);
*n = v / v.magnitude();
}
let x0 = normals[0];
let origin = stations[0].at;
let mut shells: Vec<Shape> = Vec::with_capacity(loops.len());
for (li, wire) in loops.iter().enumerate() {
let wire_edges = model.ordered_children_of(wire)?;
let wire_smooth = wire_edges.len() == 1
&& ogeom_algo::edge_vertices(model, &wire_edges[0])?
.is_some_and(|(a, b)| a.is_same(&b));
let shell = closed_loop_shell(
model,
wire,
&wire_edges,
wire_smooth,
&stations,
&normals,
(origin, x0),
tolerance,
tol,
)?;
shells.push(if li == 0 { shell } else { shell.reversed() });
}
let mut built = make_solid(model, &shells)?;
built.history.generate(profile, built.shape.clone());
built.history.generate(spine, built.shape.clone());
let _ = (smooth, profile_loop, edges);
Ok(built)
}
fn shell_stations(model: &Model, spine: &Shape, tol: Tolerances) -> OgeomResult<Vec<SpineStation>> {
let edges: Vec<Shape> = match model.kind_of(spine)? {
ShapeType::Edge => vec![spine.clone()],
ShapeType::Wire => model.ordered_children_of(spine)?,
other => ogeom_bail!(
Construction,
"a pipe shell runs along an edge or a wire, not a {other:?}"
),
};
if edges.is_empty() {
ogeom_bail!(Construction, "the spine has no edge to run along");
}
let mut stations: Vec<SpineStation> = Vec::new();
for (ei, edge) in edges.iter().enumerate() {
let (curve, range) = {
let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
ogeom_bail!(Construction, "a spine edge holds no data");
};
let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "a spine edge has no curve");
};
let Some(geometry) = model.geometry().curve(*curve) else {
ogeom_bail!(Dangling, "curve is not in this model");
};
(geometry.clone(), *range)
};
let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
let turning = {
let mut sum = 0.0_f64;
let mut last: Option<Vector> = None;
for i in 0..=16 {
let t = range.0 + (range.1 - range.0) * f64::from(i) / 16.0;
let d = curve.d1_at(t, tol)?;
let m = d.magnitude();
if m <= tol.confusion() {
continue;
}
let u = d / m;
if let Some(prev) = last {
sum += prev.dot(u).clamp(-1.0, 1.0).acos() * 16.0 / 16.0;
}
last = Some(u);
}
sum
};
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let count = (turning / (core::f64::consts::TAU / 64.0)).ceil().max(8.0) as usize;
for i in 0..=count {
#[allow(clippy::cast_precision_loss)]
let f = (i as f64) / (count as f64);
let t = if reversed {
range.1 - (range.1 - range.0) * f
} else {
range.0 + (range.1 - range.0) * f
};
let p = curve.point_at(t, tol)?;
let d = curve.d1_at(t, tol)?;
let m = d.magnitude();
if m <= tol.confusion() {
ogeom_bail!(Construction, "the spine is degenerate at {t}");
}
let tangent = if reversed { -(d / m) } else { d / m };
if let Some(prev) = stations.last()
&& prev.at.distance(p) <= tol.confusion()
&& prev.tangent.cross(tangent).magnitude() <= tol.angular()
&& prev.tangent.dot(tangent) > 0.0
{
continue;
}
stations.push(SpineStation {
at: p,
tangent,
edge: ei,
t,
});
}
}
if stations.len() < 2 {
ogeom_bail!(Construction, "the spine collapses to a point");
}
Ok(stations)
}
fn frenet_normals(stations: &[SpineStation], tol: Tolerances) -> OgeomResult<Vec<Vector>> {
let mut normals: Vec<Option<Vector>> = Vec::with_capacity(stations.len());
for i in 0..stations.len() {
let (before, after) = (
&stations[i.saturating_sub(1)],
&stations[(i + 1).min(stations.len() - 1)],
);
let dt = after.tangent - before.tangent;
let t = stations[i].tangent;
let bend = dt - t * dt.dot(t);
let m = bend.magnitude();
normals.push(if m > tol.angular().max(1e-9) {
Some(bend / m)
} else {
None
});
}
let mut carried: Vec<Vector> = Vec::with_capacity(stations.len());
let mut last: Option<Vector> = None;
for n in &normals {
if let Some(n) = n {
last = Some(*n);
}
carried.push(last.unwrap_or(Vector::new(0.0, 0.0, 0.0)));
}
let mut ahead: Option<Vector> = None;
for i in (0..stations.len()).rev() {
if let Some(n) = normals[i] {
ahead = Some(n);
} else if carried[i].magnitude() < 0.5
&& let Some(n) = ahead
{
carried[i] = n;
}
}
if carried.iter().any(|n| n.magnitude() < 0.5) {
ogeom_bail!(
Construction,
"a straight spine has no Frenet frame; use the \
rotation-minimizing default"
);
}
Ok(carried)
}
fn frenet_normals_closed(stations: &[SpineStation], tol: Tolerances) -> OgeomResult<Vec<Vector>> {
let n = stations.len();
let mut normals: Vec<Option<Vector>> = Vec::with_capacity(n);
for i in 0..n {
let (before, after) = (&stations[(i + n - 1) % n], &stations[(i + 1) % n]);
let dt = after.tangent - before.tangent;
let t = stations[i].tangent;
let bend = dt - t * dt.dot(t);
let m = bend.magnitude();
normals.push(if m > tol.angular().max(1e-9) {
Some(bend / m)
} else {
None
});
}
let Some(first_bend) = normals.iter().position(Option::is_some) else {
ogeom_bail!(
Construction,
"a straight spine has no Frenet frame; use the \
rotation-minimizing default"
);
};
let mut carried: Vec<Vector> = vec![Vector::new(0.0, 0.0, 0.0); n];
let mut last = normals[first_bend].unwrap_or_else(|| unreachable!());
for k in 0..n {
let i = (first_bend + k) % n;
if let Some(bend) = normals[i] {
last = bend;
}
carried[i] = last;
}
Ok(carried)
}
struct Station {
from: Point,
to: Point,
tangent_in: Vector,
tangent_out: Vector,
turn: Option<(ogeom_math::Axis, f64)>,
}
pub fn make_evolved(
model: &mut Model,
spine: &Shape,
profile: &Shape,
tol: Tolerances,
) -> OgeomResult<Built> {
use ogeom_algo::{make_prism, make_revolution, transformed};
let (wire, capped_by_spine_plane) = match model.kind_of(spine)? {
ShapeType::Face => {
let wires = explore(model, spine, Filter::OfType(ShapeType::Wire))?;
let Some(outer) = wires.first().cloned() else {
ogeom_bail!(Construction, "a face with no wire has no spine to run");
};
(outer, true)
}
ShapeType::Wire => (spine.clone(), false),
other => ogeom_bail!(
Construction,
"a {other:?} is not a spine; sweep along a wire or a planar face"
),
};
let stations = spine_stations(model, &wire, tol)?;
if stations.is_empty() {
ogeom_bail!(Construction, "a spine with no edges goes nowhere");
}
let normal = spine_normal(&stations, tol)?;
let (profile_origin, profile_normal) = profile_plane(model, profile, tol)?;
if profile_normal.dot(normal.vector()).abs() > tol.angular() {
ogeom_bail!(
Construction,
"the profile's plane must contain the spine's normal, or the \
profile is not square to the spine it travels"
);
}
let start = &stations[0];
if profile_normal.cross(start.tangent_in).magnitude() > tol.angular() {
ogeom_bail!(
Construction,
"the profile's plane must cut the spine across, not run along it: \
the profile is not square to the spine it travels"
);
}
let _ = profile_origin;
let reference = (start.from, start.tangent_in);
let section = profile_face(
model,
profile,
profile_normal,
capped_by_spine_plane.then(|| Plane::through(start.from, normal)),
tol,
)?;
let mut pieces: Vec<Shape> = Vec::new();
for (index, station) in stations.iter().enumerate() {
if index > 0 {
let previous = &stations[index - 1];
if let Some(piece) = corner_piece(
model,
§ion,
reference,
previous.to,
previous.tangent_out,
station.tangent_in,
normal,
tol,
)? {
pieces.push(piece);
}
}
let placed = transformed(
model,
§ion,
station_transform(reference, station.from, station.tangent_in, normal, tol)?,
)?
.shape;
pieces.push(match station.turn {
None => make_prism(model, &placed, station.to - station.from, tol)?.shape,
Some((axis, angle)) => make_revolution(model, &placed, axis, angle, tol)?.shape,
});
}
let last = &stations[stations.len() - 1];
if last.to.distance(start.from) <= tol.confusion()
&& let Some(piece) = corner_piece(
model,
§ion,
reference,
last.to,
last.tangent_out,
start.tangent_in,
normal,
tol,
)?
{
pieces.push(piece);
}
let mut history = History::new();
let mut shape = pieces[0].clone();
for piece in &pieces[1..] {
shape = ogeom_bool::fuse(model, &shape, piece, tol)?.shape;
}
history.generate(spine, shape.clone());
history.generate(profile, shape.clone());
Ok(Built::new(shape, history))
}
fn profile_face(
model: &mut Model,
profile: &Shape,
profile_normal: Vector,
against: Option<Plane>,
tol: Tolerances,
) -> OgeomResult<Shape> {
if model.kind_of(profile)? == ShapeType::Face {
return Ok(profile.clone());
}
if model.kind_of(profile)? != ShapeType::Wire {
ogeom_bail!(Construction, "a profile is a wire or a face");
}
let mut edges = ogeom_topo::explore(model, profile, Filter::OfType(ShapeType::Edge))?;
let closed = ogeom_algo::is_wire_closed(model, profile, tol)?;
if !closed {
let Some(plane) = against else {
ogeom_bail!(
Construction,
"an open profile sweeps a shell, not a volume; give the spine \
as a planar face for its plane to close the profile against, \
or close the profile itself"
);
};
let [(from, v0), (to, v1)] = wire_ends(model, profile, tol)?;
for end in [from, to] {
if plane.signed_distance_to(end).abs() > tol.confusion() * 1e2 {
ogeom_bail!(
Construction,
"an open profile is closed against the spine face's own \
plane, and this one does not reach it"
);
}
}
let line = LineCurve::new(ogeom_math::Axis {
location: from,
direction: Direction::new(to - from, tol)?,
});
edges.push(
make_edge_between(model, line.into(), (0.0, from.distance(to)), &v0, &v1, tol)?.shape,
);
}
let ordered = ogeom_algo::order_edges(model, &edges, tol)?;
let mut bound = ogeom_math::Aabb::EMPTY;
for edge in &ordered {
bound = bound.union(&ogeom_algo::shape_bounds(model, edge, tol)?);
}
let Some(centre) = bound.centre() else {
ogeom_bail!(Construction, "a profile with no extent sweeps nothing");
};
let reach = bound.diagonal().mul_add(2.0, 1.0);
let plane = Plane::through(centre, Direction::new(profile_normal, tol)?);
let surface = PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?;
Ok(make_face_with_pcurves(model, surface.into(), &[ordered], tol)?.shape)
}
fn wire_ends(model: &Model, wire: &Shape, tol: Tolerances) -> OgeomResult<[(Point, Shape); 2]> {
let mut counts: Vec<(Point, Shape, usize)> = Vec::new();
for edge in explore(model, wire, Filter::OfType(ShapeType::Edge))? {
for v in explore(model, &edge, Filter::OfType(ShapeType::Vertex))? {
let Some(data) = model.node(&v).and_then(|n| n.data().as_vertex()) else {
continue;
};
let at = v.transform(model.datums())?.apply(data.point);
match counts
.iter_mut()
.find(|(p, _, _)| p.distance(at) <= tol.confusion() * 10.0)
{
Some((_, _, n)) => *n += 1,
None => counts.push((at, v.clone(), 1)),
}
}
}
let free: Vec<(Point, Shape)> = counts
.into_iter()
.filter(|(_, _, n)| *n == 1)
.map(|(p, v, _)| (p, v))
.collect();
if free.len() != 2 {
ogeom_bail!(
Construction,
"an open profile has exactly two ends; this one has {}",
free.len()
);
}
let mut ends = free.into_iter();
let (Some(a), Some(b)) = (ends.next(), ends.next()) else {
ogeom_bail!(Construction, "the profile lost an end between checks");
};
Ok([a, b])
}
fn spine_stations(model: &Model, wire: &Shape, tol: Tolerances) -> OgeomResult<Vec<Station>> {
let mut out = Vec::new();
for edge in explore(model, wire, Filter::OfType(ShapeType::Edge))? {
let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
ogeom_bail!(Construction, "a spine edge is not in this model");
};
let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
ogeom_bail!(Construction, "a spine edge with no curve runs nowhere");
};
let Some(geometry) = model.geometry().curve(*curve) else {
ogeom_bail!(Dangling, "curve is not in this model");
};
let placed = geometry.transformed(&edge.transform(model.datums())?, tol)?;
let reversed = edge.orientation() == ogeom_topo::Orientation::Reversed;
let (t0, t1) = if reversed {
(range.1, range.0)
} else {
(range.0, range.1)
};
let sign = if reversed { -1.0 } else { 1.0 };
let unit = |t: f64| -> OgeomResult<Vector> {
let d = placed.d1_at(t, tol)? * sign;
if d.magnitude() <= tol.confusion() {
ogeom_bail!(Construction, "a spine edge has no direction at {t}");
}
Ok(d / d.magnitude())
};
let station = match &placed {
Curve::Line(_) => Station {
from: placed.point_at(t0, tol)?,
to: placed.point_at(t1, tol)?,
tangent_in: unit(t0)?,
tangent_out: unit(t1)?,
turn: None,
},
Curve::Circle(c) => {
let circle = c.circle();
let swept = (range.1 - range.0).abs();
let axis = ogeom_math::Axis {
location: circle.centre(),
direction: if reversed {
-circle.frame().z()
} else {
circle.frame().z()
},
};
Station {
from: placed.point_at(t0, tol)?,
to: placed.point_at(t1, tol)?,
tangent_in: unit(t0)?,
tangent_out: unit(t1)?,
turn: Some((axis, swept)),
}
}
other => ogeom_bail!(
Construction,
"a spine runs on straight and circular edges; a {:?} sweeps a \
surface this construction does not have",
other.kind()
),
};
out.push(station);
}
Ok(out)
}
fn spine_normal(stations: &[Station], tol: Tolerances) -> OgeomResult<Direction> {
let mut best: Option<(f64, Vector)> = None;
let mut consider = |a: Vector, b: Vector| {
let cross = a.cross(b);
let magnitude = cross.magnitude();
if magnitude > best.map_or(tol.angular(), |(m, _)| m) {
best = Some((magnitude, cross / magnitude));
}
};
for (index, station) in stations.iter().enumerate() {
consider(station.tangent_in, station.tangent_out);
if index + 1 < stations.len() {
consider(station.tangent_out, stations[index + 1].tangent_in);
}
}
if stations.len() > 1 {
consider(
stations[stations.len() - 1].tangent_out,
stations[0].tangent_in,
);
}
let Some((_, normal)) = best else {
ogeom_bail!(
Construction,
"a spine that never turns has no plane of its own; give the \
profile's own orientation a spine with at least one corner or arc"
);
};
for station in stations {
for tangent in [station.tangent_in, station.tangent_out] {
if tangent.dot(normal).abs() > tol.angular() {
ogeom_bail!(
Construction,
"the spine leaves its own plane; an evolved sweep runs a \
planar spine"
);
}
}
if let Some((axis, _)) = &station.turn
&& axis.direction.vector().cross(normal).magnitude() > tol.angular()
{
ogeom_bail!(
Construction,
"a spine arc turns about an axis off the spine's own normal"
);
}
}
Direction::new(normal, tol)
}
fn profile_plane(model: &Model, profile: &Shape, tol: Tolerances) -> OgeomResult<(Point, Vector)> {
let mut points: Vec<Point> = Vec::new();
for edge in explore(model, profile, Filter::OfType(ShapeType::Edge))? {
let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
continue;
};
let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
continue;
};
let Some(geometry) = model.geometry().curve(*curve) else {
ogeom_bail!(Dangling, "curve is not in this model");
};
let placed = geometry.transformed(&edge.transform(model.datums())?, tol)?;
for i in 0..=8 {
let t = range.0 + (range.1 - range.0) * f64::from(i) / 8.0;
points.push(placed.point_at(t, tol)?);
}
}
if points.len() < 3 {
ogeom_bail!(Construction, "a profile needs an extent to sweep");
}
let origin = points[0];
let mut best: Option<(f64, Vector)> = None;
for (i, a) in points.iter().enumerate() {
for b in points.iter().skip(i + 1) {
let cross = (*a - origin).cross(*b - origin);
let magnitude = cross.magnitude();
if magnitude > best.map_or(tol.confusion(), |(m, _)| m) {
best = Some((magnitude, cross / magnitude));
}
}
}
let Some((_, normal)) = best else {
ogeom_bail!(Construction, "a profile with no area has no plane");
};
for p in &points {
if (*p - origin).dot(normal).abs() > tol.confusion() * 1e2 {
ogeom_bail!(Construction, "the profile is not planar");
}
}
Ok((origin, normal))
}
fn station_transform(
reference: (Point, Vector),
at: Point,
tangent: Vector,
normal: Direction,
tol: Tolerances,
) -> OgeomResult<Transform> {
let (origin, from) = reference;
let n = normal.vector();
let angle = from.cross(tangent).dot(n).atan2(from.dot(tangent));
let turn = if angle.abs() <= tol.angular() {
Transform::IDENTITY
} else {
Transform::rotation(
ogeom_math::Axis {
location: origin,
direction: normal,
},
angle,
)
};
Ok(Transform::translation(at - origin) * turn)
}
#[allow(clippy::too_many_arguments)]
fn corner_piece(
model: &mut Model,
profile: &Shape,
reference: (Point, Vector),
corner: Point,
incoming: Vector,
outgoing: Vector,
normal: Direction,
tol: Tolerances,
) -> OgeomResult<Option<Shape>> {
let n = normal.vector();
let angle = incoming
.cross(outgoing)
.dot(n)
.atan2(incoming.dot(outgoing));
if angle.abs() <= tol.angular() {
return Ok(None);
}
let placed = ogeom_algo::transformed(
model,
profile,
station_transform(reference, corner, incoming, normal, tol)?,
)?
.shape;
let axis = ogeom_math::Axis {
location: corner,
direction: if angle > 0.0 { normal } else { -normal },
};
let turned = ogeom_algo::make_revolution(model, &placed, axis, angle.abs(), tol);
match turned {
Ok(built) => Ok(Some(built.shape)),
Err(_) => ogeom_bail!(
NotDone,
"the profile straddles the spine at a corner, so turning it about \
that corner sweeps it through itself; there is no revolution for \
that wedge"
),
}
}