use crate::{make_arc, make_sphere_brep_framed, transform_brep, AffineTransform, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OrRefuse, Vec3, Vec4};
fn frame(name: &str, major: Vec3, minor_hint: Vec3) -> Result<(Vec3, Vec3), String> {
let x = major.normalized().map_err(|_| format!("{name}: the major direction must be non-zero"))?;
let y = minor_hint
.sub(x.scale(minor_hint.dot(x)))
.normalized()
.map_err(|_| format!("{name}: the minor direction must not be parallel to the major one"))?;
Ok((x, y))
}
pub fn make_ellipse_arc(
center: Vec3,
major_direction: Vec3,
minor_hint: Vec3,
semi_major: f64,
semi_minor: f64,
start_angle: f64,
end_angle: f64,
) -> Result<NurbsCurve, String> {
if !(semi_major.is_finite() && semi_minor.is_finite() && semi_major > 0.0 && semi_minor > 0.0) {
return Err("makeEllipse: both semi-axes must be positive".into());
}
let (x, y) = frame("makeEllipse", major_direction, minor_hint)?;
let origin = Vec3::new(0.0, 0.0, 0.0);
let unit = make_arc(origin, Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 1.0, start_angle, end_angle)?;
let poles = unit
.control_points
.iter()
.map(|pole| {
let point = center.add(x.scale(semi_major * pole.x / pole.w)).add(y.scale(semi_minor * pole.y / pole.w));
Vec4::from_point(point, pole.w)
})
.collect();
NurbsCurve::new(unit.degree, unit.knots.clone(), poles)
}
pub fn make_ellipse(center: Vec3, major_direction: Vec3, minor_hint: Vec3, semi_major: f64, semi_minor: f64) -> Result<NurbsCurve, String> {
make_ellipse_arc(center, major_direction, minor_hint, semi_major, semi_minor, 0.0, std::f64::consts::TAU)
}
pub fn make_periodic_bspline(degree: usize, control_points: &[Vec3]) -> Result<NurbsCurve, String> {
let (p, n) = (degree, control_points.len());
if p == 0 {
return Err("makePeriodicBspline: the degree must be at least 1".into());
}
if n < p + 1 {
return Err(format!("makePeriodicBspline: degree {p} needs at least {} control points, got {n}", p + 1));
}
let mut poles: Vec<[f64; 3]> = (0..n + p).map(|index| control_points[index % n]).map(|point| [point.x, point.y, point.z]).collect();
let mut knots: Vec<f64> = (0..=n + 2 * p).map(|index| index as f64).collect();
for at in [p as f64, (n + p) as f64] {
for _ in 1..p {
insert_knot(p, &mut knots, &mut poles, at);
}
}
let first = knots.iter().position(|&knot| knot == p as f64).expect("the start knot");
let last = knots.iter().position(|&knot| knot == (n + p) as f64).expect("the end knot");
let mut clamped = vec![0.0];
clamped.extend(knots[first..last + p].iter().map(|knot| knot - p as f64));
clamped.push(n as f64);
let poles = poles[first - 1..last].iter().map(|pole| Vec4::from_point(Vec3::new(pole[0], pole[1], pole[2]), 1.0)).collect();
NurbsCurve::new(p, clamped, poles)
}
fn insert_knot(degree: usize, knots: &mut Vec<f64>, poles: &mut Vec<[f64; 3]>, at: f64) {
let span = knots.iter().rposition(|&knot| knot <= at).expect("a knot at or before the new one");
let mut refined = Vec::with_capacity(poles.len() + 1);
for index in 0..=poles.len() {
let pole = if index + degree <= span {
poles[index]
} else if index > span {
poles[index - 1]
} else {
let alpha = (at - knots[index]) / (knots[index + degree] - knots[index]);
let before = poles[index - 1];
if alpha > 0.0 {
let after = poles[index];
[0, 1, 2].map(|axis| (1.0 - alpha) * before[axis] + alpha * after[axis])
} else {
before
}
};
refined.push(pole);
}
*poles = refined;
knots.insert(span + 1, at);
}
pub(crate) fn closed_curve_halves(curve: &NurbsCurve) -> Result<Option<Vec<NurbsCurve>>, String> {
let [start, end] = curve.domain()?;
let (from, to) = (curve.evaluate(start)?, curve.evaluate(end)?);
let size = curve
.control_points
.iter()
.map(|pole| Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w).sub(from).length())
.fold(0.0, f64::max);
if size <= 0.0 || from.sub(to).length() > 1e-9 * size {
return Ok(None);
}
let (first, second) = curve.split(0.5 * (start + end))?;
Ok(Some(vec![first, second]))
}
pub(crate) fn extrude_closed_curve(curve: &NurbsCurve, direction: Vec3, distance: f64) -> Result<Option<BrepSolid>, KernelRefusal> {
use crate::{face_from_surface, face_from_surface_trimmed, make_plane, solid_from_sheets, NurbsSurface};
let collect = KernelStage::Collect;
let [start, end] = curve.domain().or_refuse(collect, "extrudeClosed.domain")?;
let points: Vec<Vec3> = curve.control_points.iter().map(|pole| Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)).collect();
let origin = curve.evaluate(start).or_refuse(collect, "extrudeClosed.start")?;
let size = points.iter().map(|point| point.sub(origin).length()).fold(0.0, f64::max);
if size <= 0.0 || origin.sub(curve.evaluate(end).or_refuse(collect, "extrudeClosed.end")?).length() > 1e-9 * size {
return Ok(None);
}
let slide = direction
.normalized()
.map_err(|_| KernelRefusal::input(collect, "direction", "extrudeSolid: direction must be non-zero"))?
.scale(distance);
let far = |from: Vec3, off: Option<Vec3>| {
points.iter().map(|point| point.sub(from)).map(|arm| off.map_or(arm, |axis| arm.sub(axis.scale(arm.dot(axis))))).fold(Vec3::new(0.0, 0.0, 0.0), |best, arm| if arm.length() > best.length() { arm } else { best })
};
let planar = || KernelRefusal::input(collect, "profile_plane", "extrudeSolid: the closed profile curve does not lie in a plane");
let e1 = far(origin, None).normalized().map_err(|_| planar())?;
let e2 = far(origin, Some(e1)).normalized().map_err(|_| planar())?;
let normal = e1.cross(e2);
if points.iter().any(|point| point.sub(origin).dot(normal).abs() > 1e-9 * size) {
return Err(planar());
}
if slide.dot(normal).abs() <= 1e-12 * slide.length() {
return Err(KernelRefusal::input(collect, "direction", "extrudeSolid: direction lies in the profile's plane"));
}
let net = curve.control_points.iter().map(|pole| vec![*pole, Vec4 { x: pole.x + slide.x * pole.w, y: pole.y + slide.y * pole.w, z: pole.z + slide.z * pole.w, w: pole.w }]).collect();
let side = NurbsSurface::new(curve.degree, 1, curve.knots.clone(), vec![0.0, 0.0, 1.0, 1.0], net).or_refuse(collect, "extrudeClosed.side")?;
let reach = |axis: Vec3| points.iter().map(|point| point.sub(origin).dot(axis)).fold((0.0f64, 0.0f64), |(low, high), at| (low.min(at), high.max(at)));
let ((low1, high1), (low2, high2)) = (reach(e1), reach(e2));
let corner = origin.add(e1.scale(low1 - 0.5 * size)).add(e2.scale(low2 - 0.5 * size));
let end_face = |shift: Vec3, v: f64| -> Result<BrepSolid, KernelRefusal> {
let plane = make_plane(corner.add(shift), e1, e2, high1 - low1 + size, high2 - low2 + size).or_refuse(collect, "extrudeClosed.plane")?;
face_from_surface_trimmed(&plane, &[vec![side.iso_curve_v(v).or_refuse(collect, "extrudeClosed.rim")?]])
};
let sheets = [face_from_surface(&side)?, end_face(Vec3::new(0.0, 0.0, 0.0), 0.0)?, end_face(slide, 1.0)?];
solid_from_sheets(&sheets, (1e-8 * size.max(slide.length())).max(1e-7)).map(Some)
}
pub fn make_ellipsoid_brep(center: Vec3, x_direction: Vec3, y_hint: Vec3, semi_axes: [f64; 3]) -> Result<BrepSolid, KernelRefusal> {
if semi_axes.iter().any(|axis| !(axis.is_finite() && *axis > 0.0)) {
return Err(KernelRefusal::input(KernelStage::Collect, "ellipsoid_axes", "makeEllipsoid: the three semi-axes must be positive"));
}
let (x, y) = frame("makeEllipsoid", x_direction, y_hint).map_err(|message| KernelRefusal::input(KernelStage::Collect, "ellipsoid_frame", message))?;
let z = x.cross(y);
let origin = Vec3::new(0.0, 0.0, 0.0);
let ball = make_sphere_brep_framed(origin, 1.0, Vec3::new(0.0, 0.0, 1.0), Some(Vec3::new(1.0, 0.0, 0.0)))
.or_refuse(KernelStage::Collect, "makeEllipsoid.ball")?;
let [a, b, c] = semi_axes;
let (ex, ey, ez) = (x.scale(a), y.scale(b), z.scale(c));
let map = AffineTransform {
elements: [ex.x, ey.x, ez.x, center.x, ex.y, ey.y, ez.y, center.y, ex.z, ey.z, ez.z, center.z, 0.0, 0.0, 0.0, 1.0],
};
transform_brep(&ball, map, false).or_refuse(KernelStage::Collect, "makeEllipsoid.stretch")
}