#![allow(clippy::many_single_char_names)]
use crate::generated::model as m;
use crate::scene::Ctx;
#[derive(Clone, Debug, PartialEq)]
pub struct NurbsCurve {
pub degree: usize,
pub control_points: Vec<[f64; 3]>,
pub weights: Vec<f64>,
pub knots: Vec<f64>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct NurbsSurface {
pub degree_u: usize,
pub degree_v: usize,
pub control_points: Vec<Vec<[f64; 3]>>,
pub weights: Vec<Vec<f64>>,
pub knots_u: Vec<f64>,
pub knots_v: Vec<f64>,
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn normalize(a: [f64; 3]) -> Option<[f64; 3]> {
let n = dot(a, a).sqrt();
(n >= 1e-12).then(|| scale(a, 1.0 / n))
}
fn point_coords(cx: Ctx<'_>, r: &m::CartesianPointRef) -> Option<[f64; 3]> {
let m::CartesianPointRef::CartesianPoint(id) = r else {
return None;
};
let c = &cx.model.cartesian_point_arena.get(id.0).coordinates;
Some([
c.first().copied().unwrap_or(0.0),
c.get(1).copied().unwrap_or(0.0),
c.get(2).copied().unwrap_or(0.0),
])
}
fn dir_ratios(cx: Ctx<'_>, r: &m::DirectionRef) -> Option<[f64; 3]> {
let m::DirectionRef::Direction(id) = r else {
return None;
};
let d = &cx.model.direction_arena.get(id.0).direction_ratios;
Some([
d.first().copied().unwrap_or(0.0),
d.get(1).copied().unwrap_or(0.0),
d.get(2).copied().unwrap_or(0.0),
])
}
const CIRCLE_U: [(f64, f64); 9] = [
(1.0, 0.0),
(1.0, 1.0),
(0.0, 1.0),
(-1.0, 1.0),
(-1.0, 0.0),
(-1.0, -1.0),
(0.0, -1.0),
(1.0, -1.0),
(1.0, 0.0),
];
const CIRCLE_KNOTS: [f64; 12] = [0., 0., 0., 1., 1., 2., 2., 3., 3., 4., 4., 4.];
fn circle_weights() -> Vec<f64> {
let w = std::f64::consts::FRAC_1_SQRT_2;
vec![1.0, w, 1.0, w, 1.0, w, 1.0, w, 1.0]
}
type Frame = ([f64; 3], [f64; 3], [f64; 3], [f64; 3]);
type KnotsRef<'a> = (&'a [f64], &'a [i64]);
fn frame_of(cx: Ctx<'_>, id: m::Axis2Placement3dId) -> Option<Frame> {
let p = cx.model.axis2_placement3d_arena.get(id.0);
let center = point_coords(cx, &p.location)?;
let z = match &p.axis {
Some(a) => normalize(dir_ratios(cx, a)?)?,
None => [0.0, 0.0, 1.0],
};
let x0 = match &p.ref_direction {
Some(d) => dir_ratios(cx, d)?,
None => [1.0, 0.0, 0.0],
};
let x = normalize(sub(x0, scale(z, dot(x0, z))))?;
let y = cross(z, x);
Some((center, x, y, z))
}
fn placement_frame_3d(cx: Ctx<'_>, r: &m::Axis2PlacementRef) -> Option<Frame> {
let m::Axis2PlacementRef::Axis2Placement3d(id) = r else {
return None;
};
frame_of(cx, *id)
}
fn frame_of_3dref(cx: Ctx<'_>, r: &m::Axis2Placement3dRef) -> Option<Frame> {
let m::Axis2Placement3dRef::Axis2Placement3d(id) = r;
frame_of(cx, *id)
}
pub(crate) fn axis1_frame(cx: Ctx<'_>, r: &m::Axis1PlacementRef) -> Option<Frame> {
let m::Axis1PlacementRef::Axis1Placement(id) = r;
let p = cx.model.axis1_placement_arena.get(id.0);
let center = point_coords(cx, &p.location)?;
let z = match &p.axis {
Some(a) => normalize(dir_ratios(cx, a)?)?,
None => [0.0, 0.0, 1.0],
};
let x0 = if z[0].abs() < 0.9 {
[1.0, 0.0, 0.0]
} else {
[0.0, 1.0, 0.0]
};
let x = normalize(sub(x0, scale(z, dot(x0, z))))?;
Some((center, x, cross(z, x), z))
}
fn conic_nurbs(center: [f64; 3], x: [f64; 3], y: [f64; 3], a: f64, b: f64) -> NurbsCurve {
let control_points = CIRCLE_U
.iter()
.map(|&(ux, uy)| add(center, add(scale(x, ux * a), scale(y, uy * b))))
.collect();
NurbsCurve {
degree: 2,
control_points,
weights: circle_weights(),
knots: CIRCLE_KNOTS.to_vec(),
}
}
pub(crate) fn circle_to_nurbs(cx: Ctx<'_>, c: &m::Circle) -> Option<NurbsCurve> {
if let Some((center, x, y, _)) = placement_frame_3d(cx, &c.position) {
Some(conic_nurbs(center, x, y, c.radius, c.radius))
} else {
cx.warn("CIRCLE.to_nurbs: unsupported (2D or degenerate) placement".to_owned());
None
}
}
pub(crate) fn ellipse_to_nurbs(cx: Ctx<'_>, e: &m::Ellipse) -> Option<NurbsCurve> {
if let Some((center, x, y, _)) = placement_frame_3d(cx, &e.position) {
Some(conic_nurbs(center, x, y, e.semi_axis_1, e.semi_axis_2))
} else {
cx.warn("ELLIPSE.to_nurbs: unsupported (2D or degenerate) placement".to_owned());
None
}
}
fn expand_knots(knots: &[f64], mults: &[i64]) -> Vec<f64> {
let mut out = Vec::new();
for (k, mult) in knots.iter().zip(mults) {
for _ in 0..(*mult).max(0) {
out.push(*k);
}
}
out
}
fn nurbs_curve_from(
cx: Ctx<'_>,
degree: i64,
cp: &[m::CartesianPointRef],
knots: &[f64],
mults: &[i64],
weights: Option<&[f64]>,
) -> Option<NurbsCurve> {
let degree = usize::try_from(degree).ok()?;
let control_points: Vec<[f64; 3]> = cp
.iter()
.map(|r| point_coords(cx, r))
.collect::<Option<_>>()?;
let n = control_points.len();
let weights = weights.map_or_else(|| vec![1.0; n], <[f64]>::to_vec);
let knots = expand_knots(knots, mults);
if weights.len() != n || knots.len() != n + degree + 1 {
cx.warn("B_SPLINE_CURVE.to_nurbs: inconsistent weight/knot count".to_owned());
return None;
}
Some(NurbsCurve {
degree,
control_points,
weights,
knots,
})
}
pub(crate) fn bspline_wk_to_nurbs(cx: Ctx<'_>, b: &m::BSplineCurveWithKnots) -> Option<NurbsCurve> {
nurbs_curve_from(
cx,
b.degree,
&b.control_points_list,
&b.knots,
&b.knot_multiplicities,
None,
)
}
#[derive(Clone, Copy)]
pub(crate) enum KnotFamily {
Uniform,
QuasiUniform,
Bezier,
}
#[allow(clippy::cast_precision_loss)] fn family_knots(family: KnotFamily, degree: i64, n_cp: usize) -> Option<(Vec<f64>, Vec<i64>)> {
let d = usize::try_from(degree).ok()?;
if d == 0 || n_cp < d + 1 {
return None;
}
let d_i = i64::try_from(d).ok()?;
Some(match family {
KnotFamily::Uniform => {
let count = n_cp + d + 1;
((0..count).map(|j| j as f64).collect(), vec![1; count])
}
KnotFamily::QuasiUniform => {
let m = n_cp - d;
let mut mults = vec![1; m + 1];
mults[0] = d_i + 1;
mults[m] = d_i + 1;
((0..=m).map(|j| j as f64).collect(), mults)
}
KnotFamily::Bezier => {
if (n_cp - 1) % d != 0 {
return None;
}
let k = (n_cp - 1) / d;
let mut mults = vec![d_i; k + 1];
mults[0] = d_i + 1;
mults[k] = d_i + 1;
((0..=k).map(|j| j as f64).collect(), mults)
}
})
}
pub(crate) fn uniform_family_curve_to_nurbs(
cx: Ctx<'_>,
degree: i64,
cp: &[m::CartesianPointRef],
family: KnotFamily,
) -> Option<NurbsCurve> {
let Some((knots, mults)) = family_knots(family, degree, cp.len()) else {
cx.warn(
"B_SPLINE_CURVE.to_nurbs: control-point count invalid for its knot family".to_owned(),
);
return None;
};
nurbs_curve_from(cx, degree, cp, &knots, &mults, None)
}
pub(crate) fn uniform_family_surface_to_nurbs(
cx: Ctx<'_>,
degrees: (i64, i64),
cp: &[Vec<m::CartesianPointRef>],
family: KnotFamily,
) -> Option<NurbsSurface> {
let n_v = cp.first().map_or(0, Vec::len);
let u = family_knots(family, degrees.0, cp.len());
let v = family_knots(family, degrees.1, n_v);
let (Some((uk, um)), Some((vk, vm))) = (u, v) else {
cx.warn(
"B_SPLINE_SURFACE.to_nurbs: control-grid size invalid for its knot family".to_owned(),
);
return None;
};
nurbs_surface_from(cx, degrees, cp, (&uk, &um), (&vk, &vm), None)
}
pub(crate) fn polyline_to_nurbs(cx: Ctx<'_>, p: &m::Polyline) -> Option<NurbsCurve> {
if p.points.len() < 2 {
cx.warn("POLYLINE.to_nurbs: fewer than two points".to_owned());
return None;
}
let (knots, mults) = family_knots(KnotFamily::QuasiUniform, 1, p.points.len())?;
nurbs_curve_from(cx, 1, &p.points, &knots, &mults, None)
}
pub(crate) fn complex_bspline_curve_to_nurbs(
cx: Ctx<'_>,
parts: &[m::UnitPart],
) -> Option<NurbsCurve> {
let (degree, cp) = parts.iter().find_map(|p| match p {
m::UnitPart::BSplineCurve {
degree,
control_points_list,
..
} => Some((*degree, control_points_list)),
_ => None,
})?;
let explicit = parts.iter().find_map(|p| match p {
m::UnitPart::BSplineCurveWithKnots {
knots,
knot_multiplicities,
..
} => Some((knots, knot_multiplicities)),
_ => None,
});
let derived;
let (knots, mults): (&[f64], &[i64]) = if let Some((k, m)) = explicit {
(k, m)
} else {
let family = parts.iter().find_map(|p| match p {
m::UnitPart::UniformCurve => Some(KnotFamily::Uniform),
m::UnitPart::QuasiUniformCurve => Some(KnotFamily::QuasiUniform),
m::UnitPart::BezierCurve => Some(KnotFamily::Bezier),
_ => None,
})?;
derived = family_knots(family, degree, cp.len())?;
(&derived.0, &derived.1)
};
let weights = parts.iter().find_map(|p| match p {
m::UnitPart::RationalBSplineCurve { weights_data } => Some(weights_data.as_slice()),
_ => None,
});
nurbs_curve_from(cx, degree, cp, knots, mults, weights)
}
fn vector_dir(cx: Ctx<'_>, r: &m::VectorRef) -> Option<[f64; 3]> {
let m::VectorRef::Vector(id) = r else {
return None;
};
let v = cx.model.vector_arena.get(id.0);
Some(scale(dir_ratios(cx, &v.orientation)?, v.magnitude))
}
fn line_trim_point(
cx: Ctx<'_>,
trim: &[m::TrimmingSelectRef],
base: [f64; 3],
dir: [f64; 3],
) -> Option<[f64; 3]> {
trim.iter().find_map(|s| match s {
m::TrimmingSelectRef::CartesianPoint(id) => {
let c = &cx.model.cartesian_point_arena.get(id.0).coordinates;
Some([
c.first().copied().unwrap_or(0.0),
c.get(1).copied().unwrap_or(0.0),
c.get(2).copied().unwrap_or(0.0),
])
}
m::TrimmingSelectRef::ParameterValue(t) => Some(add(base, scale(dir, *t))),
_ => None,
})
}
fn conic_trim_angle(
cx: Ctx<'_>,
trim: &[m::TrimmingSelectRef],
frame: ([f64; 3], [f64; 3], [f64; 3]),
ab: (f64, f64),
factor: f64,
master: m::TrimmingPreference,
) -> Option<f64> {
let (center, x, y) = frame;
let (a, b) = ab;
let cartesian = || {
let p = trim.iter().find_map(|s| match s {
m::TrimmingSelectRef::CartesianPoint(id) => {
let c = &cx.model.cartesian_point_arena.get(id.0).coordinates;
Some([
c.first().copied().unwrap_or(0.0),
c.get(1).copied().unwrap_or(0.0),
c.get(2).copied().unwrap_or(0.0),
])
}
_ => None,
})?;
let d = sub(p, center);
Some((dot(d, y) / b).atan2(dot(d, x) / a))
};
let parameter = || {
trim.iter().find_map(|s| match s {
m::TrimmingSelectRef::ParameterValue(v) => Some(*v * factor),
_ => None,
})
};
match master {
m::TrimmingPreference::Parameter => parameter().or_else(cartesian),
_ => cartesian().or_else(parameter),
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn arc_nurbs(
center: [f64; 3],
x: [f64; 3],
y: [f64; 3],
a: f64,
b: f64,
theta_s: f64,
sweep: f64,
) -> NurbsCurve {
let narcs = ((sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
let dtheta = sweep / narcs as f64;
let w1 = (dtheta.abs() / 2.0).cos();
let world = |lx: f64, ly: f64| add(center, add(scale(x, lx * a), scale(y, ly * b)));
let mut control_points = vec![world(theta_s.cos(), theta_s.sin())];
let mut weights = vec![1.0];
for k in 0..narcs {
let mid = theta_s + dtheta * (k as f64 + 0.5);
control_points.push(world(mid.cos() / w1, mid.sin() / w1));
weights.push(w1);
let end = theta_s + dtheta * (k as f64 + 1.0);
control_points.push(world(end.cos(), end.sin()));
weights.push(1.0);
}
let mut knots = vec![0.0, 0.0, 0.0];
for i in 1..narcs {
knots.push(i as f64);
knots.push(i as f64);
}
let n = narcs as f64;
knots.extend([n, n, n]);
NurbsCurve {
degree: 2,
control_points,
weights,
knots,
}
}
fn arc_from_trims(
cx: Ctx<'_>,
t: &m::TrimmedCurve,
frame: ([f64; 3], [f64; 3], [f64; 3]),
ab: (f64, f64),
) -> Option<NurbsCurve> {
use std::f64::consts::TAU;
let factor = crate::scene::units::units_of(cx.model)
.angle
.map_or(1.0, |u| u.to_si);
let master = t.master_representation;
let theta1 = conic_trim_angle(cx, &t.trim_1, frame, ab, factor, master)?;
let mut theta2 = conic_trim_angle(cx, &t.trim_2, frame, ab, factor, master)?;
if t.sense_agreement {
while theta2 <= theta1 {
theta2 += TAU;
}
} else {
while theta2 >= theta1 {
theta2 -= TAU;
}
}
let (center, x, y) = frame;
let (a, b) = ab;
Some(arc_nurbs(center, x, y, a, b, theta1, theta2 - theta1))
}
pub(crate) fn trimmed_to_nurbs(cx: Ctx<'_>, t: &m::TrimmedCurve) -> Option<NurbsCurve> {
match &t.basis_curve {
m::CurveRef::Line(id) => {
let line = cx.model.line_arena.get(id.0);
let base = point_coords(cx, &line.pnt)?;
let dir = vector_dir(cx, &line.dir)?;
let p0 = line_trim_point(cx, &t.trim_1, base, dir)?;
let p1 = line_trim_point(cx, &t.trim_2, base, dir)?;
Some(NurbsCurve {
degree: 1,
control_points: vec![p0, p1],
weights: vec![1.0, 1.0],
knots: vec![0.0, 0.0, 1.0, 1.0],
})
}
m::CurveRef::Circle(id) => {
let c = cx.model.circle_arena.get(id.0);
let (center, x, y, _) = placement_frame_3d(cx, &c.position)?;
arc_from_trims(cx, t, (center, x, y), (c.radius, c.radius))
}
m::CurveRef::Ellipse(id) => {
let e = cx.model.ellipse_arena.get(id.0);
let (center, x, y, _) = placement_frame_3d(cx, &e.position)?;
arc_from_trims(cx, t, (center, x, y), (e.semi_axis_1, e.semi_axis_2))
}
_ => {
let basis = trimmed_basis_nurbs(cx, &t.basis_curve)?;
trimmed_bspline_to_nurbs(cx, t, &basis)
}
}
}
fn trimmed_basis_nurbs(cx: Ctx<'_>, r: &m::CurveRef) -> Option<NurbsCurve> {
match r {
m::CurveRef::BSplineCurveWithKnots(id) => {
bspline_wk_to_nurbs(cx, cx.model.b_spline_curve_with_knots_arena.get(id.0))
}
m::CurveRef::Complex(id) => {
complex_bspline_curve_to_nurbs(cx, &cx.model.complex_unit_arena.get(id.0).parts)
}
m::CurveRef::UniformCurve(id) => {
let c = cx.model.uniform_curve_arena.get(id.0);
uniform_family_curve_to_nurbs(cx, c.degree, &c.control_points_list, KnotFamily::Uniform)
}
m::CurveRef::QuasiUniformCurve(id) => {
let c = cx.model.quasi_uniform_curve_arena.get(id.0);
uniform_family_curve_to_nurbs(
cx,
c.degree,
&c.control_points_list,
KnotFamily::QuasiUniform,
)
}
m::CurveRef::BezierCurve(id) => {
let c = cx.model.bezier_curve_arena.get(id.0);
uniform_family_curve_to_nurbs(cx, c.degree, &c.control_points_list, KnotFamily::Bezier)
}
m::CurveRef::Polyline(id) => polyline_to_nurbs(cx, cx.model.polyline_arena.get(id.0)),
_ => None,
}
}
struct HomCurve {
degree: usize,
knots: Vec<f64>,
cps: Vec<[f64; 4]>,
dknots: Vec<f64>,
dcps: Vec<[f64; 4]>,
}
fn hom_lerp(a: [f64; 4], b: [f64; 4], t: f64) -> [f64; 4] {
[
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
a[3] + (b[3] - a[3]) * t,
]
}
fn find_span(knots: &[f64], degree: usize, t: f64) -> usize {
let n = knots.len() - degree - 1; if t >= knots[n] {
return n - 1;
}
let mut k = degree;
while k + 1 < n && knots[k + 1] <= t {
k += 1;
}
k
}
fn de_boor(cps: &[[f64; 4]], knots: &[f64], degree: usize, t: f64) -> [f64; 4] {
let k = find_span(knots, degree, t);
let mut d: Vec<[f64; 4]> = (0..=degree).map(|j| cps[k - degree + j]).collect();
for r in 1..=degree {
for j in (r..=degree).rev() {
let i = k - degree + j;
let denom = knots[i + degree + 1 - r] - knots[i];
let a = if denom.abs() < 1e-300 {
0.0
} else {
(t - knots[i]) / denom
};
d[j] = hom_lerp(d[j - 1], d[j], a);
}
}
d[degree]
}
impl HomCurve {
fn new(c: &NurbsCurve) -> Self {
let p = c.degree;
let cps: Vec<[f64; 4]> = c
.control_points
.iter()
.zip(&c.weights)
.map(|(&q, &w)| [q[0] * w, q[1] * w, q[2] * w, w])
.collect();
#[allow(clippy::cast_precision_loss)] let dcps: Vec<[f64; 4]> = cps
.windows(2)
.enumerate()
.map(|(i, w)| {
let denom = c.knots[i + p + 1] - c.knots[i + 1];
let s = if denom.abs() < 1e-300 {
0.0
} else {
p as f64 / denom
};
[
(w[1][0] - w[0][0]) * s,
(w[1][1] - w[0][1]) * s,
(w[1][2] - w[0][2]) * s,
(w[1][3] - w[0][3]) * s,
]
})
.collect();
HomCurve {
degree: p,
knots: c.knots.clone(),
cps,
dknots: c.knots[1..c.knots.len() - 1].to_vec(),
dcps,
}
}
fn domain(&self) -> (f64, f64) {
let n = self.knots.len() - self.degree - 1;
(self.knots[self.degree], self.knots[n])
}
fn point_deriv(&self, t: f64) -> ([f64; 3], [f64; 3]) {
let h = de_boor(&self.cps, &self.knots, self.degree, t);
let hd = de_boor(&self.dcps, &self.dknots, self.degree - 1, t);
let w = h[3];
let point = [h[0] / w, h[1] / w, h[2] / w];
let deriv = [
(hd[0] - point[0] * hd[3]) / w,
(hd[1] - point[1] * hd[3]) / w,
(hd[2] - point[2] * hd[3]) / w,
];
(point, deriv)
}
fn invert_point(&self, target: [f64; 3]) -> Option<f64> {
let (lo, hi) = self.domain();
let span = hi - lo;
if !span.is_finite() || span <= 0.0 {
return None;
}
let mut best = (f64::MAX, lo);
for w in self.knots.windows(2) {
if w[1] - w[0] < 1e-300 || w[1] <= lo || w[0] >= hi {
continue;
}
for s in 0..=8_i32 {
let t = w[0] + (w[1] - w[0]) * (f64::from(s) / 8.0);
let (p, _) = self.point_deriv(t);
let d = sub(p, target);
let d2 = dot(d, d);
if d2 < best.0 {
best = (d2, t);
}
}
}
let mut t = best.1;
for _ in 0..30 {
let (p, dv) = self.point_deriv(t);
let dd = dot(dv, dv);
if dd < 1e-300 {
break;
}
let dt = dot(sub(target, p), dv) / dd;
t = (t + dt).clamp(lo, hi);
if dt.abs() < 1e-13 * span {
break;
}
}
Some(t)
}
}
fn insert_knot_once(cps: &mut Vec<[f64; 4]>, knots: &mut Vec<f64>, degree: usize, t: f64) {
let k = find_span(knots, degree, t);
let q: Vec<[f64; 4]> = ((k - degree + 1)..=k)
.map(|i| {
let denom = knots[i + degree] - knots[i];
let a = if denom.abs() < 1e-300 {
0.0
} else {
(t - knots[i]) / denom
};
hom_lerp(cps[i - 1], cps[i], a)
})
.collect();
let tail = cps[k..].to_vec();
cps.truncate(k - degree + 1);
cps.extend(q);
cps.extend(tail);
knots.insert(k + 1, t);
}
fn bspline_segment(c: &NurbsCurve, t0: f64, t1: f64) -> Option<NurbsCurve> {
let p = c.degree;
let mut knots = c.knots.clone();
let mut cps: Vec<[f64; 4]> = c
.control_points
.iter()
.zip(&c.weights)
.map(|(&q, &w)| [q[0] * w, q[1] * w, q[2] * w, w])
.collect();
let n = knots.len() - p - 1;
let eps = 1e-9 * (knots[n] - knots[p]).abs();
if t1 - t0 <= eps {
return None;
}
for &t in &[t0, t1] {
let mult = knots.iter().filter(|k| (**k - t).abs() <= eps).count();
for _ in mult..p {
insert_knot_once(&mut cps, &mut knots, p, t);
}
}
let n_le = knots.iter().filter(|k| **k <= t0 + eps).count();
let cp_start = n_le.checked_sub(p + 1)?;
let interior: Vec<f64> = knots
.iter()
.copied()
.filter(|k| *k > t0 + eps && *k < t1 - eps)
.collect();
let mut seg_knots = vec![t0; p + 1];
seg_knots.extend(interior);
seg_knots.extend(vec![t1; p + 1]);
let n_seg = seg_knots.len() - p - 1;
if cp_start + n_seg > cps.len() {
return None;
}
let (control_points, weights) = cps[cp_start..cp_start + n_seg]
.iter()
.map(|h| ([h[0] / h[3], h[1] / h[3], h[2] / h[3]], h[3]))
.unzip();
Some(NurbsCurve {
degree: p,
control_points,
weights,
knots: seg_knots,
})
}
fn reverse_curve(c: &NurbsCurve) -> NurbsCurve {
let (a, b) = (c.knots[0], c.knots[c.knots.len() - 1]);
NurbsCurve {
degree: c.degree,
control_points: c.control_points.iter().rev().copied().collect(),
weights: c.weights.iter().rev().copied().collect(),
knots: c.knots.iter().rev().map(|k| a + b - k).collect(),
}
}
fn bspline_trim_param(
cx: Ctx<'_>,
trim: &[m::TrimmingSelectRef],
hc: &HomCurve,
master: m::TrimmingPreference,
) -> Option<f64> {
let parameter = || {
trim.iter().find_map(|s| match s {
m::TrimmingSelectRef::ParameterValue(v) => Some(*v),
_ => None,
})
};
let cartesian = || {
let p = trim.iter().find_map(|s| match s {
m::TrimmingSelectRef::CartesianPoint(id) => {
let c = &cx.model.cartesian_point_arena.get(id.0).coordinates;
Some([
c.first().copied().unwrap_or(0.0),
c.get(1).copied().unwrap_or(0.0),
c.get(2).copied().unwrap_or(0.0),
])
}
_ => None,
})?;
hc.invert_point(p)
};
let t = match master {
m::TrimmingPreference::Cartesian => cartesian().or_else(parameter),
_ => parameter().or_else(cartesian),
}?;
Some(snap_param(hc, t))
}
fn snap_param(hc: &HomCurve, t: f64) -> f64 {
let (lo, hi) = hc.domain();
let snap = 1e-7 * (hi - lo);
hc.knots
.iter()
.copied()
.find(|k| (k - t).abs() <= snap)
.unwrap_or(t)
}
fn segment_between_params(
cx: Ctx<'_>,
basis: &NurbsCurve,
hc: &HomCurve,
(mut t0, mut t1): (f64, f64),
sense: bool,
what: &str,
) -> Option<NurbsCurve> {
let (lo, hi) = hc.domain();
let eps = 1e-7 * (hi - lo);
let closed = {
let (a, _) = hc.point_deriv(lo);
let (b, _) = hc.point_deriv(hi);
let d = sub(a, b);
let size = 1.0 + dot(a, a).sqrt().max(dot(b, b).sqrt());
dot(d, d).sqrt() <= 1e-9 * size
};
if sense {
if t0 > t1 && closed {
if (t0 - hi).abs() <= eps {
t0 = lo;
} else if (t1 - lo).abs() <= eps {
t1 = hi;
}
}
if t0 >= t1 {
cx.warn(format!(
"{what}.to_nurbs: wrap-around trim on a B-spline basis"
));
return None;
}
bspline_segment(basis, t0, t1)
} else {
if t0 < t1 && closed {
if (t0 - lo).abs() <= eps {
t0 = hi;
} else if (t1 - hi).abs() <= eps {
t1 = lo;
}
}
if t1 >= t0 {
cx.warn(format!(
"{what}.to_nurbs: wrap-around trim on a B-spline basis"
));
return None;
}
Some(reverse_curve(&bspline_segment(basis, t1, t0)?))
}
}
fn trimmed_bspline_to_nurbs(
cx: Ctx<'_>,
t: &m::TrimmedCurve,
basis: &NurbsCurve,
) -> Option<NurbsCurve> {
let hc = HomCurve::new(basis);
let master = t.master_representation;
let (Some(t0), Some(t1)) = (
bspline_trim_param(cx, &t.trim_1, &hc, master),
bspline_trim_param(cx, &t.trim_2, &hc, master),
) else {
cx.warn("TRIMMED_CURVE.to_nurbs: no usable trim on a B-spline basis".to_owned());
return None;
};
segment_between_params(cx, basis, &hc, (t0, t1), t.sense_agreement, "TRIMMED_CURVE")
}
fn resolve_curve_3d<'m>(cx: Ctx<'m>, r: &'m m::CurveRef) -> Option<&'m m::CurveRef> {
let mut cur = r;
for _ in 0..16 {
cur = match cur {
m::CurveRef::SurfaceCurve(id) => &cx.model.surface_curve_arena.get(id.0).curve_3d,
m::CurveRef::SeamCurve(id) => &cx.model.seam_curve_arena.get(id.0).curve_3d,
m::CurveRef::BoundedSurfaceCurve(id) => {
&cx.model.bounded_surface_curve_arena.get(id.0).curve_3d
}
m::CurveRef::IntersectionCurve(id) => {
&cx.model.intersection_curve_arena.get(id.0).curve_3d
}
other => return Some(other),
};
}
cx.warn("SURFACE_CURVE.to_nurbs: curve_3d container nesting too deep".to_owned());
None
}
fn resolved_curve_to_nurbs(cx: Ctx<'_>, r: &m::CurveRef) -> Option<NurbsCurve> {
match r {
m::CurveRef::TrimmedCurve(id) => {
trimmed_to_nurbs(cx, cx.model.trimmed_curve_arena.get(id.0))
}
m::CurveRef::Circle(id) => circle_to_nurbs(cx, cx.model.circle_arena.get(id.0)),
m::CurveRef::Ellipse(id) => ellipse_to_nurbs(cx, cx.model.ellipse_arena.get(id.0)),
m::CurveRef::CompositeCurve(id) => {
composite_to_nurbs(cx, cx.model.composite_curve_arena.get(id.0))
}
other => trimmed_basis_nurbs(cx, other),
}
}
pub(crate) fn surface_curve_3d_to_nurbs(cx: Ctx<'_>, r: &m::CurveRef) -> Option<NurbsCurve> {
resolved_curve_to_nurbs(cx, resolve_curve_3d(cx, r)?)
}
pub(crate) fn edge_to_nurbs(
cx: Ctx<'_>,
geometry: &m::CurveRef,
p0: [f64; 3],
p1: [f64; 3],
same_sense: bool,
) -> Option<NurbsCurve> {
let r = resolve_curve_3d(cx, geometry)?;
let size = 1.0
+ p0.iter()
.chain(p1.iter())
.fold(0.0_f64, |m, v| m.max(v.abs()));
let d = sub(p1, p0);
if dot(d, d).sqrt() <= 1e-9 * size {
if matches!(r, m::CurveRef::Line(_)) {
cx.warn("EDGE_CURVE.to_nurbs: closed edge on a line".to_owned());
return None;
}
return resolved_curve_to_nurbs(cx, r);
}
match r {
m::CurveRef::Line(_) => Some(NurbsCurve {
degree: 1,
control_points: vec![p0, p1],
weights: vec![1.0, 1.0],
knots: vec![0.0, 0.0, 1.0, 1.0],
}),
m::CurveRef::Circle(id) => {
let c = cx.model.circle_arena.get(id.0);
let frame = placement_frame_3d(cx, &c.position)?;
Some(conic_edge_arc(
frame,
(c.radius, c.radius),
p0,
p1,
same_sense,
))
}
m::CurveRef::Ellipse(id) => {
let e = cx.model.ellipse_arena.get(id.0);
let frame = placement_frame_3d(cx, &e.position)?;
Some(conic_edge_arc(
frame,
(e.semi_axis_1, e.semi_axis_2),
p0,
p1,
same_sense,
))
}
other => {
let whole = resolved_curve_to_nurbs(cx, other)?;
let hc = HomCurve::new(&whole);
let (t0, t1) = (
snap_param(&hc, hc.invert_point(p0)?),
snap_param(&hc, hc.invert_point(p1)?),
);
segment_between_params(cx, &whole, &hc, (t0, t1), same_sense, "EDGE_CURVE")
}
}
}
fn conic_edge_arc(
frame: Frame,
ab: (f64, f64),
p0: [f64; 3],
p1: [f64; 3],
sense: bool,
) -> NurbsCurve {
use std::f64::consts::TAU;
let (center, x, y, _) = frame;
let angle = |p: [f64; 3]| {
let d = sub(p, center);
(dot(d, y) / ab.1).atan2(dot(d, x) / ab.0)
};
let theta1 = angle(p0);
let mut theta2 = angle(p1);
if sense {
while theta2 <= theta1 {
theta2 += TAU;
}
} else {
while theta2 >= theta1 {
theta2 -= TAU;
}
}
arc_nurbs(center, x, y, ab.0, ab.1, theta1, theta2 - theta1)
}
fn segment_parent_nurbs(cx: Ctx<'_>, r: &m::CurveRef) -> Option<NurbsCurve> {
match r {
m::CurveRef::TrimmedCurve(id) => {
trimmed_to_nurbs(cx, cx.model.trimmed_curve_arena.get(id.0))
}
_ => trimmed_basis_nurbs(cx, r),
}
}
#[allow(clippy::cast_precision_loss)] fn elevate_once(c: &NurbsCurve) -> Option<NurbsCurve> {
let p = c.degree;
let mut knots = c.knots.clone();
let mut cps: Vec<[f64; 4]> = c
.control_points
.iter()
.zip(&c.weights)
.map(|(&q, &w)| [q[0] * w, q[1] * w, q[2] * w, w])
.collect();
let n = knots.len() - p - 1;
let (lo, hi) = (knots[p], knots[n]);
let eps = 1e-9 * (hi - lo).abs();
let mut distinct: Vec<f64> = Vec::new();
for &k in &c.knots {
if k > lo + eps && k < hi - eps && !distinct.iter().any(|d| (d - k).abs() <= eps) {
distinct.push(k);
}
}
for &t in &distinct {
let mult = knots.iter().filter(|k| (**k - t).abs() <= eps).count();
for _ in mult..p {
insert_knot_once(&mut cps, &mut knots, p, t);
}
}
let m = distinct.len() + 1;
if cps.len() != m * p + 1 {
return None;
}
let mut out = Vec::with_capacity(m * (p + 1) + 1);
for j in 0..m {
let b = &cps[j * p..=j * p + p];
let start = usize::from(j != 0); for i in start..=(p + 1) {
out.push(if i == 0 {
b[0]
} else if i == p + 1 {
b[p]
} else {
hom_lerp(b[i], b[i - 1], i as f64 / (p + 1) as f64)
});
}
}
let mut out_knots = vec![lo; p + 2];
for &d in &distinct {
out_knots.extend(std::iter::repeat_n(d, p + 1));
}
out_knots.extend(std::iter::repeat_n(hi, p + 2));
let (control_points, weights) = out
.iter()
.map(|h| ([h[0] / h[3], h[1] / h[3], h[2] / h[3]], h[3]))
.unzip();
Some(NurbsCurve {
degree: p + 1,
control_points,
weights,
knots: out_knots,
})
}
#[allow(clippy::cast_precision_loss)] fn join_segments(cx: Ctx<'_>, parts: &[NurbsCurve]) -> Option<NurbsCurve> {
let clamped: Vec<NurbsCurve> = parts
.iter()
.map(|c| {
let p = c.degree;
let n = c.knots.len() - p - 1;
bspline_segment(c, c.knots[p], c.knots[n])
})
.collect::<Option<_>>()?;
let target = clamped.iter().map(|c| c.degree).max()?;
let elevated: Vec<NurbsCurve> = clamped
.into_iter()
.map(|mut c| {
while c.degree < target {
c = elevate_once(&c)?;
}
Some(c)
})
.collect::<Option<_>>()?;
let size = elevated
.iter()
.flat_map(|c| c.control_points.iter())
.flat_map(|q| q.iter())
.fold(0.0_f64, |m, v| m.max(v.abs()));
let eps = 1e-9 * (1.0 + size);
let p = target;
let (mut cps, mut ws, mut knots) = (Vec::new(), Vec::new(), Vec::<f64>::new());
for (i, seg) in elevated.iter().enumerate() {
let (lo, hi) = (seg.knots[p], seg.knots[seg.knots.len() - p - 1]);
if !(hi - lo).is_finite() || hi - lo <= 0.0 {
return None;
}
let remap = |k: f64| i as f64 + (k - lo) / (hi - lo);
if i == 0 {
knots.extend(seg.knots.iter().map(|&k| remap(k)));
cps.extend(seg.control_points.iter().copied());
ws.extend(seg.weights.iter().copied());
} else {
let prev: [f64; 3] = *cps.last()?;
let d = sub(prev, seg.control_points[0]);
if dot(d, d).sqrt() > eps {
cx.warn(
"COMPOSITE_CURVE.to_nurbs: segments do not join (discontinuous)".to_owned(),
);
return None;
}
knots.pop();
knots.extend(seg.knots.iter().skip(p + 1).map(|&k| remap(k)));
let w_scale = *ws.last()? / seg.weights[0];
cps.extend(seg.control_points.iter().skip(1).copied());
ws.extend(seg.weights.iter().skip(1).map(|w| w * w_scale));
}
}
debug_assert_eq!(knots.len(), cps.len() + p + 1);
Some(NurbsCurve {
degree: p,
control_points: cps,
weights: ws,
knots,
})
}
pub(crate) fn composite_to_nurbs(cx: Ctx<'_>, cc: &m::CompositeCurve) -> Option<NurbsCurve> {
let mut parts = Vec::with_capacity(cc.segments.len());
for s in &cc.segments {
let m::CompositeCurveSegmentRef::CompositeCurveSegment(id) = s else {
cx.warn("COMPOSITE_CURVE.to_nurbs: unsupported complex segment".to_owned());
return None;
};
let seg = cx.model.composite_curve_segment_arena.get(id.0);
let Some(mut c) = segment_parent_nurbs(cx, &seg.parent_curve) else {
cx.warn("COMPOSITE_CURVE.to_nurbs: unsupported segment parent curve".to_owned());
return None;
};
if !seg.same_sense {
c = reverse_curve(&c);
}
parts.push(c);
}
if parts.is_empty() {
cx.warn("COMPOSITE_CURVE.to_nurbs: no segments".to_owned());
return None;
}
join_segments(cx, &parts)
}
fn revolve_frame_cps(
frame: Frame,
cps: &[((f64, f64), f64, f64)],
degree_v: usize,
knots_v: Vec<f64>,
) -> NurbsSurface {
let (center, x, y, z) = frame;
let w_u = circle_weights();
let control_points = CIRCLE_U
.iter()
.map(|&(ux, uy)| {
cps.iter()
.map(|&((a, b), h, _)| {
let (ra, rb) = (ux * a - uy * b, ux * b + uy * a);
add(center, add(add(scale(x, ra), scale(y, rb)), scale(z, h)))
})
.collect()
})
.collect();
let weights = w_u
.iter()
.map(|&wu| cps.iter().map(|&(_, _, wv)| wu * wv).collect())
.collect();
NurbsSurface {
degree_u: 2,
degree_v,
control_points,
weights,
knots_u: CIRCLE_KNOTS.to_vec(),
knots_v,
}
}
fn revolve_nurbs(
center: [f64; 3],
x: [f64; 3],
y: [f64; 3],
z: [f64; 3],
profile: &[(f64, f64, f64)],
knots_v: Vec<f64>,
) -> NurbsSurface {
let cps: Vec<((f64, f64), f64, f64)> = profile
.iter()
.map(|&(rho, h, w)| ((rho, 0.0), h, w))
.collect();
revolve_frame_cps((center, x, y, z), &cps, 2, knots_v)
}
pub(crate) fn revolve_curve(profile: &NurbsCurve, frame: Frame) -> NurbsSurface {
let (center, x, y, z) = frame;
let cps: Vec<((f64, f64), f64, f64)> = profile
.control_points
.iter()
.zip(&profile.weights)
.map(|(&p, &w)| {
let d = sub(p, center);
((dot(d, x), dot(d, y)), dot(d, z), w)
})
.collect();
revolve_frame_cps(frame, &cps, profile.degree, profile.knots.clone())
}
pub(crate) fn sphere_to_nurbs(cx: Ctx<'_>, s: &m::SphericalSurface) -> Option<NurbsSurface> {
let Some((center, x, y, z)) = frame_of_3dref(cx, &s.position) else {
cx.warn("SPHERICAL_SURFACE.to_nurbs: degenerate placement".to_owned());
return None;
};
let r = s.radius;
let w = std::f64::consts::FRAC_1_SQRT_2;
let profile = [
(0.0, -r, 1.0),
(r, -r, w),
(r, 0.0, 1.0),
(r, r, w),
(0.0, r, 1.0),
];
let knots_v = vec![0., 0., 0., 1., 1., 2., 2., 2.];
Some(revolve_nurbs(center, x, y, z, &profile, knots_v))
}
pub(crate) fn torus_to_nurbs(cx: Ctx<'_>, t: &m::ToroidalSurface) -> Option<NurbsSurface> {
let Some((center, x, y, z)) = frame_of_3dref(cx, &t.position) else {
cx.warn("TOROIDAL_SURFACE.to_nurbs: degenerate placement".to_owned());
return None;
};
let big = t.major_radius;
let r = t.minor_radius;
let w = std::f64::consts::FRAC_1_SQRT_2;
let profile = [
(big + r, 0.0, 1.0),
(big + r, r, w),
(big, r, 1.0),
(big - r, r, w),
(big - r, 0.0, 1.0),
(big - r, -r, w),
(big, -r, 1.0),
(big + r, -r, w),
(big + r, 0.0, 1.0),
];
Some(revolve_nurbs(
center,
x,
y,
z,
&profile,
CIRCLE_KNOTS.to_vec(),
))
}
fn nurbs_surface_from(
cx: Ctx<'_>,
degrees: (i64, i64),
cp: &[Vec<m::CartesianPointRef>],
u: (&[f64], &[i64]),
v: (&[f64], &[i64]),
weights: Option<&[Vec<f64>]>,
) -> Option<NurbsSurface> {
let degree_u = usize::try_from(degrees.0).ok()?;
let degree_v = usize::try_from(degrees.1).ok()?;
let control_points: Vec<Vec<[f64; 3]>> = cp
.iter()
.map(|row| {
row.iter()
.map(|r| point_coords(cx, r))
.collect::<Option<_>>()
})
.collect::<Option<_>>()?;
let n_u = control_points.len();
let n_v = control_points.first().map_or(0, Vec::len);
if n_u == 0 || n_v == 0 || control_points.iter().any(|row| row.len() != n_v) {
cx.warn("B_SPLINE_SURFACE.to_nurbs: empty or ragged control grid".to_owned());
return None;
}
let weights = weights.map_or_else(|| vec![vec![1.0; n_v]; n_u], <[Vec<f64>]>::to_vec);
let knots_u = expand_knots(u.0, u.1);
let knots_v = expand_knots(v.0, v.1);
if weights.len() != n_u
|| weights.iter().any(|row| row.len() != n_v)
|| knots_u.len() != n_u + degree_u + 1
|| knots_v.len() != n_v + degree_v + 1
{
cx.warn("B_SPLINE_SURFACE.to_nurbs: inconsistent weight/knot count".to_owned());
return None;
}
Some(NurbsSurface {
degree_u,
degree_v,
control_points,
weights,
knots_u,
knots_v,
})
}
pub(crate) fn bspline_surf_wk_to_nurbs(
cx: Ctx<'_>,
b: &m::BSplineSurfaceWithKnots,
) -> Option<NurbsSurface> {
nurbs_surface_from(
cx,
(b.u_degree, b.v_degree),
&b.control_points_list,
(&b.u_knots, &b.u_multiplicities),
(&b.v_knots, &b.v_multiplicities),
None,
)
}
pub(crate) fn complex_bspline_surface_to_nurbs(
cx: Ctx<'_>,
parts: &[m::UnitPart],
) -> Option<NurbsSurface> {
let (u_degree, v_degree, cp) = parts.iter().find_map(|p| match p {
m::UnitPart::BSplineSurface {
u_degree,
v_degree,
control_points_list,
..
} => Some((*u_degree, *v_degree, control_points_list)),
_ => None,
})?;
let explicit = parts.iter().find_map(|p| match p {
m::UnitPart::BSplineSurfaceWithKnots {
u_knots,
u_multiplicities,
v_knots,
v_multiplicities,
..
} => Some((u_knots, u_multiplicities, v_knots, v_multiplicities)),
_ => None,
});
let derived;
let (u, v): (KnotsRef<'_>, KnotsRef<'_>) = if let Some((uk, um, vk, vm)) = explicit {
((uk, um), (vk, vm))
} else {
let family = parts.iter().find_map(|p| match p {
m::UnitPart::UniformSurface => Some(KnotFamily::Uniform),
m::UnitPart::QuasiUniformSurface => Some(KnotFamily::QuasiUniform),
m::UnitPart::BezierSurface => Some(KnotFamily::Bezier),
_ => None,
})?;
let n_v = cp.first().map_or(0, Vec::len);
derived = (
family_knots(family, u_degree, cp.len())?,
family_knots(family, v_degree, n_v)?,
);
((&derived.0.0, &derived.0.1), (&derived.1.0, &derived.1.1))
};
let weights = parts.iter().find_map(|p| match p {
m::UnitPart::RationalBSplineSurface { weights_data } => Some(weights_data.as_slice()),
_ => None,
});
nurbs_surface_from(cx, (u_degree, v_degree), cp, u, v, weights)
}
pub(crate) fn plane_patch(
cx: Ctx<'_>,
plane: &m::Plane,
points: &[[f64; 3]],
) -> Option<NurbsSurface> {
let (origin, x, y, _) = frame_of_3dref(cx, &plane.position)?;
if points.is_empty() {
return None;
}
let (mut u_lo, mut u_hi, mut v_lo, mut v_hi) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
for p in points {
let d = sub(*p, origin);
let (u, v) = (dot(d, x), dot(d, y));
u_lo = u_lo.min(u);
u_hi = u_hi.max(u);
v_lo = v_lo.min(v);
v_hi = v_hi.max(v);
}
if u_hi - u_lo < 1e-12 || v_hi - v_lo < 1e-12 {
cx.warn("PLANE face.to_nurbs: degenerate (collinear) parameter extent".to_owned());
return None;
}
let corner = |u: f64, v: f64| add(origin, add(scale(x, u), scale(y, v)));
Some(NurbsSurface {
degree_u: 1,
degree_v: 1,
control_points: vec![
vec![corner(u_lo, v_lo), corner(u_lo, v_hi)],
vec![corner(u_hi, v_lo), corner(u_hi, v_hi)],
],
weights: vec![vec![1.0; 2]; 2],
knots_u: vec![0.0, 0.0, 1.0, 1.0],
knots_v: vec![0.0, 0.0, 1.0, 1.0],
})
}
pub(crate) fn cylinder_cone_patch(
cx: Ctx<'_>,
position: &m::Axis2Placement3dRef,
radius: f64,
semi_angle: f64,
points: &[[f64; 3]],
) -> Option<NurbsSurface> {
let (center, x, y, z) = frame_of_3dref(cx, position)?;
if points.is_empty() {
return None;
}
let (mut v_lo, mut v_hi) = (f64::MAX, f64::MIN);
for p in points {
let v = dot(sub(*p, center), z);
v_lo = v_lo.min(v);
v_hi = v_hi.max(v);
}
if v_hi - v_lo < 1e-12 {
cx.warn("CYLINDRICAL/CONICAL face.to_nurbs: degenerate axial extent".to_owned());
return None;
}
let r_lo = radius + v_lo * semi_angle.tan();
let r_hi = radius + v_hi * semi_angle.tan();
let w_u = circle_weights();
let control_points = CIRCLE_U
.iter()
.map(|&(ux, uy)| {
let radial = add(scale(x, ux), scale(y, uy));
vec![
add(add(center, scale(z, v_lo)), scale(radial, r_lo)),
add(add(center, scale(z, v_hi)), scale(radial, r_hi)),
]
})
.collect();
let weights = w_u.iter().map(|&w| vec![w, w]).collect();
Some(NurbsSurface {
degree_u: 2,
degree_v: 1,
control_points,
weights,
knots_u: CIRCLE_KNOTS.to_vec(),
knots_v: vec![0.0, 0.0, 1.0, 1.0],
})
}
pub(crate) fn extrude_patch(
cx: Ctx<'_>,
profile: &NurbsCurve,
axis: &m::VectorRef,
points: &[[f64; 3]],
) -> Option<NurbsSurface> {
let m::VectorRef::Vector(id) = axis else {
return None;
};
let v = cx.model.vector_arena.get(id.0);
let Some(d) = normalize(dir_ratios(cx, &v.orientation)?) else {
cx.warn("SURFACE_OF_LINEAR_EXTRUSION face.to_nurbs: degenerate direction".to_owned());
return None;
};
if points.is_empty() {
return None;
}
let span = |ps: &[[f64; 3]]| {
ps.iter().fold((f64::MAX, f64::MIN), |(lo, hi), p| {
let t = dot(*p, d);
(lo.min(t), hi.max(t))
})
};
let (pc_lo, pc_hi) = span(&profile.control_points);
let (b_lo, b_hi) = span(points);
let (v_lo, v_hi) = (b_lo - pc_hi, b_hi - pc_lo);
if v_hi - v_lo < 1e-12 {
cx.warn("SURFACE_OF_LINEAR_EXTRUSION face.to_nurbs: degenerate extent".to_owned());
return None;
}
let control_points = profile
.control_points
.iter()
.map(|&cp| vec![add(cp, scale(d, v_lo)), add(cp, scale(d, v_hi))])
.collect();
let weights = profile.weights.iter().map(|&w| vec![w, w]).collect();
Some(NurbsSurface {
degree_u: profile.degree,
degree_v: 1,
control_points,
weights,
knots_u: profile.knots.clone(),
knots_v: vec![0.0, 0.0, 1.0, 1.0],
})
}
pub(crate) fn revolved_line_segment(
cx: Ctx<'_>,
line: &m::Line,
frame: Frame,
points: &[[f64; 3]],
) -> Option<NurbsCurve> {
let (center, _, _, z) = frame;
let base = point_coords(cx, &line.pnt)?;
let m::VectorRef::Vector(vid) = &line.dir else {
return None;
};
let v = cx.model.vector_arena.get(vid.0);
let d = normalize(dir_ratios(cx, &v.orientation)?)?;
if points.is_empty() {
return None;
}
let rel = sub(base, center);
let h_base = dot(rel, z);
let dz = dot(d, z);
let (mut t_lo, mut t_hi) = (f64::MAX, f64::MIN);
if dz.abs() >= 1e-7 {
for p in points {
let t = (dot(sub(*p, center), z) - h_base) / dz;
t_lo = t_lo.min(t);
t_hi = t_hi.max(t);
}
} else {
let w = sub(rel, scale(z, h_base));
let u = sub(d, scale(z, dz));
let (uu, wu, ww) = (dot(u, u), dot(w, u), dot(w, w));
if uu < 1e-12 {
return None;
}
for p in points {
let pr = sub(*p, center);
let perp = sub(pr, scale(z, dot(pr, z)));
let disc = wu * wu - uu * (ww - dot(perp, perp));
if disc < 0.0 {
continue;
}
let s = disc.sqrt();
for t in [(-wu - s) / uu, (-wu + s) / uu] {
t_lo = t_lo.min(t);
t_hi = t_hi.max(t);
}
}
}
if t_hi - t_lo < 1e-12 {
cx.warn("SURFACE_OF_REVOLUTION face.to_nurbs: degenerate line-profile extent".to_owned());
return None;
}
Some(NurbsCurve {
degree: 1,
control_points: vec![add(base, scale(d, t_lo)), add(base, scale(d, t_hi))],
weights: vec![1.0, 1.0],
knots: vec![0.0, 0.0, 1.0, 1.0],
})
}