use super::*;
fn guided_section_centroid(curves: &[NurbsCurve]) -> Result<Vec3, KernelRefusal> {
let mut sum = Vec3::default();
let mut count = 0usize;
for curve in curves {
let [start, end] = curve.domain().or_refuse(KernelStage::Collect, "domain")?;
for index in 0..16 {
sum = sum.add(
curve
.evaluate(start + (end - start) * index as f64 / 16.0)
.or_refuse(KernelStage::Collect, "evaluate")?,
);
count += 1;
}
}
if count == 0 {
return Err(KernelRefusal::input(
KernelStage::Collect,
"empty_section",
"guidedLoft: a section has no sampleable curves",
));
}
Ok(sum.scale(1.0 / count as f64))
}
pub fn loft_profile_brep_guided(
sections: &[Vec<NurbsCurve>],
guide: &NurbsCurve,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
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, KernelRefusal> {
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, KernelRefusal> {
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(KernelRefusal::internal(
KernelStage::Refine,
"frame_index",
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, KernelRefusal> {
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)
.or_refuse(KernelStage::Refine, "guide_derivatives")?;
let tangent = derivatives[1].normalized().map_err(|_| {
KernelRefusal::input(
KernelStage::Classify,
"guide_tangent",
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()
.or_refuse(KernelStage::Refine, "frame_start")?; s_axes.push(
tangents[0]
.cross(r0)
.normalized()
.or_refuse(KernelStage::Refine, "frame_start")?,
);
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(|_| {
KernelRefusal::internal(
KernelStage::Refine,
"frame_march",
format!("guidedLoft: frame degenerated at station {index}"),
)
})?;
s_axes.push(
t_next
.cross(r_next)
.normalized()
.or_refuse(KernelStage::Refine, "frame_march")?,
);
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, KernelRefusal> {
let _ = name;
let tolerance = 1e-6;
let section_count = sections.len();
if section_count < 2 {
return Err(KernelRefusal::input(
KernelStage::Collect,
"section_count",
"guidedLoft: need at least 2 sections",
));
}
let curve_count = validate_sections(sections, tolerance, "guidedLoft", true)?;
let [g0, g1] = guide.domain().or_refuse(KernelStage::Collect, "guide")?;
if (g1 - g0).abs() <= tolerance {
return Err(KernelRefusal::input(
KernelStage::Collect,
"guide_domain",
"guidedLoft: guide domain is degenerate",
));
}
let guide_start = guide.evaluate(g0).or_refuse(KernelStage::Collect, "guide")?;
let mut guide_extent = 0.0_f64;
for index in 1..=8 {
let point = guide
.evaluate(g0 + (g1 - g0) * index as f64 / 8.0)
.or_refuse(KernelStage::Collect, "guide")?;
guide_extent = guide_extent.max(point.sub(guide_start).length());
}
if guide_extent <= tolerance {
return Err(KernelRefusal::input(
KernelStage::Collect,
"guide_extent",
"guidedLoft: guide curve is degenerate (no spatial extent)",
));
}
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)
.or_refuse(KernelStage::Classify, "project")?;
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(KernelRefusal::input(
KernelStage::Classify,
"monotone_projection",
"guidedLoft: sections do not project monotonically onto the guide",
));
}
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(KernelRefusal::input(
KernelStage::Classify,
"coincident_stations",
"guidedLoft: sections project to coincident guide stations",
));
}
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)
.or_refuse(KernelStage::Refine, "local_curve")?,
);
}
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(KernelRefusal::input(
KernelStage::Classify,
"coincident_interval",
"guidedLoft: sections project to coincident guide stations",
));
}
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)
.or_refuse(KernelStage::Refine, "blend_curve")?,
);
}
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)
.or_refuse(KernelStage::Refine, "placed_curve")?,
);
}
placed
} else {
let blended_centroid = guided_section_centroid(&blended)?;
let guide_point = guide
.evaluate(g0 + (g1 - g0) * t)
.or_refuse(KernelStage::Refine, "guide_station")?;
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)
.or_refuse(KernelStage::Refine, "translated_curve")?,
);
}
placed
};
blended_sections.push(placed);
}
loft_profile_brep(&blended_sections).map_err(|error| {
error.with_message(|error| {
format!("guidedLoft: loft through guided sections failed: {error}")
})
})
}