use crate::topology::{BrepSolid, EdgeRecord, FaceRecord};
use crate::{fit, KernelRefusal, KernelStage, NurbsSurface, OrRefuse, PolygonClass, Vec3};
use super::stations::{
apex_point, branch_hop, closed_rail_interval_misses, continue_station, raw_normal, solve_station, station_parameters,
tangency_state, BlendMate, Continued, FittedRows, Station, NEWTON_ITERATIONS, REFINE_ROUNDS, SEED_INTERVALS,
};
pub(crate) const TANGENT_ROLLOVER_WHAT: &str = "tangent_rollover";
#[derive(Clone, Debug)]
pub(super) struct RailCrossing {
pub(crate) side: usize,
pub(crate) mate_face_id: u64,
pub(crate) edge_id: u64,
pub(crate) neighbour_face_id: u64,
pub(crate) stations: [usize; 2],
pub(crate) leaving: bool,
pub(crate) point: Vec3,
pub(crate) edge_parameter: f64,
pub(crate) fraction: f64,
pub(crate) solved: Option<(f64, Station)>,
}
#[derive(Clone, Debug)]
pub(super) struct StripePiece {
pub(crate) faces: [u64; 2],
pub(crate) stations: Vec<Station>,
pub(crate) from: usize,
pub(crate) to: usize,
}
#[derive(Clone, Debug)]
pub(super) struct RolloverPlan {
pub(crate) crossings: Vec<RailCrossing>,
pub(crate) pieces: Vec<StripePiece>,
pub(crate) rolled: Vec<Option<(f64, Station)>>,
}
fn wrapped_uv(face: &FaceRecord, mut uv: [f64; 2]) -> [f64; 2] {
if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
if closed_u {
if let Ok([low, high]) = face.surface.domain_u() {
uv[0] = low + (uv[0] - low).rem_euclid(high - low);
}
}
if closed_v {
if let Ok([low, high]) = face.surface.domain_v() {
uv[1] = low + (uv[1] - low).rem_euclid(high - low);
}
}
}
uv
}
fn track_uv(station: &Station, side: usize) -> [f64; 2] {
if side == 0 {
station.uv1
} else {
station.uv2
}
}
fn inside(face: &FaceRecord, uv: [f64; 2]) -> bool {
let uv = wrapped_uv(face, uv);
!crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
.map(|class| class == PolygonClass::Outside)
.unwrap_or(true)
}
fn segments_cross(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> Option<(f64, f64)> {
let r = [b[0] - a[0], b[1] - a[1]];
let s = [d[0] - c[0], d[1] - c[1]];
let denominator = r[0] * s[1] - r[1] * s[0];
if denominator.abs() <= 1e-18 {
return None;
}
let ac = [c[0] - a[0], c[1] - a[1]];
let lambda = (ac[0] * s[1] - ac[1] * s[0]) / denominator;
let mu = (ac[0] * r[1] - ac[1] * r[0]) / denominator;
let band = 1e-9;
((-band..=1.0 + band).contains(&lambda) && (-band..=1.0 + band).contains(&mu))
.then(|| (lambda.clamp(0.0, 1.0), mu.clamp(0.0, 1.0)))
}
const PCURVE_SAMPLES: usize = 64;
fn boundary_hit(
solid: &BrepSolid,
face: &FaceRecord,
from: [f64; 2],
to: [f64; 2],
leaving: bool,
) -> Option<(u64, f64, f64, [f64; 2])> {
let (closed_u, closed_v) = face.surface.closed_directions().ok()?;
let period_u = closed_u.then(|| face.surface.domain_u().ok().map(|[a, b]| b - a)).flatten();
let period_v = closed_v.then(|| face.surface.domain_v().ok().map(|[a, b]| b - a)).flatten();
let shifts_u: Vec<f64> = match period_u {
Some(period) => vec![-period, 0.0, period],
None => vec![0.0],
};
let shifts_v: Vec<f64> = match period_v {
Some(period) => vec![-period, 0.0, period],
None => vec![0.0],
};
let mut best: Option<(u64, f64, f64, [f64; 2])> = None;
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
if !solid.edges.iter().any(|edge| edge.id == coedge.edge_id) {
continue;
}
let Ok([s0, s1]) = coedge.pcurve.domain() else {
continue;
};
let samples: Vec<([f64; 2], f64)> = (0..=PCURVE_SAMPLES)
.filter_map(|k| {
let s = s0 + (s1 - s0) * k as f64 / PCURVE_SAMPLES as f64;
coedge.pcurve.evaluate(s).ok().map(|uv| ([uv.x, uv.y], s))
})
.collect();
if samples.len() < 2 {
continue;
}
for &du in &shifts_u {
for &dv in &shifts_v {
for pair in samples.windows(2) {
let c = [pair[0].0[0] + du, pair[0].0[1] + dv];
let d = [pair[1].0[0] + du, pair[1].0[1] + dv];
let Some((lambda, mu)) = segments_cross(from, to, c, d) else {
continue;
};
let better = match best {
None => true,
Some((_, found, _, _)) => {
if leaving {
lambda < found
} else {
lambda > found
}
}
};
if better {
let s = pair[0].1 + (pair[1].1 - pair[0].1) * mu;
let uv = [c[0] + (d[0] - c[0]) * mu, c[1] + (d[1] - c[1]) * mu];
best = Some((coedge.edge_id, lambda, s, uv));
}
}
}
}
}
}
best
}
fn face_across<'a>(solid: &'a BrepSolid, face: &FaceRecord, edge_id: u64) -> Option<&'a FaceRecord> {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|other| {
other.id != face.id
&& other
.loops
.iter()
.flat_map(|lp| &lp.coedges)
.any(|coedge| coedge.edge_id == edge_id)
})
}
fn tangent_at(solid: &BrepSolid, face: &FaceRecord, neighbour: &FaceRecord, point: Vec3) -> bool {
let angular = crate::KernelTolerances::for_solid(solid, 1e-7).angular;
match (
crate::fillet::face_plane_normal(face, point),
crate::fillet::face_plane_normal(neighbour, point),
) {
(Ok((_, here)), Ok((_, there))) => here.dot(there) > 0.0 && here.cross(there).length() <= angular,
_ => false,
}
}
pub(super) fn tangent_crossings(
solid: &BrepSolid,
stations: &[Station],
mates: [&BlendMate<'_>; 2],
radius: f64,
) -> Result<Vec<RailCrossing>, KernelRefusal> {
let mut found = Vec::new();
for (side, mate) in mates.into_iter().enumerate() {
let face = mate.face;
let classes: Vec<bool> = stations.iter().map(|station| inside(face, track_uv(station, side))).collect();
for index in 0..stations.len().saturating_sub(1) {
let (before, after) = (classes[index], classes[index + 1]);
if before == after {
continue;
}
let leaving = before && !after;
let from = wrapped_uv(face, track_uv(&stations[index], side));
let to = wrapped_uv(face, track_uv(&stations[index + 1], side));
let straddles = |closed: bool, a: f64, b: f64, domain: Result<[f64; 2], String>| -> bool {
closed && domain.map(|[low, high]| (a - b).abs() > 0.5 * (high - low)).unwrap_or(true)
};
let (closed_u, closed_v) = face.surface.closed_directions().or_refuse(KernelStage::Classify, "closed_directions")?;
if straddles(closed_u, from[0], to[0], face.surface.domain_u())
|| straddles(closed_v, from[1], to[1], face.surface.domain_v())
{
continue;
}
let Some((edge_id, fraction, edge_parameter, uv)) = boundary_hit(solid, face, from, to, leaving) else {
continue;
};
let Some(neighbour) = face_across(solid, face, edge_id) else {
continue;
};
let point = face.surface.evaluate(uv[0], uv[1]).or_refuse(KernelStage::Classify, "evaluate")?;
if !tangent_at(solid, face, neighbour, point) {
continue;
}
if !crate::fillet::neighbour_carries_ball(solid, face, neighbour, radius) {
continue;
}
found.push(RailCrossing {
side,
mate_face_id: face.id,
edge_id,
neighbour_face_id: neighbour.id,
stations: [index, index + 1],
leaving,
point,
edge_parameter,
fraction,
solved: None,
});
}
}
Ok(found)
}
pub(super) fn edge_parameter_near(edge: &EdgeRecord, point: Vec3) -> Result<f64, KernelRefusal> {
const SAMPLES: usize = 256;
let span = edge.t1 - edge.t0;
let mut best = (edge.t0, f64::INFINITY);
for k in 0..=SAMPLES {
let t = edge.t0 + span * k as f64 / SAMPLES as f64;
let distance = edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?.sub(point).length();
if distance < best.1 {
best = (t, distance);
}
}
let mut t = best.0;
for _ in 0..12 {
let derivatives = edge.curve.derivatives_extended(t, 2).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let offset = point.sub(derivatives[0]);
let value = offset.dot(derivatives[1]);
let slope = offset.dot(derivatives[2]) - derivatives[1].dot(derivatives[1]);
if slope.abs() <= 1e-18 {
break;
}
let step = value / slope;
t -= step;
if step.abs() <= 1e-14 * (1.0 + span.abs()) {
break;
}
}
Ok(t)
}
pub(super) fn solve_crossing(
edge: &EdgeRecord,
mates: [&BlendMate<'_>; 2],
radius_at: &dyn Fn(f64) -> f64,
stations: &[Station],
crossing: &RailCrossing,
scale: f64,
) -> Result<(f64, Station), KernelRefusal> {
let side = crossing.side;
let other = 1 - side;
let mate = mates[side];
let pcurve = &mate
.face
.loops
.iter()
.flat_map(|lp| &lp.coedges)
.find(|coedge| coedge.edge_id == crossing.edge_id)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "crossed_edge_pcurve", "blend rollover: the crossed edge has no pcurve on the mate"))?
.pcurve;
let surfaces = [&mates[0].face.surface, &mates[1].face.surface];
let signs = [mates[0].rho.signum(), mates[1].rho.signum()];
let [before, after] = crossing.stations;
let seed_other = {
let a = track_uv(&stations[before], other);
let b = track_uv(&stations[after], other);
[a[0] + (b[0] - a[0]) * crossing.fraction, a[1] + (b[1] - a[1]) * crossing.fraction]
};
let mut x = [
edge_parameter_near(edge, crossing.point)?,
crossing.edge_parameter,
seed_other[0],
seed_other[1],
];
let assemble = |x: &[f64; 4]| -> Result<[f64; 4], KernelRefusal> {
let on_edge = pcurve.evaluate(x[1]).or_refuse(KernelStage::Refine, "pcurve")?;
let mut uv = [0.0; 4];
uv[2 * side] = on_edge.x;
uv[2 * side + 1] = on_edge.y;
uv[2 * other] = x[2];
uv[2 * other + 1] = x[3];
Ok(uv)
};
let state_at = |x: &[f64; 4]| -> Result<super::stations::TangencyState, KernelRefusal> {
let (section_point, section_tangent) = edge
.curve
.point_and_unit_tangent_extended(x[0], edge.t0, edge.t1)
.map_err(|error| format!("blend rollover: edge {}: {error}", edge.id))
.or_refuse(KernelStage::Refine, "point_and_unit_tangent_extended")?;
let radius = radius_at(x[0]);
tangency_state(
surfaces[0],
surfaces[1],
[signs[0] * radius, signs[1] * radius],
assemble(x)?,
section_point,
section_tangent,
)
};
let tolerance = 1e-11 * (1.0 + scale);
for _ in 0..NEWTON_ITERATIONS {
let state = state_at(&x)?;
let error = state.residual.iter().map(|value| value.abs()).fold(0.0, f64::max);
if error <= tolerance {
let uv = assemble(&x)?;
let cos_alpha = signs[0] * signs[1] * state.n1.dot(state.n2);
let weight = ((1.0 + cos_alpha) * 0.5).max(0.0).sqrt();
if weight <= 1e-6 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "tangent_faces", "blend: faces are tangent at a station (α = π)"));
}
let apex = apex_point(state.p1, state.n1, state.p2, state.n2, state.center)?;
return Ok((
x[0],
Station {
uv1: [uv[0], uv[1]],
uv2: [uv[2], uv[3]],
p1: state.p1,
p2: state.p2,
center: state.center,
weight,
apex,
},
));
}
let mut jacobian = [[0.0f64; 4]; 4];
let step = 1e-7;
for column in 0..4 {
let mut probe = x;
probe[column] += step;
let probed = state_at(&probe)?;
for row in 0..4 {
jacobian[row][column] = (probed.residual[row] - state.residual[row]) / step;
}
}
let delta = fit::solve_small::<4>(jacobian, state.residual, 4)
.map_err(|error| format!("blend rollover: the frozen-rail Newton is singular ({error})"))
.or_refuse(KernelStage::Refine, "solve_small")?;
for (value, correction) in x.iter_mut().zip(delta) {
*value -= correction;
}
}
Err(KernelRefusal::non_convergence(KernelStage::Refine, "rollover_crossing", "blend rollover: the crossing station did not converge"))
}
pub(super) fn seed_on_neighbour(
solid: &BrepSolid,
edge: &EdgeRecord,
mates: [&BlendMate<'_>; 2],
radius_at: &dyn Fn(f64) -> f64,
crossing: &RailCrossing,
scale: f64,
) -> Result<(f64, Station), KernelRefusal> {
let (t_cross, station) = crossing
.solved
.as_ref()
.ok_or(KernelRefusal::internal(KernelStage::Refine, "rollover_unsolved", "blend rollover: the crossing station did not solve"))?;
let side = crossing.side;
let other = 1 - side;
let neighbour = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == crossing.neighbour_face_id)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "rollover_neighbour", "blend rollover: the neighbour face vanished"))?;
let mate = mates[side];
let on_mate = track_uv(station, side);
let projection = crate::project_point_to_surface(&neighbour.surface, crossing.point)
.or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let mate_raw = raw_normal(&mate.face.surface, on_mate[0], on_mate[1])?;
let neighbour_raw = raw_normal(&neighbour.surface, projection.u, projection.v)?;
let flip = if mate_raw.dot(neighbour_raw) < 0.0 { -1.0 } else { 1.0 };
let signs = {
let mut signs = [mates[0].rho.signum(), mates[1].rho.signum()];
signs[side] *= flip;
signs
};
let mut surfaces = [&mates[0].face.surface, &mates[1].face.surface];
surfaces[side] = &neighbour.surface;
let step = (edge.t1 - edge.t0) / 64.0;
let t = if crossing.leaving { t_cross + step } else { t_cross - step };
let (section_point, section_tangent) = edge
.curve
.point_and_unit_tangent_extended(t, edge.t0, edge.t1)
.map_err(|error| format!("blend rollover: edge {}: {error}", edge.id))
.or_refuse(KernelStage::Refine, "point_and_unit_tangent_extended")?;
let radius = radius_at(t);
let mut seed = [0.0; 4];
seed[2 * side] = projection.u;
seed[2 * side + 1] = projection.v;
let other_uv = track_uv(station, other);
seed[2 * other] = other_uv[0];
seed[2 * other + 1] = other_uv[1];
let rho = [signs[0] * radius, signs[1] * radius];
let uv = solve_station(surfaces[0], surfaces[1], rho, seed, section_point, section_tangent, scale)?;
let state = tangency_state(surfaces[0], surfaces[1], rho, uv, section_point, section_tangent)?;
let cos_alpha = signs[0] * signs[1] * state.n1.dot(state.n2);
let weight = ((1.0 + cos_alpha) * 0.5).max(0.0).sqrt();
if weight <= 1e-6 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "tangent_faces", "blend: faces are tangent at a station (α = π)"));
}
let apex = apex_point(state.p1, state.n1, state.p2, state.n2, state.center)?;
Ok((
t,
Station {
uv1: [uv[0], uv[1]],
uv2: [uv[2], uv[3]],
p1: state.p1,
p2: state.p2,
center: state.center,
weight,
apex,
},
))
}
pub(super) fn split_closed_at_crossings(
stations: &[Station],
mates: [&BlendMate<'_>; 2],
crossings: &[RailCrossing],
) -> Vec<StripePiece> {
if crossings.is_empty() || crossings.iter().any(|crossing| crossing.solved.is_none()) {
return Vec::new();
}
let mate_ids = [mates[0].face.id, mates[1].face.id];
let count = crossings.len();
let mut pieces = Vec::with_capacity(count);
for index in 0..count {
let from = &crossings[index];
let to = &crossings[(index + 1) % count];
let mut faces = mate_ids;
if from.leaving {
faces[from.side] = from.neighbour_face_id;
}
let mut piece = vec![from.solved.as_ref().map(|(_, station)| *station).expect("solved")];
let [_, start] = from.stations;
let [end, _] = to.stations;
if index + 1 < count {
piece.extend_from_slice(&stations[start..=end]);
} else {
piece.extend_from_slice(&stations[start..stations.len() - 1]);
piece.extend_from_slice(&stations[..=end]);
}
piece.push(to.solved.as_ref().map(|(_, station)| *station).expect("solved"));
pieces.push(StripePiece {
faces,
stations: piece,
from: index,
to: (index + 1) % count,
});
}
pieces
}
pub(super) fn plan_closed(
solid: &BrepSolid,
edge: &EdgeRecord,
stations: &[Station],
mates: [&BlendMate<'_>; 2],
radius_at: &dyn Fn(f64) -> f64,
scale: f64,
) -> Result<Option<RolloverPlan>, KernelRefusal> {
let radius = radius_at(0.5 * (edge.t0 + edge.t1)).abs();
let mut crossings = tangent_crossings(solid, stations, mates, radius)?;
if crossings.is_empty() {
return Ok(None);
}
for crossing in &mut crossings {
match solve_crossing(edge, mates, radius_at, stations, crossing, scale) {
Ok((t, station)) => {
crossing.point = if crossing.side == 0 { station.p1 } else { station.p2 };
crossing.solved = Some((t, station));
}
Err(error) => {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("closed-rim rollover: crossing station did not solve: {error}");
}
}
}
}
let pieces = split_closed_at_crossings(stations, mates, &crossings);
let rolled = crossings
.iter()
.map(|crossing| {
if crossing.solved.is_none() {
return None;
}
match seed_on_neighbour(solid, edge, mates, radius_at, crossing, scale) {
Ok(seeded) => Some(seeded),
Err(error) => {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("closed-rim rollover: the station on the neighbour did not solve: {error}");
}
None
}
}
})
.collect();
Ok(Some(RolloverPlan { crossings, pieces, rolled }))
}
pub(super) struct RolledPiece {
pub(super) faces: [u64; 2],
pub(super) stations: Vec<Station>,
#[allow(dead_code)]
pub(super) ts: Vec<f64>,
pub(super) rows: FittedRows,
pub(super) from: usize,
pub(super) to: usize,
pub(super) rolled_side: Option<usize>,
pub(super) seam_station: Option<usize>,
}
const ROLLED_PIECE_MIN_INTERVALS: usize = 8;
const ROLLED_PIECE_STATION_CEILING: usize = (SEED_INTERVALS << 6) + 1;
struct PieceFrame<'a> {
edge: &'a EdgeRecord,
surfaces: [&'a NurbsSurface; 2],
signs: [f64; 2],
radius_at: &'a dyn Fn(f64) -> f64,
scale: f64,
}
impl PieceFrame<'_> {
fn rho(&self, t: f64) -> [f64; 2] {
let radius = (self.radius_at)(t);
[self.signs[0] * radius, self.signs[1] * radius]
}
fn section(&self, t: f64) -> Result<(Vec3, Vec3), KernelRefusal> {
self.edge
.curve
.point_and_unit_tangent_extended(t, self.edge.t0, self.edge.t1)
.map_err(|error| format!("blend rollover: edge {}: {error}", self.edge.id))
.or_refuse(KernelStage::Refine, "point_and_unit_tangent_extended")
}
fn station(&self, t: f64, uv: [f64; 4]) -> Result<Station, KernelRefusal> {
let (section_point, section_tangent) = self.section(t)?;
let state = tangency_state(self.surfaces[0], self.surfaces[1], self.rho(t), uv, section_point, section_tangent)?;
let cos_alpha = self.signs[0] * self.signs[1] * state.n1.dot(state.n2);
let weight = ((1.0 + cos_alpha) * 0.5).max(0.0).sqrt();
if weight <= 1e-6 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "tangent_faces", "blend: faces are tangent at a station (α = π)"));
}
let apex = apex_point(state.p1, state.n1, state.p2, state.n2, state.center)?;
Ok(Station {
uv1: [uv[0], uv[1]],
uv2: [uv[2], uv[3]],
p1: state.p1,
p2: state.p2,
center: state.center,
weight,
apex,
})
}
fn continue_from(&self, left: &Continued, t: f64) -> Result<(Continued, Station), KernelRefusal> {
let rho_at = |t: f64| self.rho(t);
let solve = |t: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3), KernelRefusal> {
let (section_point, section_tangent) = self.section(t)?;
let uv = solve_station(self.surfaces[0], self.surfaces[1], self.rho(t), seed, section_point, section_tangent, self.scale)?;
let state = tangency_state(self.surfaces[0], self.surfaces[1], self.rho(t), uv, section_point, section_tangent)?;
Ok((uv, state.center))
};
let hop = |left: &Continued, right: &Continued| branch_hop(self.surfaces, self.edge, &rho_at, left, right);
let node = continue_station(&solve, &hop, left, t, 0)?;
let station = self.station(node.t, node.uv)?;
Ok((node, station))
}
}
fn unwrap_track(stations: &mut [Station], side: usize, surface: &NurbsSurface) -> Result<(), KernelRefusal> {
let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let periods = [
closed_u.then(|| surface.domain_u().map(|[a, b]| b - a)).transpose().or_refuse(KernelStage::Refine, "domain_u")?,
closed_v.then(|| surface.domain_v().map(|[a, b]| b - a)).transpose().or_refuse(KernelStage::Refine, "domain_v")?,
];
for index in 1..stations.len() {
let previous = track_uv(&stations[index - 1], side);
let mut current = track_uv(&stations[index], side);
for (axis, period) in periods.iter().enumerate() {
if let Some(period) = period {
if *period > 0.0 {
let laps = ((current[axis] - previous[axis]) / period).round();
current[axis] -= laps * period;
}
}
}
if side == 0 {
stations[index].uv1 = current;
} else {
stations[index].uv2 = current;
}
}
Ok(())
}
fn unwrap_parameters(ts: &mut [f64], span: f64) {
for index in 1..ts.len() {
while ts[index] < ts[index - 1] - 0.5 * span {
ts[index] += span;
}
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn march_pieces(
solid: &BrepSolid,
edge: &EdgeRecord,
plan: &RolloverPlan,
stations: &[Station],
mates: [&BlendMate<'_>; 2],
radius_at: &dyn Fn(f64) -> f64,
scale: f64,
rail_bar: f64,
chamfer: bool,
anchor_u: Option<f64>,
) -> Result<Vec<RolledPiece>, KernelRefusal> {
if plan.pieces.is_empty() {
return Err(KernelRefusal::unsupported(
KernelStage::Refine,
TANGENT_ROLLOVER_WHAT,
"blend rollover: a crossing station did not solve, so the stripe was not split",
));
}
let span = edge.t1 - edge.t0;
let mate_ids = [mates[0].face.id, mates[1].face.id];
let face_by_id = |id: u64| -> Result<&FaceRecord, KernelRefusal> {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == id)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "rollover_carrier", "blend rollover: a piece's carrier face vanished"))
};
let count = plan.pieces.len();
let mut pieces = Vec::with_capacity(count);
for (index, piece) in plan.pieces.iter().enumerate() {
let from = &plan.crossings[piece.from];
let to = &plan.crossings[piece.to];
let (t_from, from_station) = from.solved.as_ref().expect("the plan holds solved crossings only");
let (t_to, to_station) = to.solved.as_ref().expect("the plan holds solved crossings only");
let (t_from, mut t_to) = (*t_from, *t_to);
if t_to <= t_from {
t_to += span;
}
let rolled_side = (0..2).find(|&side| piece.faces[side] != mate_ids[side]);
let faces = [face_by_id(piece.faces[0])?, face_by_id(piece.faces[1])?];
let surfaces = [&faces[0].surface, &faces[1].surface];
let mut signs = [mates[0].rho.signum(), mates[1].rho.signum()];
if let Some(side) = rolled_side {
let on_mate = track_uv(from_station, side);
let mate_raw = raw_normal(&mates[side].face.surface, on_mate[0], on_mate[1])?;
let projection = crate::project_point_to_surface(surfaces[side], from.point)
.or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let neighbour_raw = raw_normal(surfaces[side], projection.u, projection.v)?;
if mate_raw.dot(neighbour_raw) < 0.0 {
signs[side] = -signs[side];
}
}
let frame = PieceFrame { edge, surfaces, signs, radius_at, scale };
let on_carriers = |station: &Station, point: Vec3| -> Result<Station, KernelRefusal> {
let Some(side) = rolled_side else { return Ok(*station) };
let projection = crate::project_point_to_surface(surfaces[side], point)
.or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let mut moved = *station;
if side == 0 {
moved.uv1 = [projection.u, projection.v];
} else {
moved.uv2 = [projection.u, projection.v];
}
Ok(moved)
};
let first = on_carriers(from_station, from.point)?;
let last = on_carriers(to_station, to.point)?;
let (mut marched, mut ts): (Vec<Station>, Vec<f64>) = match rolled_side {
None => {
let mut ts = Vec::with_capacity(piece.stations.len());
for (k, station) in piece.stations.iter().enumerate() {
ts.push(if k == 0 {
t_from
} else if k + 1 == piece.stations.len() {
t_to
} else {
edge_parameter_near(edge, station.center)?
});
}
unwrap_parameters(&mut ts, span);
let mut stations = piece.stations.clone();
let n = stations.len();
stations[0] = first;
stations[n - 1] = last;
while stations.len() - 1 < ROLLED_PIECE_MIN_INTERVALS {
let mut denser = Vec::with_capacity(2 * stations.len());
let mut denser_ts = Vec::with_capacity(denser.capacity());
for k in 0..stations.len() - 1 {
denser.push(stations[k]);
denser_ts.push(ts[k]);
let left = Continued {
t: ts[k],
uv: [stations[k].uv1[0], stations[k].uv1[1], stations[k].uv2[0], stations[k].uv2[1]],
center: Some(stations[k].center),
};
let (node, station) = frame.continue_from(&left, 0.5 * (ts[k] + ts[k + 1]))?;
denser.push(station);
denser_ts.push(node.t);
}
denser.push(*stations.last().expect("stations"));
denser_ts.push(*ts.last().expect("parameters"));
stations = denser;
ts = denser_ts;
}
(stations, ts)
}
Some(_) => {
let fraction = (t_to - t_from) / span;
let intervals = ((stations.len().saturating_sub(1)) as f64 * fraction).ceil().max(ROLLED_PIECE_MIN_INTERVALS as f64) as usize;
let mut stations = vec![first];
let mut ts = vec![t_from];
let seed_uv = [first.uv1[0], first.uv1[1], first.uv2[0], first.uv2[1]];
let mut left = Continued { t: t_from, uv: seed_uv, center: Some(first.center) };
{
let check = frame.station(t_from, seed_uv)?;
let agreement = check.center.sub(first.center).length().max(check.p1.sub(first.p1).length()).max(check.p2.sub(first.p2).length());
if agreement > 1e-6 * scale {
return Err(KernelRefusal::non_convergence(
KernelStage::Refine,
"rollover_rib",
format!("blend rollover: the crossing station re-read on the neighbour moves by {agreement:.3e}"),
));
}
}
for k in 1..intervals {
let t = t_from + (t_to - t_from) * k as f64 / intervals as f64;
let (node, station) = frame.continue_from(&left, t)?;
stations.push(station);
ts.push(node.t);
left = node;
}
stations.push(last);
ts.push(t_to);
(stations, ts)
}
};
for side in 0..2 {
unwrap_track(&mut marched, side, surfaces[side])?;
}
let seam_station = match anchor_u {
Some(_) if index + 1 == count && rolled_side.is_none() => {
let [_, start] = from.stations;
Some(1 + (stations.len() - 1 - start))
}
_ => None,
};
let mut rows = None;
for round in 0..=REFINE_ROUNDS {
let parameters = station_parameters(&marched);
let fitted = super::edge::fit_open_rows(&marched, ¶meters, true, None, None)?;
let misses = closed_rail_interval_misses(&fitted, &marched, ¶meters, surfaces)?;
let failing: Vec<usize> = misses.iter().enumerate().filter(|(_, &miss)| !(miss <= rail_bar)).map(|(k, _)| k).collect();
let worst = misses.iter().copied().fold(0.0_f64, f64::max);
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"closed-rim rollover: piece {index} ({}) round {round}: {} stations, {} interval(s) over the rail bar {rail_bar:.3e}, worst {worst:.3e}",
if rolled_side.is_some() { "rolled" } else { "on the mates" },
marched.len(),
failing.len()
);
}
if failing.is_empty() {
rows = Some(());
break;
}
if round == REFINE_ROUNDS || marched.len() + failing.len() > ROLLED_PIECE_STATION_CEILING {
return Err(KernelRefusal::non_convergence(
KernelStage::Refine,
super::miter::MARCHED_FIT_OFF_CARRIERS_WHAT,
format!(
"blend rollover: piece {index}'s rails stand {worst:.9e} off their carriers (bar {rail_bar:.9e}) after {} stations and {round} local rounds; bounded refinement exhausted",
marched.len()
),
));
}
let mut refined = Vec::with_capacity(marched.len() + failing.len());
let mut refined_ts = Vec::with_capacity(refined.capacity());
for k in 0..marched.len() - 1 {
refined.push(marched[k]);
refined_ts.push(ts[k]);
if failing.contains(&k) {
let left = Continued {
t: ts[k],
uv: [marched[k].uv1[0], marched[k].uv1[1], marched[k].uv2[0], marched[k].uv2[1]],
center: Some(marched[k].center),
};
let (node, station) = frame.continue_from(&left, 0.5 * (ts[k] + ts[k + 1]))?;
refined.push(station);
refined_ts.push(node.t);
}
}
refined.push(*marched.last().expect("a piece has stations"));
refined_ts.push(*ts.last().expect("a piece has parameters"));
marched = refined;
ts = refined_ts;
for side in 0..2 {
unwrap_track(&mut marched, side, surfaces[side])?;
}
}
debug_assert!(rows.is_some());
let seam_station = match (seam_station, anchor_u) {
(Some(_), Some(lock)) => {
let period = {
let [a, b] = surfaces[0].domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
b - a
};
marched.iter().enumerate().skip(1).find(|(k, station)| {
*k + 1 < marched.len() && ((station.uv1[0] - lock) / period - ((station.uv1[0] - lock) / period).round()).abs() * period <= 1e-9 * (1.0 + period)
}).map(|(k, _)| k)
}
_ => None,
};
let parameters = station_parameters(&marched);
let mut rows = super::edge::fit_open_rows(&marched, ¶meters, chamfer, None, None)?;
rows.cr_pcurve = super::edge::refit_closed_support_pcurve(&rows.cr, &rows.cr_pcurve, surfaces[0])?;
rows.cs_pcurve = super::edge::refit_closed_support_pcurve(&rows.cs, &rows.cs_pcurve, surfaces[1])?;
pieces.push(RolledPiece {
faces: piece.faces,
stations: marched,
ts,
rows,
from: piece.from,
to: piece.to,
rolled_side,
seam_station,
});
}
Ok(pieces)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build(
solid: &BrepSolid,
edge: &EdgeRecord,
mates: [&BlendMate<'_>; 2],
plan: &RolloverPlan,
pieces: Vec<RolledPiece>,
anchor_u: Option<f64>,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let built = super::rollover_surgery::rollover_surgery(solid, edge, mates, plan, &pieces, anchor_u, name);
built
}
pub(super) fn refusal(plan: &RolloverPlan, radius: f64) -> KernelRefusal {
let crossings = &plan.crossings;
let first = &crossings[0];
let more = match crossings.len() {
1 => String::new(),
n => format!(" and {} more crossing(s)", n - 1),
};
let solved = if first.solved.is_some() { "the crossing station solved" } else { "located by the bracket's chord" };
let rolled = plan.rolled.iter().filter(|seed| seed.is_some()).count();
KernelRefusal::unsupported(
KernelStage::Classify,
TANGENT_ROLLOVER_WHAT,
format!(
"blend: the radius {radius} rail on face {} crosses the tangent edge {} onto face {} at \
({:.6}, {:.6}, {:.6}){more} ({solved}; {} piece(s); {rolled} seeded on the neighbour): re-marching the rolled pieces on the \
neighbour and sewing them on the shared rib is not built, \
so this lane declines",
first.mate_face_id, first.edge_id, first.neighbour_face_id, first.point.x, first.point.y, first.point.z,
plan.pieces.len()
),
)
}