use ogeom_algo::{
Built, History, attach_pcurve, attach_seam, edge_vertices, make_edge_between, make_face_on,
make_face_with_pcurves, make_shell, make_vertex, make_wire,
};
use ogeom_core::{OgeomResult, Tolerances, ogeom_bail, ogeom_err};
use ogeom_geom::Curve2d as _;
use ogeom_geom::Curve3d as _;
use ogeom_geom::Surface as _;
use ogeom_geom::Transformable as _;
use ogeom_geom::{
BSpline2d, BSplineCurve, BSplineSurface, Curve, Line2d, LineCurve, PlanarCurve, PlaneSurface,
SurfaceGeometry,
};
use ogeom_math::Blend as _;
use ogeom_math::{
Axis2, ControlGrid, Direction, Direction2, Frame, KnotVector, Plane, Point, Point2, Transform,
TransformKind, Vector, Weighted,
};
use ogeom_topo::{Location, Model, Orientation, Shape, ShapeType};
use crate::sweep::{PipeLaw, SpineStation, law_normals, spine_curve_of, station_frame};
mod guided;
const KNOT_SAME: f64 = 1e-12;
const MOST_SWEEP_SECTIONS: usize = 1025;
pub fn make_ruled(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
make_loft_surface(model, &[a.clone(), b.clone()], false, &[], true, tol)
}
pub fn make_loft_surface(
model: &mut Model,
sections: &[Shape],
closed: bool,
guides: &[Shape],
ruled: bool,
tol: Tolerances,
) -> OgeomResult<Built> {
if !guides.is_empty() {
return guided::guided_loft(model, sections, closed, guides, ruled, tol);
}
let least = if closed { 3 } else { 2 };
if sections.len() < least {
ogeom_bail!(
Construction,
"a {}loft surface needs at least {least} sections, given {}",
if closed { "closed " } else { "" },
sections.len()
);
}
let mut read: Vec<Section> = Vec::with_capacity(sections.len());
for shape in sections {
read.push(read_section(model, shape, "loft section", tol)?);
}
let count = read[0].edges.len();
let closed_u = read[0].closed;
for (k, s) in read.iter().enumerate() {
if s.edges.len() != count {
ogeom_bail!(
Construction,
"loft sections pair edge for edge; section 0 has {count} edges and section \
{k} has {}",
s.edges.len()
);
}
if s.closed != closed_u {
ogeom_bail!(
Construction,
"loft sections are all closed or all open; section {k} is {} and section 0 \
is not",
if s.closed { "closed" } else { "open" }
);
}
}
for e in 0..count {
let curves: Vec<BSplineCurve> = read.iter().map(|s| s.edges[e].curve.clone()).collect();
let matched = made_compatible(&curves, tol)?;
for (s, c) in read.iter_mut().zip(matched) {
s.edges[e].curve = c;
}
}
let n = read.len();
let mut chords = Vec::with_capacity(n);
for k in 0..n - 1 {
chords.push(section_chord(&read[k], &read[k + 1], k, tol)?);
}
if closed {
chords.push(section_chord(&read[n - 1], &read[0], n - 1, tol)?);
}
let (spans, surfaces, seam_v) = if ruled {
let mut spans: Vec<(usize, usize)> = (0..n - 1).map(|k| (k, k + 1)).collect();
if closed {
spans.push((n - 1, 0));
}
let mut surfaces = Vec::with_capacity(count);
for e in 0..count {
let mut per_span = Vec::with_capacity(spans.len());
for &(lo, hi) in &spans {
let rows = vec![
read[lo].edges[e].curve.control_points().to_vec(),
read[hi].edges[e].curve.control_points().to_vec(),
];
let v_knots = KnotVector::clamped_uniform(1, 2)?;
per_span.push(surface_of(
read[lo].edges[e].curve.knots(),
v_knots,
&rows,
tol,
)?);
}
surfaces.push(per_span);
}
(spans, surfaces, false)
} else if closed {
let mut surfaces = Vec::with_capacity(count);
for e in 0..count {
let rows: Vec<Vec<Weighted<Point>>> = read
.iter()
.map(|s| s.edges[e].curve.control_points().to_vec())
.collect();
let (v_knots, net) = interpolated_closed(&rows, tol)?;
surfaces.push(vec![surface_of(
read[0].edges[e].curve.knots(),
v_knots,
&net,
tol,
)?]);
}
for edge in &mut read[0].edges {
edge.edge = None;
}
(vec![(0, 0)], surfaces, true)
} else {
let params = unit_params(&chords);
let mut surfaces = Vec::with_capacity(count);
for e in 0..count {
let rows: Vec<Vec<Weighted<Point>>> = read
.iter()
.map(|s| s.edges[e].curve.control_points().to_vec())
.collect();
let (v_knots, net) = interpolated(&rows, ¶ms, tol)?;
surfaces.push(vec![surface_of(
read[0].edges[e].curve.knots(),
v_knots,
&net,
tol,
)?]);
}
(vec![(0, n - 1)], surfaces, false)
};
let shape = sheet(model, &read, &surfaces, &spans, seam_v, ruled, tol)?;
let mut history = History::new();
for section in sections {
history.generate(section, shape.clone());
}
Ok(Built { shape, history })
}
pub fn make_sweep_surface(
model: &mut Model,
profile: &Shape,
spine: &Shape,
law: &PipeLaw<'_>,
tol: Tolerances,
) -> OgeomResult<Built> {
let section = read_section(model, profile, "sweep profile", tol)?;
let target = tol.confusion() * 10.0;
let motions = |model: &Model, density: usize| -> OgeomResult<Vec<Transform>> {
let stations = sweep_stations(model, spine, density, tol)?;
if matches!(law, PipeLaw::Fixed) {
let start = stations[0].at;
return Ok(stations
.iter()
.map(|s| Transform::translation(s.at - start))
.collect());
}
let normals = law_normals(model, &stations, law, target, tol)?;
let start = station_frame(&stations[0], normals[0], tol)?;
let mut out = Vec::with_capacity(stations.len());
for (s, n) in stations.iter().zip(&normals) {
let frame = station_frame(s, *n, tol)?;
out.push(Transform::from_frame(&frame) * Transform::to_frame(&start));
}
Ok(out)
};
let shape = swept_sheet(model, §ion, motions, target, tol)?;
let mut history = History::new();
history.generate(profile, shape.clone());
history.generate(spine, shape.clone());
if let PipeLaw::Auxiliary { guide } = law {
history.generate(guide, shape.clone());
}
Ok(Built { shape, history })
}
pub fn make_sweep_two_rails(
model: &mut Model,
profile: &Shape,
rail_a: &Shape,
rail_b: &Shape,
tol: Tolerances,
) -> OgeomResult<Built> {
let section = read_section(model, profile, "sweep profile", tol)?;
if section.closed {
ogeom_bail!(
Construction,
"a two-rail profile runs from one rail to the other; a closed profile has one end"
);
}
let start = section.edges[0].curve.point_at(0.0, tol)?;
let end = section.edges[section.edges.len() - 1]
.curve
.point_at(1.0, tol)?;
let mut first = Rail::read(model, rail_a, tol)?;
let mut second = Rail::read(model, rail_b, tol)?;
let (a0, b0) = (first.at(0.0, tol)?.0, second.at(0.0, tol)?.0);
let near = tol.confusion() * 10.0;
if start.distance(b0) <= near && end.distance(a0) <= near {
core::mem::swap(&mut first, &mut second);
}
let (a0, b0) = (first.at(0.0, tol)?.0, second.at(0.0, tol)?.0);
if start.distance(a0) > near || end.distance(b0) > near {
ogeom_bail!(
Construction,
"the profile's ends must sit on the rails' starts; they are {:.3e} and {:.3e} away",
start.distance(a0),
end.distance(b0)
);
}
let frame_at = |f: f64| -> OgeomResult<(Frame, f64)> {
let (a, ta) = first.at(f, tol)?;
let (b, tb) = second.at(f, tol)?;
let chord = b - a;
let width = chord.magnitude();
if width <= tol.confusion() {
ogeom_bail!(
Construction,
"the rails meet at {a:?}; a profile between them has no width there"
);
}
let x = chord / width;
let mean = ta + tb;
let along = mean - x * mean.dot(x);
if along.magnitude() <= 1e-6 * mean.magnitude().max(tol.confusion()) {
ogeom_bail!(
Construction,
"the rails' mean direction runs along the chord between them at {a:?}"
);
}
Ok((
Frame::new(a, Direction::new(along, tol)?, Direction::new(x, tol)?, tol)?,
width,
))
};
let (start_frame, start_width) = frame_at(0.0)?;
let target = tol.confusion() * 10.0;
let motions = |_: &Model, density: usize| -> OgeomResult<Vec<Transform>> {
let count = 32 * density;
let mut out = Vec::with_capacity(count + 1);
for i in 0..=count {
#[allow(clippy::cast_precision_loss)]
let f = i as f64 / count as f64;
let (frame, width) = frame_at(f)?;
let scale = Transform::scaling(Point::ORIGIN, width / start_width, tol)?;
out.push(Transform::from_frame(&frame) * scale * Transform::to_frame(&start_frame));
}
Ok(out)
};
let shape = swept_sheet(model, §ion, motions, target, tol)?;
let mut history = History::new();
for input in [profile, rail_a, rail_b] {
history.generate(input, shape.clone());
}
Ok(Built { shape, history })
}
#[derive(Clone)]
struct SectionEdge {
edge: Option<Shape>,
curve: BSplineCurve,
paced: bool,
}
#[derive(Clone)]
struct Section {
edges: Vec<SectionEdge>,
closed: bool,
}
fn read_section(model: &Model, shape: &Shape, what: &str, tol: Tolerances) -> OgeomResult<Section> {
let edges = match model.kind_of(shape)? {
ShapeType::Edge => vec![shape.clone()],
ShapeType::Wire => model.ordered_children_of(shape)?,
other => ogeom_bail!(
Construction,
"a {what} is an edge or a wire, not a {other:?}"
),
};
if edges.is_empty() {
ogeom_bail!(Construction, "the {what} has no edges");
}
let mut adoptable = true;
let mut out = Vec::with_capacity(edges.len());
for edge in &edges {
let (curve, range) = spine_curve_of(model, edge)?;
let placement = edge.transform(model.datums())?;
if placement.kind() != TransformKind::Identity {
adoptable = false;
}
let exact = curve.to_bspline_over(range, tol)?;
let exact = moved(&exact, &placement, tol)?;
let exact = if edge.orientation() == Orientation::Reversed {
let (knots, control) =
ogeom_math::bspline::reverse(exact.knots(), exact.control_points());
BSplineCurve::rational(knots, control)?
} else {
exact
};
out.push(SectionEdge {
edge: Some(edge.clone()),
curve: standard(&exact)?,
paced: true,
});
}
let head = out[0].curve.point_at(0.0, tol)?;
let tail = out[out.len() - 1].curve.point_at(1.0, tol)?;
let closed = head.distance(tail) <= tol.confusion();
if adoptable {
let mut ends = Vec::with_capacity(edges.len());
for edge in &edges {
match edge_vertices(model, edge)? {
Some(pair) => ends.push(pair),
None => adoptable = false,
}
}
if adoptable {
adoptable = ends.windows(2).all(|w| w[0].1.is_partner(&w[1].0))
&& (!closed || ends[ends.len() - 1].1.is_partner(&ends[0].0));
}
}
if !adoptable {
for e in &mut out {
e.edge = None;
}
}
Ok(Section { edges: out, closed })
}
fn moved(curve: &BSplineCurve, motion: &Transform, tol: Tolerances) -> OgeomResult<BSplineCurve> {
if motion.kind() == TransformKind::Identity {
return Ok(curve.clone());
}
let control = curve
.control_points()
.iter()
.map(|w| Weighted::new(motion.apply(w.point()), w.weight, tol))
.collect::<OgeomResult<Vec<_>>>()?;
BSplineCurve::rational(curve.knots().clone(), control)
}
fn standard(curve: &BSplineCurve) -> OgeomResult<BSplineCurve> {
let knots = curve.knots().reparameterized(0.0, 1.0)?;
let first = curve.control_points()[0].weight;
let control = curve
.control_points()
.iter()
.map(|w| w.scale(1.0 / first))
.collect();
BSplineCurve::rational(knots, control)
}
fn made_compatible(curves: &[BSplineCurve], tol: Tolerances) -> OgeomResult<Vec<BSplineCurve>> {
let degree = curves.iter().map(BSplineCurve::degree).max().unwrap_or(1);
let mut raised = Vec::with_capacity(curves.len());
for c in curves {
let mut c = c.clone();
while c.degree() < degree {
c = c.elevated(tol)?;
}
raised.push(c);
}
let interior = |c: &BSplineCurve| -> Vec<(f64, usize)> {
c.knots()
.distinct()
.into_iter()
.filter(|(v, _)| *v > KNOT_SAME && *v < 1.0 - KNOT_SAME)
.collect()
};
let mut union: Vec<(f64, usize)> = Vec::new();
for c in &raised {
for (value, mult) in interior(c) {
match union
.iter_mut()
.find(|(v, _)| (*v - value).abs() <= KNOT_SAME)
{
Some(entry) => entry.1 = entry.1.max(mult),
None => union.push((value, mult)),
}
}
}
for c in &mut raised {
for &(value, mult) in &union {
let have = interior(c)
.iter()
.find(|(v, _)| (*v - value).abs() <= KNOT_SAME)
.map_or(0, |entry| entry.1);
if have < mult {
*c = c.with_knot_inserted(value, mult - have, tol)?;
}
}
}
let knots = raised[0].knots().clone();
for c in &raised {
let same = c.knots().knots().len() == knots.knots().len()
&& c.knots()
.knots()
.iter()
.zip(knots.knots())
.all(|(a, b)| (a - b).abs() <= KNOT_SAME * 10.0);
if !same {
ogeom_bail!(Construction, "the sections' knots could not be matched");
}
}
raised
.iter()
.map(|c| BSplineCurve::rational(knots.clone(), c.control_points().to_vec()))
.collect()
}
fn section_chord(a: &Section, b: &Section, index: usize, tol: Tolerances) -> OgeomResult<f64> {
let mut sum = 0.0;
let mut count = 0.0;
for (ea, eb) in a.edges.iter().zip(&b.edges) {
for (p, q) in ea
.curve
.control_points()
.iter()
.zip(eb.curve.control_points())
{
sum += p.point().distance(q.point());
count += 1.0;
}
}
let chord = sum / count;
if chord <= tol.confusion() {
ogeom_bail!(
Construction,
"sections {index} and {} coincide; a skin between them has no extent",
index + 1
);
}
Ok(chord)
}
fn unit_params(chords: &[f64]) -> Vec<f64> {
let total: f64 = chords.iter().sum();
let mut params = Vec::with_capacity(chords.len() + 1);
let mut run = 0.0;
params.push(0.0);
for c in chords {
run += c;
params.push(run / total);
}
let last = params.len() - 1;
params[last] = 1.0;
params
}
fn interpolated(
rows: &[Vec<Weighted<Point>>],
params: &[f64],
tol: Tolerances,
) -> OgeomResult<(KnotVector, Vec<Vec<Weighted<Point>>>)> {
let n = rows.len();
let degree = (n - 1).min(3);
let knots = KnotVector::averaged(degree, params)?;
let mut matrix = vec![vec![0.0; n]; n];
for (k, v) in params.iter().enumerate() {
let span = knots.span(*v, tol)?;
for (b, j) in knots.basis(span, *v).iter().zip(span - degree..=span) {
matrix[k][j] = *b;
}
}
let inverse = inverted(matrix)?;
Ok((knots, combined(&inverse, rows)))
}
fn interpolated_closed(
rows: &[Vec<Weighted<Point>>],
tol: Tolerances,
) -> OgeomResult<(KnotVector, Vec<Vec<Weighted<Point>>>)> {
let n = rows.len();
let degree = (n - 1).min(3);
let shift = if degree.is_multiple_of(2) { 0.5 } else { 0.0 };
let count = n + degree + 2;
#[allow(clippy::cast_precision_loss)]
let knots = KnotVector::new((0..=count + degree).map(|i| i as f64).collect(), degree)?;
let ring = |j: usize| (j + n - 1) % n;
#[allow(clippy::cast_precision_loss)]
let at = |k: usize| (degree + 1 + k) as f64 + shift;
let mut matrix = vec![vec![0.0; n]; n];
for (k, row) in matrix.iter_mut().enumerate() {
let v = at(k);
let span = knots.span(v, tol)?;
for (b, j) in knots.basis(span, v).iter().zip(span - degree..=span) {
row[ring(j)] += *b;
}
}
let inverse = inverted(matrix)?;
let solved = combined(&inverse, rows);
let wrapped: Vec<Vec<Weighted<Point>>> = (0..count).map(|j| solved[ring(j)].clone()).collect();
#[allow(clippy::cast_precision_loss)]
let (from, to) = (at(0), at(n));
let width = rows[0].len();
let mut columns: Vec<Vec<Weighted<Point>>> = Vec::with_capacity(width);
let mut cut_knots: Option<KnotVector> = None;
for i in 0..width {
let column: Vec<Weighted<Point>> = wrapped.iter().map(|r| r[i]).collect();
let (_, (k1, c1)) = ogeom_math::bspline::split(&knots, &column, from, tol)?;
let ((k2, c2), _) = ogeom_math::bspline::split(&k1, &c1, to, tol)?;
cut_knots = Some(k2);
columns.push(c2);
}
let Some(cut_knots) = cut_knots else {
ogeom_bail!(Construction, "a section has no control points");
};
let l = columns[0].len();
let mut net: Vec<Vec<Weighted<Point>>> = (0..l)
.map(|j| columns.iter().map(|c| c[j]).collect())
.collect();
net[l - 1] = net[0].clone();
Ok((cut_knots.reparameterized(0.0, 1.0)?, net))
}
fn combined(inverse: &[Vec<f64>], rows: &[Vec<Weighted<Point>>]) -> Vec<Vec<Weighted<Point>>> {
let width = rows[0].len();
inverse
.iter()
.map(|line| {
(0..width)
.map(|i| {
line.iter()
.zip(rows)
.fold(Weighted::<Point>::zero(), |acc, (a, row)| {
acc.add(row[i].scale(*a))
})
})
.collect()
})
.collect()
}
fn inverted(mut a: Vec<Vec<f64>>) -> OgeomResult<Vec<Vec<f64>>> {
let n = a.len();
let mut inv: Vec<Vec<f64>> = (0..n)
.map(|i| (0..n).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
.collect();
for col in 0..n {
let pivot = (col..n)
.max_by(|&x, &y| a[x][col].abs().total_cmp(&a[y][col].abs()))
.unwrap_or(col);
if a[pivot][col].abs() <= 1e-12 {
ogeom_bail!(Numeric, "the skin's interpolation system is singular");
}
a.swap(col, pivot);
inv.swap(col, pivot);
let d = a[col][col];
for j in 0..n {
a[col][j] /= d;
inv[col][j] /= d;
}
for r in 0..n {
if r == col {
continue;
}
let f = a[r][col];
if f == 0.0 {
continue;
}
for j in 0..n {
a[r][j] -= f * a[col][j];
inv[r][j] -= f * inv[col][j];
}
}
}
Ok(inv)
}
fn surface_of(
u_knots: &KnotVector,
v_knots: KnotVector,
rows: &[Vec<Weighted<Point>>],
tol: Tolerances,
) -> OgeomResult<BSplineSurface> {
let (k, l) = (rows[0].len(), rows.len());
let mut points = Vec::with_capacity(k * l);
for i in 0..k {
for row in rows {
let w = row[i];
if !w.weight.is_finite() || w.weight <= tol.confusion() {
ogeom_bail!(
Construction,
"the skin's weights fall to {} between the sections; sections this unlike \
cannot be skinned exactly",
w.weight
);
}
points.push(w);
}
}
BSplineSurface::rational(u_knots.clone(), v_knots, ControlGrid::new(points, k, l)?)
}
struct Side {
edge: Shape,
image: PlanarCurve,
range: (f64, f64),
}
#[allow(clippy::too_many_lines, reason = "one assembly, spelled out")]
fn sheet(
model: &mut Model,
sections: &[Section],
surfaces: &[Vec<BSplineSurface>],
spans: &[(usize, usize)],
seam_v: bool,
ruled: bool,
tol: Tolerances,
) -> OgeomResult<Shape> {
let count = sections[0].edges.len();
let closed_u = sections[0].closed;
let joints = if closed_u { count } else { count + 1 };
let end_joint = |e: usize| if closed_u { (e + 1) % count } else { e + 1 };
for j in 0..joints {
let (before, after) = if closed_u {
((j + count - 1) % count, j)
} else if j == 0 || j == count {
continue;
} else {
(j - 1, j)
};
let ratio = |s: &Section| {
let end = s.edges[before].curve.control_points();
let start = s.edges[after].curve.control_points();
end[end.len() - 1].weight / start[0].weight
};
let first = ratio(§ions[0]);
if sections
.iter()
.any(|s| (ratio(s) - first).abs() > 1e-9 * first.abs())
{
ogeom_bail!(
Construction,
"neighbouring section edges carry weights at their shared corner {j} that \
change from section to section; their skins would part"
);
}
}
let mut bounding: Vec<usize> = spans.iter().flat_map(|&(a, b)| [a, b]).collect();
bounding.sort_unstable();
bounding.dedup();
let mut vertices: Vec<Option<Vec<Shape>>> = vec![None; sections.len()];
let mut borders: Vec<Option<Vec<(Shape, bool)>>> = vec![None; sections.len()];
for &k in &bounding {
let section = §ions[k];
let adopted = section.edges.iter().all(|e| e.edge.is_some());
let mut joint_vertices = Vec::with_capacity(joints);
let mut edges = Vec::with_capacity(count);
if adopted {
for e in §ion.edges {
let Some(edge) = &e.edge else {
ogeom_bail!(Construction, "an adopted section lost an edge");
};
let Some((start, _)) = edge_vertices(model, edge)? else {
ogeom_bail!(Construction, "a section edge has no vertices");
};
joint_vertices.push(start);
edges.push((edge.clone(), true));
}
if !closed_u {
let Some(last) = §ion.edges[count - 1].edge else {
ogeom_bail!(Construction, "an adopted section lost an edge");
};
let Some((_, end)) = edge_vertices(model, last)? else {
ogeom_bail!(Construction, "a section edge has no vertices");
};
joint_vertices.push(end);
}
} else {
for e in §ion.edges {
let at = e.curve.point_at(0.0, tol)?;
joint_vertices.push(make_vertex(model, at).shape);
}
if !closed_u {
let at = section.edges[count - 1].curve.point_at(1.0, tol)?;
joint_vertices.push(make_vertex(model, at).shape);
}
for (e, piece) in section.edges.iter().enumerate() {
let edge = make_edge_between(
model,
Curve::BSpline(piece.curve.clone()),
(0.0, 1.0),
&joint_vertices[e],
&joint_vertices[end_joint(e)],
tol,
)?
.shape;
edges.push((edge, false));
}
}
vertices[k] = Some(joint_vertices);
borders[k] = Some(edges);
}
let vertex = |k: usize, j: usize| -> OgeomResult<Shape> {
vertices[k]
.as_ref()
.map(|v| v[j].clone())
.ok_or_else(|| ogeom_err!(Construction, "section {k} bounds no face"))
};
let mut rails: Vec<Vec<(Shape, bool)>> = Vec::with_capacity(spans.len());
for (s, &(lo, hi)) in spans.iter().enumerate() {
let mut per_joint = Vec::with_capacity(joints);
for j in 0..joints {
let (e, u) = if j < count {
(j, 0.0)
} else {
(count - 1, 1.0)
};
let surface = &surfaces[e][s];
let control = sections[lo].edges[e].curve.control_points();
let index = if u == 0.0 { 0 } else { control.len() - 1 };
let w_lo = control[index].weight;
let w_hi = sections[hi].edges[e].curve.control_points()[index].weight;
let (from, to) = (vertex(lo, j)?, vertex(hi, j)?);
if ruled && (w_lo - w_hi).abs() <= 1e-12 * w_lo {
let a = surface.point_at(u, 0.0, tol)?;
let b = surface.point_at(u, 1.0, tol)?;
let line: Curve = LineCurve::segment(a, b, tol)?.into();
let range = line.domain();
let edge = make_edge_between(model, line, range, &from, &to, tol)?.shape;
per_joint.push((edge, true));
} else {
let curve = Curve::BSpline(surface.iso_u_curve(u, tol)?);
let edge = make_edge_between(model, curve, (0.0, 1.0), &from, &to, tol)?.shape;
per_joint.push((edge, false));
}
}
rails.push(per_joint);
}
let column = |u: f64, straight: bool, range: (f64, f64)| -> OgeomResult<PlanarCurve> {
if straight {
let knots = KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
Ok(BSpline2d::new(knots, vec![Point2::new(u, 0.0), Point2::new(u, 1.0)], tol)?.into())
} else {
Ok(Line2d::over(Axis2::new(Point2::new(u, 0.0), Direction2::Y), -1.0, 2.0)?.into())
}
};
let rail_range = |model: &Model, edge: &Shape| -> OgeomResult<(f64, f64)> {
Ok(spine_curve_of(model, edge)?.1)
};
let mut faces = Vec::with_capacity(count * spans.len());
for e in 0..count {
for (s, &(lo, hi)) in spans.iter().enumerate() {
let surface = &surfaces[e][s];
let geometry: SurfaceGeometry = surface.clone().into();
let border = |k: usize| -> OgeomResult<(Shape, bool)> {
borders[k]
.as_ref()
.map(|b| b[e].clone())
.ok_or_else(|| ogeom_err!(Construction, "section {k} bounds no face"))
};
let (bottom, bottom_adopted) = border(lo)?;
let (top, top_adopted) = border(hi)?;
let (rail0, straight0) = rails[s][e].clone();
let (rail1, straight1) = rails[s][end_joint(e)].clone();
if ruled
&& let Some(plane) =
ruled_plane(§ions[lo].edges[e], §ions[hi].edges[e], tol)?
{
let reach = [0.0, 1.0]
.iter()
.flat_map(|u| [(*u, 0.0), (*u, 1.0)])
.map(|(u, v)| {
surface
.point_at(u, v, tol)
.map(|p| p.distance(plane.origin()))
})
.collect::<OgeomResult<Vec<f64>>>()?
.into_iter()
.fold(1.0_f64, f64::max)
* 2.0;
let flat: SurfaceGeometry =
PlaneSurface::over(plane, (-reach, reach), (-reach, reach))?.into();
let face = make_face_with_pcurves(
model,
flat,
&[vec![bottom, rail1, top.reversed(), rail0.reversed()]],
tol,
)?
.shape;
faces.push(face);
continue;
}
let row = |model: &mut Model,
edge: &Shape,
adopted: bool,
v: f64|
-> OgeomResult<Side> {
let (image, range) = if adopted {
let section = §ions[if v == 0.0 { lo } else { hi }].edges[e];
adopted_row(
model,
edge,
§ion.curve,
section.paced,
v,
&geometry,
tol,
)?
} else {
(
Line2d::over(Axis2::new(Point2::new(0.0, v), Direction2::X), -1.0, 2.0)?
.into(),
(0.0, 1.0),
)
};
Ok(Side {
edge: edge.clone(),
image,
range,
})
};
let bottom_side = row(model, &bottom, bottom_adopted, 0.0)?;
let top_side = row(model, &top, top_adopted, 1.0)?;
let range0 = rail_range(model, &rail0)?;
let range1 = rail_range(model, &rail1)?;
let rail0_side = Side {
image: column(0.0, straight0, range0)?,
edge: rail0,
range: range0,
};
let rail1_side = Side {
image: column(1.0, straight1, range1)?,
edge: rail1,
range: range1,
};
debug_assert!(!seam_v || bottom_side.edge.is_partner(&top_side.edge));
faces.push(sheet_face(
model,
geometry,
bottom_side,
rail1_side,
top_side,
rail0_side,
tol,
)?);
}
}
if faces.len() == 1 {
return Ok(faces.swap_remove(0));
}
Ok(make_shell(model, &faces)?.shape)
}
fn ruled_plane(a: &SectionEdge, b: &SectionEdge, tol: Tolerances) -> OgeomResult<Option<Plane>> {
let straight = |c: &BSplineCurve| c.degree() == 1 && c.control_points().len() == 2;
if !straight(&a.curve) || !straight(&b.curve) {
return Ok(None);
}
let (a0, a1) = (a.curve.point_at(0.0, tol)?, a.curve.point_at(1.0, tol)?);
let (b0, b1) = (b.curve.point_at(0.0, tol)?, b.curve.point_at(1.0, tol)?);
let normal = [
(a1 - a0).cross(b0 - a0),
(a1 - a0).cross(b1 - a1),
(b1 - b0).cross(b0 - a0),
]
.into_iter()
.max_by(|x, y| x.magnitude().total_cmp(&y.magnitude()))
.unwrap_or(Vector::new(0.0, 0.0, 0.0));
if normal.magnitude() <= tol.confusion() * (a1 - a0).magnitude().max(1.0) {
return Ok(None);
}
let plane = Plane::through(a0, Direction::new(normal, tol)?);
Ok([a1, b0, b1]
.iter()
.all(|p| plane.distance_to(*p) <= tol.confusion())
.then_some(plane))
}
fn sheet_face(
model: &mut Model,
surface: SurfaceGeometry,
bottom: Side,
rail1: Side,
top: Side,
rail0: Side,
tol: Tolerances,
) -> OgeomResult<Shape> {
let id = model.geometry_mut().add_surface(surface);
let here = Location::identity;
if bottom.edge.is_partner(&top.edge) {
attach_seam(
model,
&bottom.edge,
bottom.image,
top.image,
id,
here(),
bottom.range,
)?;
} else {
attach_pcurve(model, &bottom.edge, bottom.image, id, here(), bottom.range)?;
attach_pcurve(model, &top.edge, top.image, id, here(), top.range)?;
}
if rail0.edge.is_partner(&rail1.edge) {
attach_seam(
model,
&rail1.edge,
rail1.image,
rail0.image,
id,
here(),
rail1.range,
)?;
} else {
attach_pcurve(model, &rail1.edge, rail1.image, id, here(), rail1.range)?;
attach_pcurve(model, &rail0.edge, rail0.image, id, here(), rail0.range)?;
}
let wire = make_wire(
model,
&[
bottom.edge,
rail1.edge,
top.edge.reversed(),
rail0.edge.reversed(),
],
tol,
)?
.shape;
Ok(make_face_on(model, id, &[wire], tol)?.shape)
}
fn adopted_row(
model: &mut Model,
edge: &Shape,
section: &BSplineCurve,
paced: bool,
v: f64,
surface: &SurfaceGeometry,
tol: Tolerances,
) -> OgeomResult<(PlanarCurve, (f64, f64))> {
let (curve, range) = spine_curve_of(model, edge)?;
let reversed = edge.orientation() == Orientation::Reversed;
let even = paced
&& match &curve {
Curve::Line(_) => true,
Curve::BSpline(b) => b.knots().is_clamped(),
_ => false,
};
if even {
let (a, b) = if reversed { (1.0, 0.0) } else { (0.0, 1.0) };
let knots = KnotVector::new(vec![range.0, range.0, range.1, range.1], 1)?;
let image = BSpline2d::new(knots, vec![Point2::new(a, v), Point2::new(b, v)], tol)?;
return Ok((image.into(), range));
}
let u_at = |t: f64, guess: f64| -> OgeomResult<f64> {
foot_on(section, curve.point_at(t, tol)?, guess, tol)
};
let ends = if reversed { (1.0, 0.0) } else { (0.0, 1.0) };
let mut breaks: Vec<(f64, f64)> = vec![(range.0, ends.0)];
let mut inner: Vec<f64> = section
.knots()
.distinct()
.into_iter()
.map(|(k, _)| k)
.filter(|k| *k > KNOT_SAME && *k < 1.0 - KNOT_SAME)
.collect();
if reversed {
inner.reverse();
}
for knot in inner {
let (mut lo, mut hi) = (breaks[breaks.len() - 1].0, range.1);
for _ in 0..80 {
let mid = f64::midpoint(lo, hi);
let below = u_at(mid, knot)? < knot;
if below != reversed {
lo = mid;
} else {
hi = mid;
}
}
breaks.push((f64::midpoint(lo, hi), knot));
}
breaks.push((range.1, ends.1));
let reach: f64 = section
.control_points()
.windows(2)
.map(|w| w[0].point().distance(w[1].point()))
.sum();
let target = tol.confusion() * 0.01 / reach.max(tol.confusion());
const SAMPLES: u32 = 64;
let mut joined: Option<(KnotVector, Vec<Weighted<Point2>>)> = None;
for w in breaks.windows(2) {
let ((ta, ua), (tb, ub)) = (w[0], w[1]);
let mut params = Vec::with_capacity(SAMPLES as usize + 1);
let mut image = Vec::with_capacity(SAMPLES as usize + 1);
for i in 0..=SAMPLES {
let f = f64::from(i) / f64::from(SAMPLES);
let t = ta + (tb - ta) * f;
let u = if i == 0 {
ua
} else if i == SAMPLES {
ub
} else {
u_at(t, ua + (ub - ua) * f)?
};
params.push(t);
image.push(Point2::new(u, v));
}
let fitted = ogeom_geom::fit::fit_points_2d_at(¶ms, &image, 3, target, tol)?;
let mut control = fitted.curve.control_points().to_vec();
let last = control.len() - 1;
control[0] = Weighted::new(Point2::new(ua, v), 1.0, tol)?;
control[last] = Weighted::new(Point2::new(ub, v), 1.0, tol)?;
let piece = (fitted.curve.knots().clone(), control);
joined = Some(match joined {
None => piece,
Some(before) => ogeom_math::bspline::join(&before, &piece)?,
});
}
let Some((knots, control)) = joined else {
ogeom_bail!(Construction, "a section edge has no extent");
};
let pcurve: PlanarCurve = BSpline2d::rational(knots, control)?.into();
let mut worst: f64 = 0.0;
for i in 0..=4096 {
let t = range.0 + (range.1 - range.0) * f64::from(i) / 4096.0;
let q = pcurve.point_at(t, tol)?;
let on = surface.point_at(q.x, q.y, tol)?;
worst = worst.max(on.distance(curve.point_at(t, tol)?));
}
if worst > tol.confusion() * 100.0 {
ogeom_bail!(
NotDone,
"a section edge's image on the skin strays {worst:.3e} from the edge"
);
}
if worst > tol.confusion() {
let widened = ogeom_core::Tolerance::new(worst + tol.confusion())?;
model.widen(edge, widened)?;
if let Some((a, b)) = edge_vertices(model, edge)? {
model.widen(&a, widened)?;
model.widen(&b, widened)?;
}
}
Ok((pcurve, range))
}
fn foot_on(section: &BSplineCurve, p: Point, guess: f64, tol: Tolerances) -> OgeomResult<f64> {
let mut u = guess;
for _ in 0..64 {
let c = section.point_at(u, tol)?;
let d = section.d1_at(u, tol)?;
let speed = d.dot(d);
if speed <= f64::MIN_POSITIVE {
break;
}
let next = (u + (p - c).dot(d) / speed).clamp(0.0, 1.0);
let moved = (next - u).abs();
u = next;
if moved <= 1e-15 {
break;
}
}
let off = section.point_at(u, tol)?.distance(p);
if off > tol.confusion() * 10.0 {
ogeom_bail!(
NotDone,
"a section edge's point {p:?} was not found on its exact form ({off:.3e} away)"
);
}
Ok(u)
}
fn swept_sheet(
model: &mut Model,
profile: &Section,
motions: impl Fn(&Model, usize) -> OgeomResult<Vec<Transform>>,
target: f64,
tol: Tolerances,
) -> OgeomResult<Shape> {
let count = profile.edges.len();
let mut density = 1;
let mut reached = (f64::INFINITY, 0);
loop {
let placed = motions(model, density)?;
let skin_count = placed.len().div_ceil(2);
if skin_count > MOST_SWEEP_SECTIONS {
ogeom_bail!(
NotDone,
"the sweep's skin reached {:.3e} against a target of {target:.3e} through {} \
sections",
reached.0,
reached.1
);
}
let mut sections: Vec<Section> = Vec::with_capacity(skin_count);
for (k, motion) in placed.iter().step_by(2).enumerate() {
let mut edges = Vec::with_capacity(count);
for piece in &profile.edges {
edges.push(SectionEdge {
edge: if k == 0 { piece.edge.clone() } else { None },
curve: moved(&piece.curve, motion, tol)?,
paced: piece.paced,
});
}
sections.push(Section {
edges,
closed: profile.closed,
});
}
let mut chords = Vec::with_capacity(skin_count);
for k in 0..skin_count - 1 {
chords.push(section_chord(§ions[k], §ions[k + 1], k, tol)?);
}
let params = unit_params(&chords);
let mut surfaces = Vec::with_capacity(count);
for e in 0..count {
let rows: Vec<Vec<Weighted<Point>>> = sections
.iter()
.map(|s| s.edges[e].curve.control_points().to_vec())
.collect();
let (v_knots, net) = interpolated(&rows, ¶ms, tol)?;
surfaces.push(vec![surface_of(
profile.edges[e].curve.knots(),
v_knots,
&net,
tol,
)?]);
}
let mut worst: f64 = 0.0;
for (e, piece) in profile.edges.iter().enumerate() {
let geometry: SurfaceGeometry = surfaces[e][0].clone().into();
for (h, motion) in placed.iter().enumerate().skip(1).step_by(2) {
let k = h / 2;
let guess_v = f64::midpoint(params[k], params[k + 1]);
for i in 0..=16 {
let u = f64::from(i) / 16.0;
let p = motion.apply(piece.curve.point_at(u, tol)?);
let foot =
ogeom_algo::project_on_surface_from(&geometry, p, (u, guess_v), tol)?;
worst = worst.max(foot.distance);
}
}
}
reached = (worst, skin_count);
if worst <= target {
return sheet(
model,
§ions,
&surfaces,
&[(0, skin_count - 1)],
false,
false,
tol,
);
}
density *= 2;
}
}
fn sweep_stations(
model: &Model,
spine: &Shape,
density: usize,
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 sweep 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 out: Vec<SpineStation> = Vec::new();
for (ei, edge) in edges.iter().enumerate() {
let (curve, range) = spine_curve_of(model, edge)?;
let curve = curve.transformed(&edge.transform(model.datums())?, tol)?;
let reversed = edge.orientation() == Orientation::Reversed;
let mut turning = 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)?;
if d.magnitude() <= tol.confusion() {
continue;
}
let unit = d / d.magnitude();
if let Some(prev) = last {
turning += prev.dot(unit).clamp(-1.0, 1.0).acos();
}
last = Some(unit);
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "a station count, bounded"
)]
let count =
(turning / (core::f64::consts::TAU / 64.0)).ceil().max(8.0) as usize * 2 * density;
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)?;
if d.magnitude() <= tol.confusion() {
ogeom_bail!(Construction, "the spine stands still at {p:?}");
}
let tangent = (if reversed { -d } else { d }) / d.magnitude();
if let Some(prev) = out.last()
&& prev.at.distance(p) <= tol.confusion()
{
if i == 0 && prev.tangent.dot(tangent) >= 1.0 - 1e-12 {
continue;
}
ogeom_bail!(
Construction,
"the spine turns a sharp corner at {p:?}; a sweep surface follows a \
spine whose direction is continuous"
);
}
out.push(SpineStation {
at: p,
tangent,
edge: ei,
t,
});
}
}
if out[0].at.distance(out[out.len() - 1].at) <= tol.confusion() {
ogeom_bail!(
Construction,
"a sweep surface along a closed spine is not built; sweep along an open one"
);
}
Ok(out)
}
struct Rail {
pieces: Vec<(Curve, (f64, f64), bool, f64)>,
total: f64,
}
impl Rail {
fn read(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Self> {
let edges = match model.kind_of(shape)? {
ShapeType::Edge => vec![shape.clone()],
ShapeType::Wire => model.ordered_children_of(shape)?,
other => ogeom_bail!(Construction, "a rail is an edge or a wire, not a {other:?}"),
};
let mut pieces = Vec::with_capacity(edges.len());
let mut total = 0.0;
for edge in &edges {
let (curve, range) = spine_curve_of(model, edge)?;
let curve = curve.transformed(&edge.transform(model.datums())?, tol)?;
let length = ogeom_algo::curve_length(&curve, range, tol)?;
total += length;
pieces.push((
curve,
range,
edge.orientation() == Orientation::Reversed,
length,
));
}
if total <= tol.confusion() {
ogeom_bail!(Construction, "a rail has no length");
}
let rail = Self { pieces, total };
let (head, tail) = (rail.at(0.0, tol)?.0, rail.at(1.0, tol)?.0);
if head.distance(tail) <= tol.confusion() {
ogeom_bail!(
Construction,
"a two-rail sweep runs along open rails; a rail is closed"
);
}
Ok(rail)
}
fn at(&self, f: f64, tol: Tolerances) -> OgeomResult<(Point, Vector)> {
let mut along = self.total * f.clamp(0.0, 1.0);
let last = self.pieces.len() - 1;
for (i, (curve, range, reversed, length)) in self.pieces.iter().enumerate() {
if along > *length && i < last {
along -= length;
continue;
}
let into = along.min(*length);
let t = if *reversed {
ogeom_algo::parameter_at_length(curve, *range, length - into, tol)?
} else {
ogeom_algo::parameter_at_length(curve, *range, into, tol)?
};
let d = curve.d1_at(t, tol)?;
if d.magnitude() <= tol.confusion() {
ogeom_bail!(Construction, "a rail stands still at {t}");
}
let tangent = (if *reversed { -d } else { d }) / d.magnitude();
return Ok((curve.point_at(t, tol)?, tangent));
}
Err(ogeom_err!(Construction, "a rail has no edges"))
}
}