use super::*;
#[derive(Clone)]
struct SectionFrame {
normal: Vec3,
centroid: Vec3,
samples: Vec<Vec3>,
planar: bool,
}
pub(super) fn closed_points(curves: &[NurbsCurve], tolerance: f64) -> Result<Vec<Vec3>, String> {
if curves.len() < 2 {
return Err("loftSolid: a section needs at least 2 curves".into());
}
let mut points = Vec::with_capacity(curves.len());
for (index, curve) in curves.iter().enumerate() {
let [start, end] = curve.domain()?;
let next = &curves[(index + 1) % curves.len()];
let next_start = next.domain()?[0];
if curve
.evaluate(end)?
.sub(next.evaluate(next_start)?)
.length()
> tolerance
{
return Err(format!("loftSolid: section is open at curve {index}"));
}
points.push(curve.evaluate(start)?);
}
Ok(points)
}
fn section_frame(curves: &[NurbsCurve], tolerance: f64) -> Result<SectionFrame, String> {
let mut samples = Vec::new();
for curve in curves {
let [start, end] = curve.domain()?;
for index in 0..16 {
samples.push(curve.evaluate(start + (end - start) * index as f64 / 16.0)?);
}
}
let mut normal = crate::polygon::newell_normal(&samples);
let mut centroid = samples.iter().fold(Vec3::default(), |sum, &point| sum.add(point));
normal = normal.normalized()?;
centroid = centroid.scale(1.0 / samples.len() as f64);
let planar = samples
.iter()
.all(|point| point.sub(samples[0]).dot(normal).abs() <= tolerance * 100.0);
Ok(SectionFrame {
normal,
centroid,
samples,
planar,
})
}
const CAP_ADVANCE_MIN: f64 = 0.1;
fn advance_from(
sections: &[Vec<NurbsCurve>],
anchor: Vec3,
normal: Vec3,
order: impl Iterator<Item = usize>,
tolerance: f64,
) -> Result<Option<Vec3>, String> {
for index in order {
let step = section_frame(§ions[index], tolerance)?
.centroid
.sub(anchor);
let length = step.length();
if length > tolerance && (step.dot(normal) / length).abs() >= CAP_ADVANCE_MIN {
return Ok(Some(step.scale(1.0 / length)));
}
}
Ok(None)
}
fn reverse_section(curves: &[NurbsCurve]) -> Result<Vec<NurbsCurve>, String> {
curves.iter().rev().map(NurbsCurve::reversed).collect()
}
fn cap_face(
curves: &[NurbsCurve],
edge_ids: &[u64],
frame: &SectionFrame,
advance: Vec3,
outward: bool,
next_id: &mut u64,
) -> Result<FaceRecord, String> {
let normal = if frame.normal.dot(advance) >= 0.0 {
frame.normal
} else {
frame.normal.scale(-1.0)
};
let x_axis = normal.perpendicular()?;
let y_axis = normal.cross(x_axis).normalized()?;
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for point in &frame.samples {
let delta = point.sub(frame.centroid);
min_x = min_x.min(delta.dot(x_axis));
max_x = max_x.max(delta.dot(x_axis));
min_y = min_y.min(delta.dot(y_axis));
max_y = max_y.max(delta.dot(y_axis));
}
let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
let origin = frame
.centroid
.add(x_axis.scale(min_x - padding))
.add(y_axis.scale(min_y - padding));
let surface = make_plane(
origin,
x_axis,
y_axis,
max_x - min_x + 2.0 * padding,
max_y - min_y + 2.0 * padding,
)?;
let mut coedges = Vec::with_capacity(curves.len());
if outward {
for (index, curve) in curves.iter().enumerate() {
coedges.push(CoedgeRecord {
id: *next_id,
edge_id: edge_ids[index],
forward: true,
pcurve: curve_to_plane_parameters(curve, origin, x_axis, y_axis)?,
});
*next_id += 1;
}
} else {
for index in (0..curves.len()).rev() {
coedges.push(CoedgeRecord {
id: *next_id,
edge_id: edge_ids[index],
forward: false,
pcurve: curve_to_plane_parameters(&curves[index], origin, x_axis, y_axis)?
.reversed()?,
});
*next_id += 1;
}
}
let loop_id = *next_id;
*next_id += 1;
let face_id = *next_id;
*next_id += 1;
Ok(FaceRecord {
id: face_id,
surface,
same_sense: outward,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: None,
})
}
pub fn loft_profile_brep(input_sections: &[Vec<NurbsCurve>]) -> Result<BrepSolid, String> {
loft_profile_brep_core(input_sections, None)
}
pub fn loft_profile_brep_tangent(
input_sections: &[Vec<NurbsCurve>],
start_direction: Vec3,
end_direction: Vec3,
) -> Result<BrepSolid, String> {
let start = start_direction
.normalized()
.map_err(|_| "loftSolid: start tangent must be a nonzero direction".to_string())?;
let end = end_direction
.normalized()
.map_err(|_| "loftSolid: end tangent must be a nonzero direction".to_string())?;
loft_profile_brep_core(input_sections, Some((start, end)))
}
fn loft_profile_brep_core(
input_sections: &[Vec<NurbsCurve>],
end_tangents: Option<(Vec3, Vec3)>,
) -> Result<BrepSolid, String> {
let tolerance = 1e-6;
let section_count = input_sections.len();
if section_count < 2 {
return Err("loftSolid: need at least 2 sections".into());
}
let mut sections = input_sections.to_vec();
let curve_count = validate_sections(§ions, tolerance, "loftSolid", true)?;
let first_frame = section_frame(§ions[0], tolerance)?;
let last_frame = section_frame(§ions[section_count - 1], tolerance)?;
if !first_frame.planar || !last_frame.planar {
return Err("loftSolid: end sections must be planar".into());
}
last_frame
.centroid
.sub(first_frame.centroid)
.normalized()
.map_err(|_| "loftSolid: end sections coincide".to_string())?;
let start_advance = advance_from(
§ions,
first_frame.centroid,
first_frame.normal,
1..section_count,
tolerance,
)?
.ok_or(
"loftSolid: the loft runs inside its START section's plane — every later section \
lies in it, so the start cap would be a sliver rather than a face",
)?;
let finish_advance = advance_from(
§ions,
last_frame.centroid,
last_frame.normal,
(0..section_count - 1).rev(),
tolerance,
)?
.map(|step| step.scale(-1.0))
.ok_or(
"loftSolid: the loft runs inside its END section's plane — every earlier section \
lies in it, so the end cap would be a sliver rather than a face",
)?;
let first_normal = if first_frame.normal.dot(start_advance) >= 0.0 {
first_frame.normal
} else {
first_frame.normal.scale(-1.0)
};
let x_axis = first_normal.perpendicular()?;
let y_axis = first_normal.cross(x_axis).normalized()?;
let mut section_reversed = Vec::with_capacity(section_count);
section_reversed.push(profile_area(§ions[0], first_frame.centroid, x_axis, y_axis)? < 0.0);
let mut previous_normal = if section_reversed[0] {
first_frame.normal.scale(-1.0)
} else {
first_frame.normal
};
for section in sections.iter().skip(1) {
let normal = section_frame(section, tolerance)?.normal;
let flip = normal.dot(previous_normal) < 0.0;
section_reversed.push(flip);
previous_normal = if flip { normal.scale(-1.0) } else { normal };
}
let reversed_winding = section_reversed[0];
for (index, flip) in section_reversed.into_iter().enumerate() {
if flip {
sections[index] = reverse_section(§ions[index])?;
}
}
let mut parameters = vec![0.0; section_count];
let mut accumulated = vec![0.0; section_count];
let mut columns = 0usize;
for curve_index in 0..curve_count {
for control_index in 0..sections[0][curve_index].control_points.len() {
let mut total = 0.0;
let mut chords = vec![0.0; section_count];
for section_index in 1..section_count {
let previous = sections[section_index - 1][curve_index].control_points
[control_index]
.point()?;
let current =
sections[section_index][curve_index].control_points[control_index].point()?;
total += current.sub(previous).length();
chords[section_index] = total;
}
if total <= tolerance {
continue;
}
for section_index in 0..section_count {
accumulated[section_index] += chords[section_index] / total;
}
columns += 1;
}
}
if columns == 0 {
return Err("loftSolid: sections coincide".into());
}
for index in 0..section_count {
parameters[index] = accumulated[index] / columns as f64;
}
parameters[0] = 0.0;
parameters[section_count - 1] = 1.0;
if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
return Err("loftSolid: sections are not strictly ordered".into());
}
let degree_v = if end_tangents.is_some() {
3
} else {
3usize.min(section_count - 1)
};
let mut skins = Vec::with_capacity(curve_count);
for curve_index in 0..curve_count {
let reference = §ions[0][curve_index];
let mut grid = Vec::with_capacity(reference.control_points.len());
let mut knots_v = Vec::new();
for control_index in 0..reference.control_points.len() {
let points = sections
.iter()
.map(|section| section[curve_index].control_points[control_index].point())
.collect::<Result<Vec<_>, _>>()?;
let interpolated = match end_tangents {
None => interpolate_curve(&points, degree_v, ¶meters)?,
Some((start_direction, end_direction)) => {
let chord: f64 = points
.windows(2)
.map(|pair| pair[1].sub(pair[0]).length())
.sum();
let magnitude = if chord > tolerance { chord } else { 1.0 };
crate::interpolate_curve_with_end_tangents(
&points,
¶meters,
start_direction.scale(magnitude),
end_direction.scale(magnitude),
)?
}
};
knots_v = interpolated.knots.clone();
let weight = reference.control_points[control_index].w;
grid.push(
interpolated
.control_points
.iter()
.map(|point| Vec4::from_point(point.point().unwrap(), weight))
.collect(),
);
}
skins.push(NurbsSurface::new(
reference.degree,
degree_v,
reference.knots.clone(),
knots_v,
grid,
)?);
}
let bottom = §ions[0];
let top = §ions[section_count - 1];
let bottom_points = closed_points(bottom, tolerance)?;
let top_points = closed_points(top, tolerance)?;
let mut vertices = Vec::with_capacity(2 * curve_count);
for (index, point) in bottom_points.iter().chain(&top_points).enumerate() {
vertices.push(VertexRecord {
id: index as u64 + 1,
point: *point,
});
}
let mut edges = Vec::with_capacity(3 * curve_count);
let mut bottom_edge_ids = Vec::new();
let mut top_edge_ids = Vec::new();
let mut vertical_edge_ids = Vec::new();
for index in 0..curve_count {
let [start, end] = bottom[index].domain()?;
let bottom_id = 10 + index as u64;
let top_id = 10 + curve_count as u64 + index as u64;
let vertical_id = 10 + 2 * curve_count as u64 + index as u64;
bottom_edge_ids.push(bottom_id);
top_edge_ids.push(top_id);
vertical_edge_ids.push(vertical_id);
edges.push(EdgeRecord {
id: bottom_id,
curve: bottom[index].clone(),
t0: start,
t1: end,
start_vertex_id: index as u64 + 1,
end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
degenerate: false,
name: None,
});
edges.push(EdgeRecord {
id: top_id,
curve: top[index].clone(),
t0: start,
t1: end,
start_vertex_id: (curve_count + index) as u64 + 1,
end_vertex_id: (curve_count + (index + 1) % curve_count) as u64 + 1,
degenerate: false,
name: None,
});
let vertical = skins[index].iso_curve_u(start)?;
let [v_start, v_end] = vertical.domain()?;
edges.push(EdgeRecord {
id: vertical_id,
curve: vertical,
t0: v_start,
t1: v_end,
start_vertex_id: index as u64 + 1,
end_vertex_id: (curve_count + index) as u64 + 1,
degenerate: false,
name: None,
});
}
let mut next_id = 1000u64;
let mut faces = Vec::with_capacity(curve_count + 2);
for index in 0..curve_count {
let [start, end] = bottom[index].domain()?;
let coedges = vec![
CoedgeRecord {
id: next_id,
edge_id: bottom_edge_ids[index],
forward: true,
pcurve: parameter_line(start, 0.0, end, 0.0)?,
},
CoedgeRecord {
id: next_id + 1,
edge_id: vertical_edge_ids[(index + 1) % curve_count],
forward: true,
pcurve: parameter_line(end, 0.0, end, 1.0)?,
},
CoedgeRecord {
id: next_id + 2,
edge_id: top_edge_ids[index],
forward: false,
pcurve: parameter_line(end, 1.0, start, 1.0)?,
},
CoedgeRecord {
id: next_id + 3,
edge_id: vertical_edge_ids[index],
forward: false,
pcurve: parameter_line(start, 1.0, start, 0.0)?,
},
];
next_id += 4;
let loop_id = next_id;
next_id += 1;
let face_id = next_id;
next_id += 1;
faces.push(FaceRecord {
id: face_id,
surface: skins[index].clone(),
same_sense: true,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: None,
});
}
if reversed_winding {
faces.reverse();
}
faces.push(cap_face(
bottom,
&bottom_edge_ids,
&first_frame,
start_advance,
false,
&mut next_id,
)?);
faces.push(cap_face(
top,
&top_edge_ids,
&last_frame,
finish_advance,
true,
&mut next_id,
)?);
let solid = BrepSolid {
id: next_id + 1,
vertices,
edges,
shells: vec![ShellRecord { id: next_id, faces }],
genus: 0,
};
let issues = solid.validate();
if issues.is_empty() {
Ok(solid)
} else {
Err(format!(
"Rust loft builder produced invalid topology: {issues:?}"
))
}
}