use super::*;
pub(super) fn extend_ruled_neighbour_over(
solid: &mut BrepSolid,
face_id: u64,
points: &[Vec3],
tolerance: f64,
) -> Result<(), String> {
let op = "delete_face_and_heal";
let (shell, face_pos) =
find_face(solid, face_id).ok_or_else(|| format!("{op}: missing face {face_id}"))?;
extend_ruled_carrier(&mut solid.shells[shell].faces[face_pos], points, tolerance)
}
pub(super) fn extend_ruled_carrier(
face: &mut FaceRecord,
points: &[Vec3],
tolerance: f64,
) -> Result<(), String> {
let Some(AnalyticSurface::RuledRevolution {
frame,
rho0,
rho1,
height,
}) = face.surface.analytic().cloned()
else {
return Ok(());
};
let mut low = 0.0f64;
let mut high = height;
for point in points {
let axial = point.sub(frame.origin).dot(frame.axis);
low = low.min(axial - tolerance);
high = high.max(axial + tolerance);
}
if low >= -tolerance && high <= height + tolerance {
return Ok(());
}
let rho_at = |z: f64| rho0 + (rho1 - rho0) * z / height;
let base = frame.origin.add(frame.axis.scale(low));
let start = base.add(frame.x_axis.scale(rho_at(low)));
let end = frame
.origin
.add(frame.axis.scale(high))
.add(frame.x_axis.scale(rho_at(high)));
let generatrix = make_line(start, end)?;
let extended = make_revolution(base, frame.axis, &generatrix, std::f64::consts::TAU)?;
let v_scale = height / (high - low);
let v_offset = -low / (high - low);
face.surface = extended;
for coedge in face
.loops
.iter_mut()
.flat_map(|loop_record| &mut loop_record.coedges)
{
for control in &mut coedge.pcurve.control_points {
control.y = control.y * v_scale + control.w * v_offset;
}
}
Ok(())
}
pub(super) fn extend_revolution_carrier_over(
solid: &mut BrepSolid,
face_id: u64,
points: &[Vec3],
tolerance: f64,
) -> Result<(), String> {
let (shell, face_pos) = find_face(solid, face_id)
.ok_or_else(|| format!("move_faces: missing ruled face {face_id}"))?;
extend_revolution_carrier(&mut solid.shells[shell].faces[face_pos], points, tolerance)
}
pub(super) fn extend_revolution_carrier(
face: &mut FaceRecord,
points: &[Vec3],
tolerance: f64,
) -> Result<(), String> {
let Some(AnalyticSurface::Revolution {
frame,
sweep,
generatrix,
..
}) = face.surface.analytic().cloned()
else {
return Ok(());
};
let controls = &generatrix.control_points;
if generatrix.degree != 1 || controls.len() != 2 {
return Ok(()); }
let p0 = controls[0].point()?;
let p1 = controls[1].point()?;
let axial = |p: Vec3| p.sub(frame.origin).dot(frame.axis);
let (z0, z1) = (axial(p0), axial(p1));
let (span_lo, span_hi) = (z0.min(z1), z0.max(z1));
let mut low = span_lo;
let mut high = span_hi;
for &point in points {
let a = axial(point);
low = low.min(a - tolerance);
high = high.max(a + tolerance);
}
if low >= span_lo - tolerance && high <= span_hi + tolerance {
return Ok(()); }
let denom = z1 - z0;
if denom.abs() <= tolerance {
return Ok(());
}
let point_at = |z: f64| -> Vec3 {
let t = (z - z0) / denom;
p0.add(p1.sub(p0).scale(t))
};
let (start_axial, end_axial) = if z1 >= z0 { (low, high) } else { (high, low) };
let extended = make_line(point_at(start_axial), point_at(end_axial))?;
let grown = make_revolution(frame.origin, frame.axis, &extended, sweep)?;
face.surface = grown;
Ok(())
}
pub(super) fn regrow_and_refit_carrier(
face: &mut FaceRecord,
edges: &HashMap<u64, EdgeRecord>,
scale: f64,
op: &str,
) -> Result<(), String> {
let tolerance = (scale * 1e-7).max(1e-9);
let points = boundary_samples(face, edges, op)?;
if let Ok(plane) = plane_of_surface(&face.surface, (scale * 1e-6).max(1e-7), op) {
let mut u_min = f64::INFINITY;
let mut u_max = f64::NEG_INFINITY;
let mut v_min = f64::INFINITY;
let mut v_max = f64::NEG_INFINITY;
for point in &points {
let delta = point.sub(plane.origin);
u_min = u_min.min(delta.dot(plane.u_dir));
u_max = u_max.max(delta.dot(plane.u_dir));
v_min = v_min.min(delta.dot(plane.v_dir));
v_max = v_max.max(delta.dot(plane.v_dir));
}
if !(u_min.is_finite() && v_min.is_finite()) {
return Err(format!("{op}: empty face boundary"));
}
let margin = ((u_max - u_min).max(v_max - v_min) * 0.25).max(scale * 1e-3);
let origin = plane
.origin
.add(plane.u_dir.scale(u_min - margin))
.add(plane.v_dir.scale(v_min - margin));
face.surface = crate::make_plane(
origin,
plane.u_dir,
plane.v_dir,
(u_max - u_min) + 2.0 * margin,
(v_max - v_min) + 2.0 * margin,
)?;
} else {
extend_ruled_carrier(face, &points, tolerance)?;
extend_revolution_carrier(face, &points, tolerance)?;
}
let surface = face.surface.clone();
crate::offset_retrim::rebuild_loop_pcurves(
face,
edges,
&surface,
PcurveFit::SubrangeAware { tolerance },
op,
)
}
pub(super) enum OpenNeighbourCarrier {
Planar(Plane),
Curved,
}
pub(super) fn curve_plane_crossing_near(
curve: &NurbsCurve,
plane: &Plane,
center: Vec3,
reach: f64,
) -> Option<(f64, Vec3)> {
let [t0, t1] = curve.domain().ok()?;
let height = |t: f64| -> Option<f64> {
curve
.evaluate(t)
.ok()
.map(|point| point.sub(plane.origin).dot(plane.normal))
};
const SAMPLES: usize = 64;
let mut best: Option<(f64, Vec3, f64)> = None;
let mut previous_t = t0;
let mut previous_h = height(previous_t)?;
for index in 1..=SAMPLES {
let t = t0 + (t1 - t0) * index as f64 / SAMPLES as f64;
let h = height(t)?;
if previous_h * h <= 0.0 {
let (mut low, mut high, mut low_h) = (previous_t, t, previous_h);
for _ in 0..80 {
let mid = 0.5 * (low + high);
let mid_h = height(mid)?;
if low_h * mid_h <= 0.0 {
high = mid;
} else {
low = mid;
low_h = mid_h;
}
}
let root = 0.5 * (low + high);
let point = curve.evaluate(root).ok()?;
let distance = point.sub(center).length();
if distance <= reach && best.map(|(_, _, known)| distance < known).unwrap_or(true) {
best = Some((root, point, distance));
}
}
previous_t = t;
previous_h = h;
}
best.map(|(t, point, _)| (t, point))
}
pub(super) fn curve_cap_crossing_near(
curve: &NurbsCurve,
cap_carrier: &OpenNeighbourCarrier,
cap_surface: &NurbsSurface,
center: Vec3,
reach: f64,
tolerance: f64,
) -> Option<(f64, Vec3)> {
match cap_carrier {
OpenNeighbourCarrier::Planar(plane) => {
curve_plane_crossing_near(curve, plane, center, reach)
}
OpenNeighbourCarrier::Curved => {
let hits = intersect_curve_surface(curve, cap_surface, tolerance).ok()?;
let mut best: Option<(f64, Vec3, f64)> = None;
for hit in hits {
let point = curve.evaluate(hit.t).ok()?;
let distance = point.sub(center).length();
if distance <= reach && best.map(|(_, _, known)| distance < known).unwrap_or(true) {
best = Some((hit.t, point, distance));
}
}
best.map(|(t, point, _)| (t, point))
}
}
}
pub(super) fn relocate_open_side_edge(
edge: &mut EdgeRecord,
start_target: Option<(u64, Vec3)>,
end_target: Option<(u64, Vec3)>,
on_curve_tolerance: f64,
tolerance: f64,
op: &str,
) -> Result<bool, String> {
if edge.degenerate {
return Err(format!(
"{op}: degenerate side edge {} cannot be relocated (deferred)",
edge.id
));
}
let [d0, d1] = edge.curve.domain()?;
let span = (d1 - d0).max(1e-12);
let snap = |t: f64| -> f64 {
if (t - d0).abs() <= 1e-9 * span {
d0
} else if (t - d1).abs() <= 1e-9 * span {
d1
} else {
t
}
};
let widen = |target: Vec3| -> Option<f64> {
let projection = project_point_to_curve(&edge.curve, target).ok()?;
(projection.distance <= on_curve_tolerance).then(|| snap(projection.u))
};
let new_t0 = match &start_target {
Some((_, point)) => widen(*point),
None => Some(edge.t0),
};
let new_t1 = match &end_target {
Some((_, point)) => widen(*point),
None => Some(edge.t1),
};
if let (Some(new_t0), Some(new_t1)) = (new_t0, new_t1) {
if new_t1 - new_t0 > 1e-9 * span {
edge.t0 = new_t0;
edge.t1 = new_t1;
if let Some((vertex, _)) = start_target {
edge.start_vertex_id = vertex;
}
if let Some((vertex, _)) = end_target {
edge.end_vertex_id = vertex;
}
return Ok(true);
}
}
if edge.curve.degree == 1 && edge.curve.control_points.len() == 2 {
let start_point = match &start_target {
Some((_, point)) => *point,
None => edge.curve.evaluate(edge.t0)?,
};
let end_point = match &end_target {
Some((_, point)) => *point,
None => edge.curve.evaluate(edge.t1)?,
};
if start_point.sub(end_point).length() <= tolerance {
return Err(format!(
"{op}: healing would collapse side edge {} to zero length",
edge.id
));
}
edge.curve = make_line(start_point, end_point)?;
edge.t0 = 0.0;
edge.t1 = 1.0;
if let Some((vertex, _)) = start_target {
edge.start_vertex_id = vertex;
}
if let Some((vertex, _)) = end_target {
edge.end_vertex_id = vertex;
}
return Ok(true);
}
Ok(false)
}
pub(super) fn rederive_side_edge_on_carriers(
solid: &BrepSolid,
edge: &EdgeRecord,
start_target: Option<(u64, Vec3)>,
end_target: Option<(u64, Vec3)>,
on_curve_tolerance: f64,
tolerance: f64,
op: &str,
) -> Result<EdgeRecord, String> {
let mut adjacent: Vec<&NurbsSurface> = Vec::new();
for shell in &solid.shells {
for face in &shell.faces {
if face
.loops
.iter()
.any(|loop_record| loop_record.coedges.iter().any(|c| c.edge_id == edge.id))
{
adjacent.push(&face.surface);
}
}
}
if adjacent.len() != 2 {
return Err(format!(
"{op}: side edge {} is not shared by exactly two faces (found {})",
edge.id,
adjacent.len()
));
}
let start_point = match &start_target {
Some((_, point)) => *point,
None => edge.curve.evaluate(edge.t0)?,
};
let end_point = match &end_target {
Some((_, point)) => *point,
None => edge.curve.evaluate(edge.t1)?,
};
if start_point.sub(end_point).length() <= tolerance {
return Err(format!(
"{op}: healing would collapse side edge {} to zero length",
edge.id
));
}
let middle_point = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1))?;
let branches =
intersect_analytic_pair(adjacent[0], adjacent[1], tolerance).ok_or_else(|| {
format!(
"{op}: side edge {} cannot be extended onto the recovered corner (its curve \
stops short and its flanking carriers are not a closed-form analytic pair — \
deferred)",
edge.id
)
})?;
for branch in branches {
let Ok(projected_start) = project_point_to_curve(&branch, start_point) else {
continue;
};
let Ok(projected_end) = project_point_to_curve(&branch, end_point) else {
continue;
};
let Ok(projected_middle) = project_point_to_curve(&branch, middle_point) else {
continue;
};
if projected_start.distance > on_curve_tolerance
|| projected_end.distance > on_curve_tolerance
|| projected_middle.distance > on_curve_tolerance
{
continue;
}
let [d0, d1] = branch.domain()?;
let branch_span = (d1 - d0).max(1e-12);
let (t_start, t_end) = (projected_start.u, projected_end.u);
if (t_end - t_start).abs() <= 1e-9 * branch_span {
continue;
}
let (low, high) = if t_start < t_end {
(t_start, t_end)
} else {
(t_end, t_start)
};
let slack = 1e-6 * branch_span;
if projected_middle.u < low - slack || projected_middle.u > high + slack {
continue;
}
let (curve, new_t0, new_t1) = if t_start < t_end {
(branch, t_start, t_end)
} else {
let reversed = branch.reversed()?;
(reversed, d0 + d1 - t_start, d0 + d1 - t_end)
};
let mut record = edge.clone();
record.curve = curve;
record.t0 = new_t0;
record.t1 = new_t1;
if let Some((vertex, _)) = start_target {
record.start_vertex_id = vertex;
}
if let Some((vertex, _)) = end_target {
record.end_vertex_id = vertex;
}
return Ok(record);
}
Err(format!(
"{op}: side edge {} cannot be extended onto the recovered corner (no closed-form \
intersection branch of its flanking carriers reaches both endpoints — deferred)",
edge.id
))
}
pub(super) fn refit_touched_pcurves(
face: &mut FaceRecord,
edges: &HashMap<u64, EdgeRecord>,
touched: &HashSet<u64>,
only_subrange: bool,
tolerance: f64,
op: &str,
) -> Result<(), String> {
let surface = face.surface.clone();
for loop_record in &mut face.loops {
for coedge in &mut loop_record.coedges {
if !touched.contains(&coedge.edge_id) {
continue;
}
let edge = edges
.get(&coedge.edge_id)
.ok_or_else(|| format!("{op}: missing edge {}", coedge.edge_id))?;
if only_subrange {
let [d0, d1] = edge.curve.domain()?;
let span = (d1 - d0).max(1e-12);
if (edge.t0 - d0).abs() <= 1e-9 * span && (edge.t1 - d1).abs() <= 1e-9 * span {
continue;
}
}
coedge.pcurve = build_pcurve_on_surface_range(
&surface,
&edge.curve,
edge.t0,
edge.t1,
coedge.forward,
tolerance,
)?;
}
}
Ok(())
}