use super::*;
fn guided_section_centroid(curves: &[NurbsCurve]) -> Result<Vec3, String> {
let mut sum = Vec3::default();
let mut count = 0usize;
for curve in curves {
let [start, end] = curve.domain()?;
for index in 0..16 {
sum = sum.add(curve.evaluate(start + (end - start) * index as f64 / 16.0)?);
count += 1;
}
}
if count == 0 {
return Err("guidedLoft: a section has no sampleable curves".into());
}
Ok(sum.scale(1.0 / count as f64))
}
pub fn loft_profile_brep_guided(
sections: &[Vec<NurbsCurve>],
guide: &NurbsCurve,
name: Option<&str>,
) -> Result<BrepSolid, String> {
loft_profile_brep_guided_core(sections, guide, name, false)
}
pub fn loft_profile_brep_guided_frame(
sections: &[Vec<NurbsCurve>],
guide: &NurbsCurve,
name: Option<&str>,
) -> Result<BrepSolid, String> {
loft_profile_brep_guided_core(sections, guide, name, true)
}
struct GuidedFrames {
params: Vec<f64>,
points: Vec<Vec3>,
tangents: Vec<Vec3>,
r_axes: Vec<Vec3>,
s_axes: Vec<Vec3>,
}
impl GuidedFrames {
fn index_of(&self, t: f64) -> Result<usize, String> {
let lower = self.params.partition_point(|p| *p < t - 1e-9);
if lower < self.params.len() && (self.params[lower] - t).abs() <= 1e-9 {
Ok(lower)
} else {
Err(format!("guidedLoft: no frame marched at parameter {t}"))
}
}
}
fn guided_frames(
guide: &NurbsCurve,
g0: f64,
g1: f64,
station_params: &[f64],
section_params: &[f64],
) -> Result<GuidedFrames, String> {
let mut params: Vec<f64> = station_params
.iter()
.chain(section_params.iter())
.copied()
.collect();
params.sort_by(|a, b| a.partial_cmp(b).expect("guide params are finite"));
params.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
let count = params.len();
let mut points = Vec::with_capacity(count);
let mut tangents = Vec::with_capacity(count);
for (index, t) in params.iter().enumerate() {
let derivatives = guide.derivatives(g0 + (g1 - g0) * t, 1)?;
let tangent = derivatives[1]
.normalized()
.map_err(|_| format!("guidedLoft: guide tangent is degenerate at station {index}"))?;
points.push(derivatives[0]);
tangents.push(tangent);
}
let mut r_axes = Vec::with_capacity(count);
let mut s_axes = Vec::with_capacity(count);
let r0 = tangents[0].perpendicular()?; s_axes.push(tangents[0].cross(r0).normalized()?);
r_axes.push(r0);
for index in 0..count - 1 {
let t_next = tangents[index + 1];
let v1 = points[index + 1].sub(points[index]);
let c1 = v1.dot(v1);
let r_candidate = if c1 <= 1e-18 {
r_axes[index]
} else {
let reflected_r = r_axes[index].sub(v1.scale(2.0 / c1 * v1.dot(r_axes[index])));
let reflected_t = tangents[index].sub(v1.scale(2.0 / c1 * v1.dot(tangents[index])));
let v2 = t_next.sub(reflected_t);
let c2 = v2.dot(v2);
if c2 <= 1e-18 {
reflected_r
} else {
reflected_r.sub(v2.scale(2.0 / c2 * v2.dot(reflected_r)))
}
};
let r_next = r_candidate
.sub(t_next.scale(r_candidate.dot(t_next)))
.normalized()
.map_err(|_| format!("guidedLoft: frame degenerated at station {index}"))?;
s_axes.push(t_next.cross(r_next).normalized()?);
r_axes.push(r_next);
}
Ok(GuidedFrames {
params,
points,
tangents,
r_axes,
s_axes,
})
}
fn loft_profile_brep_guided_core(
sections: &[Vec<NurbsCurve>],
guide: &NurbsCurve,
name: Option<&str>,
rotate_to_frame: bool,
) -> Result<BrepSolid, String> {
let _ = name;
let tolerance = 1e-6;
let section_count = sections.len();
if section_count < 2 {
return Err("guidedLoft: need at least 2 sections".into());
}
let curve_count = validate_sections(sections, tolerance, "guidedLoft", true)?;
let [g0, g1] = guide.domain()?;
if (g1 - g0).abs() <= tolerance {
return Err("guidedLoft: guide domain is degenerate".into());
}
let guide_start = guide.evaluate(g0)?;
let mut guide_extent = 0.0_f64;
for index in 1..=8 {
let point = guide.evaluate(g0 + (g1 - g0) * index as f64 / 8.0)?;
guide_extent = guide_extent.max(point.sub(guide_start).length());
}
if guide_extent <= tolerance {
return Err("guidedLoft: guide curve is degenerate (no spatial extent)".into());
}
let mut u_list = Vec::with_capacity(section_count);
for section in sections {
let centroid = guided_section_centroid(section)?;
let projection = crate::project_point_to_curve(guide, centroid)?;
let u = ((projection.u - g0) / (g1 - g0)).clamp(0.0, 1.0);
u_list.push(u);
}
let increasing = u_list.windows(2).all(|pair| pair[1] > pair[0] + tolerance);
let decreasing = u_list.windows(2).all(|pair| pair[1] < pair[0] - tolerance);
if !increasing && !decreasing {
return Err("guidedLoft: sections do not project monotonically onto the guide".into());
}
let mut ordered_sections: Vec<Vec<NurbsCurve>> = sections.to_vec();
let mut ordered_u = u_list;
if decreasing {
ordered_sections.reverse();
ordered_u.reverse();
}
let u_first = ordered_u[0];
let u_last = ordered_u[section_count - 1];
if u_last - u_first <= tolerance {
return Err("guidedLoft: sections project to coincident guide stations".into());
}
let station_count = 24usize.max(6 * section_count);
let station_params: Vec<f64> = (0..station_count)
.map(|station| {
let frac = station as f64 / (station_count - 1) as f64;
u_first + (u_last - u_first) * frac
})
.collect();
let frames = if rotate_to_frame {
Some(guided_frames(guide, g0, g1, &station_params, &ordered_u)?)
} else {
None
};
let blend_sources: Vec<Vec<NurbsCurve>> = if let Some(frames) = &frames {
let mut localized = Vec::with_capacity(section_count);
for (section_index, section) in ordered_sections.iter().enumerate() {
let frame = frames.index_of(ordered_u[section_index])?;
let origin = frames.points[frame];
let (r, s, t_axis) = (
frames.r_axes[frame],
frames.s_axes[frame],
frames.tangents[frame],
);
let mut local_section = Vec::with_capacity(curve_count);
for curve in section {
let control_points = curve
.control_points
.iter()
.map(|point| {
let weight = point.w;
let local = Vec3::new(point.x / weight, point.y / weight, point.z / weight)
.sub(origin);
Vec4 {
x: local.dot(r) * weight,
y: local.dot(s) * weight,
z: local.dot(t_axis) * weight,
w: weight,
}
})
.collect();
local_section.push(NurbsCurve::new(
curve.degree,
curve.knots.clone(),
control_points,
)?);
}
localized.push(local_section);
}
localized
} else {
ordered_sections.clone()
};
let mut blended_sections: Vec<Vec<NurbsCurve>> = Vec::with_capacity(station_count);
for &t in &station_params {
let mut interval = 0usize;
while interval + 1 < section_count - 1 && ordered_u[interval + 1] <= t {
interval += 1;
}
let u_lo = ordered_u[interval];
let u_hi = ordered_u[interval + 1];
let span = u_hi - u_lo;
if span <= tolerance {
return Err("guidedLoft: sections project to coincident guide stations".into());
}
let f = ((t - u_lo) / span).clamp(0.0, 1.0);
let section_lo = &blend_sources[interval];
let section_hi = &blend_sources[interval + 1];
let mut blended: Vec<NurbsCurve> = Vec::with_capacity(curve_count);
for curve_index in 0..curve_count {
let curve_lo = §ion_lo[curve_index];
let curve_hi = §ion_hi[curve_index];
let control_points = curve_lo
.control_points
.iter()
.zip(&curve_hi.control_points)
.map(|(a, b)| Vec4 {
x: a.x * (1.0 - f) + b.x * f,
y: a.y * (1.0 - f) + b.y * f,
z: a.z * (1.0 - f) + b.z * f,
w: a.w * (1.0 - f) + b.w * f,
})
.collect();
blended.push(NurbsCurve::new(
curve_lo.degree,
curve_lo.knots.clone(),
control_points,
)?);
}
let placed: Vec<NurbsCurve> = if let Some(frames) = &frames {
let frame = frames.index_of(t)?;
let origin = frames.points[frame];
let (r, s, t_axis) = (
frames.r_axes[frame],
frames.s_axes[frame],
frames.tangents[frame],
);
let mut placed = Vec::with_capacity(curve_count);
for curve in &blended {
let control_points = curve
.control_points
.iter()
.map(|point| {
let weight = point.w;
let world = origin
.add(r.scale(point.x / weight))
.add(s.scale(point.y / weight))
.add(t_axis.scale(point.z / weight));
Vec4 {
x: world.x * weight,
y: world.y * weight,
z: world.z * weight,
w: weight,
}
})
.collect();
placed.push(NurbsCurve::new(
curve.degree,
curve.knots.clone(),
control_points,
)?);
}
placed
} else {
let blended_centroid = guided_section_centroid(&blended)?;
let guide_point = guide.evaluate(g0 + (g1 - g0) * t)?;
let delta = guide_point.sub(blended_centroid);
let mut placed = Vec::with_capacity(curve_count);
for curve in &blended {
let control_points = curve
.control_points
.iter()
.map(|point| Vec4 {
x: point.x + point.w * delta.x,
y: point.y + point.w * delta.y,
z: point.z + point.w * delta.z,
w: point.w,
})
.collect();
placed.push(NurbsCurve::new(
curve.degree,
curve.knots.clone(),
control_points,
)?);
}
placed
};
blended_sections.push(placed);
}
loft_profile_brep(&blended_sections)
.map_err(|error| format!("guidedLoft: loft through guided sections failed: {error}"))
}