use crate::{KernelRefusal, KernelStage, OrRefuse};
use crate::fit::solve_dense;
use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, VertexRecord};
use crate::{
interpolate_curve, measure_edge_against_pcurve_image,
measure_surface_fit_against_pointwise_offset, offset_construction_band, solid_model_scale,
vertex_endpoint_gap, vertex_tolerance_from_edges, KnotVector, MeasuredTolerance, NurbsCurve,
NurbsSurface, OffsetEvaluator, OffsetNormal, Vec2, Vec3, Vec4,
};
use rustc_hash::FxHashMap as HashMap;
use serde::Serialize;
fn domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
Ok((
KnotVector::new(surface.knots_u.clone(), surface.degree_u).or_refuse(KernelStage::Refine, "new")?.domain(),
KnotVector::new(surface.knots_v.clone(), surface.degree_v).or_refuse(KernelStage::Refine, "new")?.domain(),
))
}
fn stable_face_normal(face: &FaceRecord, u: f64, v: f64) -> Result<Vec3, KernelRefusal> {
OffsetEvaluator::new(
"offset_surface",
&face.surface,
OffsetNormal::FaceStable {
same_sense: face.same_sense,
},
)
.normal(u, v)
.or_refuse(KernelStage::Refine, "normal")
}
fn greville_parameters(knots: &KnotVector) -> Vec<f64> {
let mut parameters = (0..knots.control_point_count())
.map(|index| {
knots.knots[index + 1..=index + knots.degree]
.iter()
.sum::<f64>()
/ knots.degree as f64
})
.collect::<Vec<_>>();
let domain = knots.domain();
parameters[0] = domain[0];
*parameters.last_mut().unwrap() = domain[1];
parameters
}
fn collocation_matrix(knots: &KnotVector, parameters: &[f64], weights: &[f64]) -> Vec<Vec<f64>> {
parameters
.iter()
.map(|parameter| {
let mut row = vec![0.0; knots.control_point_count()];
let span = knots.find_span(*parameter);
for (offset, value) in knots
.basis_functions(span, *parameter)
.into_iter()
.enumerate()
{
let index = span - knots.degree + offset;
row[index] = value * weights[index];
}
let denominator: f64 = row.iter().sum();
if denominator.abs() > 0.0 {
for value in &mut row {
*value /= denominator;
}
}
row
})
.collect()
}
fn separable_weights(weights: &[Vec<f64>]) -> Option<(Vec<f64>, Vec<f64>)> {
let first_row = weights.first()?;
let anchor = *first_row.first()?;
if anchor.abs() <= 1e-12 {
return None;
}
let a: Vec<f64> = weights.iter().map(|row| row[0]).collect();
let b: Vec<f64> = first_row.iter().map(|w| w / anchor).collect();
for (i, row) in weights.iter().enumerate() {
for (j, &w) in row.iter().enumerate() {
if (w - a[i] * b[j]).abs() > 1e-10 * (1.0 + w.abs()) {
return None;
}
}
}
Some((a, b))
}
fn interpolate_tensor(
knot_u: &KnotVector,
knot_v: &KnotVector,
parameters_u: &[f64],
parameters_v: &[f64],
samples: &[Vec<Vec3>],
weights: &[Vec<f64>],
) -> Result<Vec<Vec<Vec4>>, KernelRefusal> {
let count_u = parameters_u.len();
let count_v = parameters_v.len();
if let Some((weights_u, weights_v)) = separable_weights(weights) {
let matrix_u = collocation_matrix(knot_u, parameters_u, &weights_u);
let matrix_v = collocation_matrix(knot_v, parameters_v, &weights_v);
let mut intermediate = vec![vec![Vec3::default(); count_v]; count_u];
for column in 0..count_v {
let solve_axis = |axis: fn(Vec3) -> f64| {
solve_dense(
matrix_u.clone(),
samples.iter().map(|row| axis(row[column])).collect(),
)
};
let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
for row in 0..count_u {
intermediate[row][column] = Vec3::new(x[row], y[row], z[row]);
}
}
let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
for row in 0..count_u {
let solve_axis = |axis: fn(Vec3) -> f64| {
solve_dense(
matrix_v.clone(),
intermediate[row].iter().copied().map(axis).collect(),
)
};
let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
for column in 0..count_v {
controls[row][column] = Vec4::from_point(
Vec3::new(x[column], y[column], z[column]),
weights[row][column],
);
}
}
return Ok(controls);
}
let unknowns = count_u * count_v;
let mut matrix = vec![vec![0.0; unknowns]; unknowns];
for (k, &u) in parameters_u.iter().enumerate() {
let span_u = knot_u.find_span(u);
let basis_u = knot_u.basis_functions(span_u, u);
for (l, &v) in parameters_v.iter().enumerate() {
let span_v = knot_v.find_span(v);
let basis_v = knot_v.basis_functions(span_v, v);
let row = &mut matrix[k * count_v + l];
let mut denominator = 0.0;
for (du, value_u) in basis_u.iter().enumerate() {
let i = span_u - knot_u.degree + du;
for (dv, value_v) in basis_v.iter().enumerate() {
let j = span_v - knot_v.degree + dv;
let entry = value_u * value_v * weights[i][j];
row[i * count_v + j] = entry;
denominator += entry;
}
}
if denominator.abs() > 0.0 {
for value in row.iter_mut() {
*value /= denominator;
}
}
}
}
let solve_axis = |axis: fn(Vec3) -> f64| {
solve_dense(
matrix.clone(),
samples
.iter()
.flat_map(|row| row.iter().copied().map(axis))
.collect(),
)
};
let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
for row in 0..count_u {
for column in 0..count_v {
let index = row * count_v + column;
controls[row][column] = Vec4::from_point(
Vec3::new(x[index], y[index], z[index]),
weights[row][column],
);
}
}
Ok(controls)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OffsetSurfaceLane {
Affine,
Analytic,
#[default]
Fit,
Reparameterised,
}
#[derive(Clone, Debug)]
pub struct MeasuredOffsetSurface {
pub surface: NurbsSurface,
pub lane: OffsetSurfaceLane,
pub fit: Option<MeasuredTolerance>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CarrierExtension {
pub u_min: f64,
pub u_max: f64,
pub v_min: f64,
pub v_max: f64,
}
impl CarrierExtension {
pub const NONE: CarrierExtension = CarrierExtension {
u_min: 0.0,
u_max: 0.0,
v_min: 0.0,
v_max: 0.0,
};
pub fn uniform(amount: f64) -> CarrierExtension {
CarrierExtension {
u_min: amount,
u_max: amount,
v_min: amount,
v_max: amount,
}
}
pub fn is_active(&self) -> bool {
self.u_min > 0.0 || self.u_max > 0.0 || self.v_min > 0.0 || self.v_max > 0.0
}
fn extends_v(&self) -> bool {
self.v_min > 0.0 || self.v_max > 0.0
}
}
fn trim_domain_fractions(
face: &FaceRecord,
[u0, u1]: [f64; 2],
[v0, v1]: [f64; 2],
) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
let mut low = Vec2 {
x: f64::INFINITY,
y: f64::INFINITY,
};
let mut high = Vec2 {
x: f64::NEG_INFINITY,
y: f64::NEG_INFINITY,
};
for coedge in face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
for sample in 0..=24 {
let uv = coedge
.pcurve
.evaluate(p0 + (p1 - p0) * sample as f64 / 24.0).or_refuse(KernelStage::Refine, "evaluate")?;
low.x = low.x.min(uv.x);
low.y = low.y.min(uv.y);
high.x = high.x.max(uv.x);
high.y = high.y.max(uv.y);
}
}
let fraction = |value: f64, start: f64, end: f64| {
let span = end - start;
if span.abs() <= f64::EPSILON || !value.is_finite() {
return None;
}
Some(((value - start) / span).clamp(0.0, 1.0))
};
let u = match (fraction(low.x, u0, u1), fraction(high.x, u0, u1)) {
(Some(a), Some(b)) if b - a > 1e-6 => [a, b],
_ => [0.0, 1.0],
};
let v = match (fraction(low.y, v0, v1), fraction(high.y, v0, v1)) {
(Some(a), Some(b)) if b - a > 1e-6 => [a, b],
_ => [0.0, 1.0],
};
Ok((u, v))
}
fn domain_end_moves(grow_min: f64, grow_max: f64, [ta, tb]: [f64; 2]) -> (f64, f64) {
let span = tb - ta;
if span <= 1e-6 {
return (grow_min, grow_max);
}
let rate = (grow_min + grow_max) / span;
let back = grow_min + ta * rate;
let forward = (1.0 - ta) * rate - grow_min;
(back.max(0.0), forward.max(0.0))
}
pub fn offset_surface(
face: &FaceRecord,
distance: f64,
planar_extension: f64,
) -> Result<NurbsSurface, KernelRefusal> {
offset_surface_with_lane(face, distance, &CarrierExtension::uniform(planar_extension))
.map(|(surface, _)| surface)
}
pub fn offset_surface_measured(
face: &FaceRecord,
distance: f64,
planar_extension: f64,
band: f64,
) -> Result<MeasuredOffsetSurface, KernelRefusal> {
offset_surface_measured_sided(
face,
distance,
&CarrierExtension::uniform(planar_extension),
band,
)
}
pub fn offset_surface_measured_sided(
face: &FaceRecord,
distance: f64,
extension: &CarrierExtension,
band: f64,
) -> Result<MeasuredOffsetSurface, KernelRefusal> {
let (surface, lane, extended) = offset_surface_constructed(face, distance, extension)?;
let fit = match lane {
OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
OffsetSurfaceLane::Analytic | OffsetSurfaceLane::Fit => {
Some(measure_surface_fit_against_pointwise_offset(
extended.as_ref().unwrap_or(&face.surface),
face.same_sense,
&surface,
distance,
band,
)?)
}
OffsetSurfaceLane::Reparameterised => None,
};
Ok(MeasuredOffsetSurface { surface, lane, fit })
}
fn sphere_offset_surface(face: &FaceRecord, distance: f64) -> Result<Option<NurbsSurface>, KernelRefusal> {
if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
return Ok(None);
}
let source = &face.surface;
let Some((centre, radius)) = source.analytic().and_then(|shape| shape.sphere_geometry()) else {
return Ok(None);
};
let ([u0, u1], [v0, v1]) = domains(source)?;
let Ok(sample) = OffsetEvaluator::new(
"offset_surface",
source,
OffsetNormal::FaceStable {
same_sense: face.same_sense,
},
)
.at((u0 + u1) / 2.0, (v0 + v1) / 2.0, -distance)
else {
return Ok(None);
};
let source_radial = sample.source.sub(centre);
let offset_radial = sample.point.sub(centre);
let offset_radius = offset_radial.length();
let scale = radius.max(1.0);
if radius <= 1e-12
|| offset_radius <= 1e-9 * scale
|| offset_radial.dot(source_radial) <= 0.0
|| ((offset_radius - radius).abs() - distance.abs()).abs() > 1e-9 * scale
{
return Ok(None);
}
let factor = offset_radius / radius;
let Ok(controls) = source
.control_points
.iter()
.map(|row| {
row.iter()
.map(|control| {
Ok(Vec4::from_point(
centre.add(control.point().or_refuse(KernelStage::Refine, "point")?.sub(centre).scale(factor)),
control.w,
))
})
.collect::<Result<Vec<_>, KernelRefusal>>()
})
.collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
else {
return Ok(None);
};
let Ok(surface) = NurbsSurface::new(
source.degree_u,
source.degree_v,
source.knots_u.clone(),
source.knots_v.clone(),
controls,
) else {
return Ok(None);
};
match measure_surface_fit_against_pointwise_offset(
source,
face.same_sense,
&surface,
distance,
1e-9 * scale,
) {
Ok(measured) if measured.within_band() => Ok(Some(surface)),
_ => Ok(None),
}
}
fn revolved_arc_offset_surface(
face: &FaceRecord,
distance: f64,
) -> Result<Option<NurbsSurface>, KernelRefusal> {
if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
return Ok(None);
}
let source = &face.surface;
match source.analytic() {
Some(crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. }) => {}
_ => return Ok(None),
}
let rows = &source.control_points;
let Some(first_row) = rows.first() else {
return Ok(None);
};
if rows.iter().flatten().any(|control| !(control.w > 0.0)) {
return Ok(None);
}
let Ok(generatrix) = NurbsCurve::new(source.degree_v, source.knots_v.clone(), first_row.clone())
else {
return Ok(None);
};
let [t0, t1] = generatrix.domain().or_refuse(KernelStage::Refine, "domain")?;
let at = |fraction: f64| generatrix.evaluate(t0 + (t1 - t0) * fraction);
let (a, b, e) = (at(0.0).or_refuse(KernelStage::Refine, "at")?, at(1.0 / 3.0).or_refuse(KernelStage::Refine, "at")?, at(2.0 / 3.0).or_refuse(KernelStage::Refine, "at")?);
let (ab, ae) = (b.sub(a), e.sub(a));
let normal = ab.cross(ae);
let normal_length_squared = normal.dot(normal);
if normal_length_squared <= 1e-24 {
return Ok(None);
}
let centre = a.add(
ae.cross(normal)
.scale(ab.dot(ab))
.add(normal.cross(ab).scale(ae.dot(ae)))
.scale(0.5 / normal_length_squared),
);
let radius = a.sub(centre).length();
let scale = (centre.length() + radius).max(1.0);
let band = 1e-9 * scale;
let unit_normal = normal.scale(1.0 / normal_length_squared.sqrt());
let samples = 16 * generatrix.control_points.len();
for index in 0..=samples {
let point = at(index as f64 / samples as f64).or_refuse(KernelStage::Refine, "at")?;
let radial = point.sub(centre);
if (radial.length() - radius).abs() > band || radial.dot(unit_normal).abs() > band {
return Ok(None);
}
}
let controls = first_row
.iter()
.map(|control| control.point().or_refuse(KernelStage::Refine, "point"))
.collect::<Result<Vec<_>, KernelRefusal>>()?;
let origin = controls[0];
let mut best = (0usize, 0usize, 0.0f64);
for first in 1..controls.len() {
for second in first + 1..controls.len() {
let area = controls[first]
.sub(origin)
.cross(controls[second].sub(origin))
.length();
if area > best.2 {
best = (first, second, area);
}
}
}
let (first, second, area) = best;
if area <= 1e-9 * scale * scale {
return Ok(None);
}
let (along_first, along_second) = (controls[first].sub(origin), controls[second].sub(origin));
let offset_from_origin = centre.sub(origin);
let (g11, g12, g22) = (
along_first.dot(along_first),
along_first.dot(along_second),
along_second.dot(along_second),
);
let determinant = g11 * g22 - g12 * g12;
let (r1, r2) = (along_first.dot(offset_from_origin), along_second.dot(offset_from_origin));
let s = (r1 * g22 - r2 * g12) / determinant;
let t = (r2 * g11 - r1 * g12) / determinant;
if origin
.add(along_first.scale(s))
.add(along_second.scale(t))
.sub(centre)
.length()
> band
{
return Ok(None);
}
let ([u0, _], [v0, v1]) = domains(source)?;
let Ok(sample) = OffsetEvaluator::new(
"offset_surface",
source,
OffsetNormal::FaceStable {
same_sense: face.same_sense,
},
)
.at(u0, (v0 + v1) / 2.0, -distance)
else {
return Ok(None);
};
let source_radial = sample.source.sub(centre);
let offset_radial = sample.point.sub(centre);
let offset_radius = offset_radial.length();
if (source_radial.length() - radius).abs() > band
|| offset_radius <= band
|| offset_radial.dot(source_radial) <= 0.0
|| ((offset_radius - radius).abs() - distance.abs()).abs() > band
{
return Ok(None);
}
let factor = offset_radius / radius;
let Ok(offset_rows) = rows
.iter()
.map(|row| {
let points = row.iter().map(|control| control.point().or_refuse(KernelStage::Refine, "point")).collect::<Result<Vec<_>, KernelRefusal>>()?;
let row_origin = points[0];
let row_centre = row_origin
.add(points[first].sub(row_origin).scale(s))
.add(points[second].sub(row_origin).scale(t));
Ok(points
.iter()
.zip(row)
.map(|(point, control)| {
Vec4::from_point(row_centre.add(point.sub(row_centre).scale(factor)), control.w)
})
.collect::<Vec<_>>())
})
.collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
else {
return Ok(None);
};
let Ok(surface) = NurbsSurface::new(
source.degree_u,
source.degree_v,
source.knots_u.clone(),
source.knots_v.clone(),
offset_rows,
) else {
return Ok(None);
};
match measure_surface_fit_against_pointwise_offset(
source,
face.same_sense,
&surface,
distance,
band,
) {
Ok(measured) if measured.within_band() => Ok(Some(surface)),
_ => Ok(None),
}
}
pub(crate) fn offset_surface_with_lane(
face: &FaceRecord,
distance: f64,
extension: &CarrierExtension,
) -> Result<(NurbsSurface, OffsetSurfaceLane), KernelRefusal> {
offset_surface_constructed(face, distance, extension).map(|(surface, lane, _)| (surface, lane))
}
fn offset_surface_constructed(
face: &FaceRecord,
distance: f64,
extension: &CarrierExtension,
) -> Result<(NurbsSurface, OffsetSurfaceLane, Option<NurbsSurface>), KernelRefusal> {
let source = &face.surface;
let extension_active = extension.is_active();
if source.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
let ([u0, u1], [v0, v1]) = domains(source)?;
let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
let shift = normal.scale(-distance);
let mut points = source
.control_points
.iter()
.map(|row| {
row.iter()
.map(|control| Ok(control.point().or_refuse(KernelStage::Refine, "point")?.add(shift)))
.collect::<Result<Vec<_>, KernelRefusal>>()
})
.collect::<Result<Vec<_>, KernelRefusal>>()?;
if extension_active {
let p00 = points[0][0];
let p01 = points[0][1];
let p10 = points[1][0];
let direction_u = p10.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let direction_v = p01.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let (trim_u, trim_v) = trim_domain_fractions(face, [u0, u1], [v0, v1])?;
let (back_u, forward_u) = domain_end_moves(extension.u_min, extension.u_max, trim_u);
let (back_v, forward_v) = domain_end_moves(extension.v_min, extension.v_max, trim_v);
points[0][0] = p00
.sub(direction_u.scale(back_u))
.sub(direction_v.scale(back_v));
points[0][1] = p01
.sub(direction_u.scale(back_u))
.add(direction_v.scale(forward_v));
points[1][0] = p10
.add(direction_u.scale(forward_u))
.sub(direction_v.scale(back_v));
points[1][1] = points[1][1]
.add(direction_u.scale(forward_u))
.add(direction_v.scale(forward_v));
}
let controls = points
.into_iter()
.enumerate()
.map(|(row, points)| {
points
.into_iter()
.enumerate()
.map(|(column, point)| {
Vec4::from_point(point, source.control_points[row][column].w)
})
.collect()
})
.collect();
let lane = if extension_active {
OffsetSurfaceLane::Reparameterised
} else {
OffsetSurfaceLane::Affine
};
return Ok((
NurbsSurface::new(
source.degree_u,
source.degree_v,
source.knots_u.clone(),
source.knots_v.clone(),
controls,
).or_refuse(KernelStage::Refine, "new")?,
lane,
None,
));
}
if let Some(surface) = sphere_offset_surface(face, distance)? {
return Ok((surface, OffsetSurfaceLane::Analytic, None));
}
if let Some(surface) = revolved_arc_offset_surface(face, distance)? {
return Ok((surface, OffsetSurfaceLane::Analytic, None));
}
let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u).or_refuse(KernelStage::Refine, "new")?;
let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v).or_refuse(KernelStage::Refine, "new")?;
let parameters_u = greville_parameters(&knot_u);
let parameters_v = greville_parameters(&knot_v);
let evaluator = OffsetEvaluator::new(
"offset_surface",
source,
OffsetNormal::FaceStable {
same_sense: face.same_sense,
},
);
let mut samples = Vec::new();
for &u in ¶meters_u {
let mut row = Vec::new();
for &v in ¶meters_v {
row.push(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point);
}
samples.push(row);
}
let mut reparameterised = false;
let mut extended_source: Option<NurbsSurface> = None;
if parameters_v.len() == 2 && parameters_u.len() >= 2 {
let centroid = |column: usize| {
let mut sum = Vec3::default();
for row in &samples {
sum = sum.add(row[column]);
}
sum.scale(1.0 / samples.len() as f64)
};
let near_centroid = centroid(0);
let far_centroid = centroid(1);
let mut inverted = true;
let mut pinch_fraction = 0.0f64;
let mut near_mean = 0.0f64;
let mut far_mean = 0.0f64;
for row in &samples {
let near_radial = row[0].sub(near_centroid);
let far_radial = row[1].sub(far_centroid);
let near_len = near_radial.length();
let far_len = far_radial.length();
if near_len <= 1e-9 || far_len <= 1e-9 {
inverted = false;
break;
}
if near_radial.dot(far_radial) >= 0.0 {
inverted = false;
break;
}
pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
near_mean += near_len / samples.len() as f64;
far_mean += far_len / samples.len() as f64;
}
if inverted {
let retrim_far = far_mean <= near_mean;
let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
let trim_reaches_the_pinch = if retrim_far {
trim_v[1] > pinch_fraction + 1e-9
} else {
trim_v[0] < pinch_fraction - 1e-9
};
if trim_reaches_the_pinch {
reparameterised = true;
for row in &mut samples {
let near = row[0];
let far = row[1];
let pinch = near.add(far.sub(near).scale(pinch_fraction));
if retrim_far {
row[1] = pinch;
} else {
row[0] = pinch;
}
}
}
}
if extension.extends_v() && !inverted {
let mut min_ruling = f64::MAX;
let mut back_allowance = f64::MAX;
let mut forward_allowance = f64::MAX;
let mut extendable = true;
for row in &samples {
let ruling = row[1].sub(row[0]);
let length = ruling.length();
min_ruling = min_ruling.min(length);
let near_radial = row[0].sub(near_centroid).length();
let far_radial = row[1].sub(far_centroid).length();
if (far_radial - near_radial).abs() > 1e-9 {
let apex_at = near_radial / (near_radial - far_radial);
if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
extendable = false;
break;
}
if apex_at < 0.0 {
back_allowance = back_allowance.min(0.9 * -apex_at);
} else {
forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
}
}
}
let mut source_back = f64::MAX;
let mut source_forward = f64::MAX;
if extendable {
let source_centroid = |column: usize| {
let mut sum = Vec3::default();
for row in &source.control_points {
if let Ok(point) = row[column].point() {
sum = sum.add(point);
}
}
sum.scale(1.0 / source.control_points.len() as f64)
};
let (near_source, far_source) = (source_centroid(0), source_centroid(1));
for row in &source.control_points {
let (near, far) = (row[0].point().or_refuse(KernelStage::Refine, "point")?, row[1].point().or_refuse(KernelStage::Refine, "point")?);
let near_radial = near.sub(near_source).length();
let far_radial = far.sub(far_source).length();
if (far_radial - near_radial).abs() > 1e-9 {
let apex_at = near_radial / (near_radial - far_radial);
if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
extendable = false;
break;
}
if apex_at < 0.0 {
source_back = source_back.min(0.9 * -apex_at);
} else {
source_forward = source_forward.min(0.9 * (apex_at - 1.0));
}
}
}
}
let (back_allowance, forward_allowance) =
(back_allowance.min(source_back), forward_allowance.min(source_forward));
if extendable && min_ruling > 1e-9 {
let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
let (back_world, forward_world) =
domain_end_moves(extension.v_min, extension.v_max, trim_v);
let (requested_back, requested_forward) = (back_world / min_ruling, forward_world / min_ruling);
let apex_end = analytic_offset_apex(source, &samples, offset_fit_contract(source)).and_then(
|(at, apex)| {
if at > 1.0 && at - 1.0 <= requested_forward && at - 1.0 <= source_forward {
Some((false, at - 1.0, apex))
} else if at < 0.0 && -at <= requested_back && -at <= source_back {
Some((true, -at, apex))
} else {
None
}
},
);
let back = match apex_end {
Some((true, reach, _)) => reach,
_ => requested_back.min(back_allowance),
};
let forward = match apex_end {
Some((false, reach, _)) => reach,
_ => requested_forward.min(forward_allowance),
};
let stretched = samples
.iter()
.map(|row| {
let ruling = row[1].sub(row[0]);
let (near, far) = (row[0].sub(ruling.scale(back)), row[1].add(ruling.scale(forward)));
match apex_end {
Some((true, _, apex)) => vec![apex, far],
Some((false, _, apex)) => vec![near, apex],
None => vec![near, far],
}
})
.collect::<Vec<_>>();
let extended = extend_along_rulings(source, back, forward)?;
let extended_offsets = OffsetEvaluator::new(
"offset_surface",
&extended,
OffsetNormal::FaceStable {
same_sense: face.same_sense,
},
);
let [v0, v1] = knot_v.domain();
let mut developability = 0.0f64;
for (row, &u) in stretched.iter().zip(¶meters_u) {
for fraction in [0.0, 0.5, 1.0] {
let exact = extended_offsets.at(u, v0 + (v1 - v0) * fraction, -distance).or_refuse(KernelStage::Refine, "at")?.point;
let along = row[0].add(row[1].sub(row[0]).scale(fraction));
developability = developability.max(exact.sub(along).length());
}
}
let contract = offset_fit_contract(source);
let stretch_kept = developability <= contract;
samples = if stretch_kept {
stretched
} else {
parameters_u
.iter()
.map(|&u| {
parameters_v
.iter()
.map(|&v| Ok(extended_offsets.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
.collect::<Result<Vec<_>, KernelRefusal>>()
})
.collect::<Result<Vec<_>, KernelRefusal>>()?
};
if let Ok(path) = std::env::var("BREP_OFFSET_FIT_TRACE") {
use std::io::Write;
let line = format!(
"extension deg=({},{}) net=({},{}) d={distance:.6} back={back:.6} forward={forward:.6} \
developability={developability:.3e} contract={contract:.3e} lane={} thread={} args={}\n",
source.degree_u,
source.degree_v,
source.control_points.len(),
source.control_points[0].len(),
if stretch_kept { "stretch" } else { "pointwise" },
std::thread::current().name().unwrap_or("-"),
std::env::args().skip(1).collect::<Vec<_>>().join(" "),
);
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
let _ = file.write_all(line.as_bytes());
}
}
extended_source = Some(extended);
}
}
}
let reference = extended_source.as_ref().unwrap_or(source);
let weights = reference
.control_points
.iter()
.map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
.collect::<Vec<_>>();
let fitted = NurbsSurface::new(
source.degree_u,
source.degree_v,
source.knots_u.clone(),
source.knots_v.clone(),
interpolate_tensor(
&knot_u,
&knot_v,
¶meters_u,
¶meters_v,
&samples,
&weights,
)?,
).or_refuse(KernelStage::Refine, "new")?;
if reparameterised {
return Ok((fitted, OffsetSurfaceLane::Reparameterised, None));
}
let reference_offsets = OffsetEvaluator::new(
"offset_surface",
reference,
OffsetNormal::FaceStable {
same_sense: face.same_sense,
},
);
Ok((
refine_offset_fit(reference, &reference_offsets, distance, fitted)?,
OffsetSurfaceLane::Fit,
extended_source,
))
}
fn analytic_offset_apex(source: &NurbsSurface, samples: &[Vec<Vec3>], contract: f64) -> Option<(f64, Vec3)> {
let Some(crate::AnalyticSurface::RuledRevolution { frame, .. }) = crate::analytic_surface::recognize(source) else {
return None;
};
let radial = |point: Vec3| {
let offset = point.sub(frame.origin);
offset.sub(frame.axis.scale(offset.dot(frame.axis)))
};
let mut crossings = Vec::with_capacity(samples.len());
for row in samples {
let (near, far) = (radial(row[0]), radial(row[1]));
let step = far.sub(near);
let at = -near.dot(step) / step.dot(step);
if !at.is_finite() {
return None;
}
crossings.push((at, row[0].add(row[1].sub(row[0]).scale(at))));
}
let count = crossings.len() as f64;
let at = crossings.iter().map(|(at, _)| at).sum::<f64>() / count;
let first = crossings.first()?.1;
let spread = crossings.iter().fold(Vec3::default(), |sum, (_, point)| sum.add(point.sub(first)));
let centre = first.add(spread.scale(1.0 / count));
let apex = frame.origin.add(frame.axis.scale(centre.sub(frame.origin).dot(frame.axis)));
crossings
.iter()
.all(|(_, point)| radial(*point).length() <= contract && point.sub(apex).length() <= contract)
.then_some((at, apex))
}
fn extend_along_rulings(surface: &NurbsSurface, back: f64, forward: f64) -> Result<NurbsSurface, KernelRefusal> {
let rows = surface
.control_points
.iter()
.map(|row| {
let step = row[1].add(row[0].scale(-1.0));
vec![row[0].add(step.scale(-back)), row[1].add(step.scale(forward))]
})
.collect::<Vec<_>>();
if rows.iter().flatten().any(|point| point.w <= 1e-12) {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_ruled_extension_weight", format!(
"offset_surface: extending a ruled face by ({back:.6}, {forward:.6}) of its rulings \
carries a weight through zero; the extended face is not this surface"
)));
}
NurbsSurface::new(surface.degree_u, 1, surface.knots_u.clone(), surface.knots_v.clone(), rows)
.or_refuse(KernelStage::Refine, "new")
}
fn offset_fit_contract(source: &NurbsSurface) -> f64 {
let scale = crate::model_scale(
source
.control_points
.iter()
.flatten()
.filter_map(|point| point.point().ok()),
);
crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit
}
const OFFSET_FIT_REFINEMENTS: usize = 6;
const OFFSET_FIT_MAX_CONTROLS_SEPARABLE: usize = 16_384;
const OFFSET_FIT_MAX_CONTROLS_TENSOR: usize = 900;
#[derive(Clone, Copy, Debug)]
struct OffsetFitDeviation {
along_u: f64,
along_v: f64,
between: f64,
}
impl OffsetFitDeviation {
fn worst(&self) -> f64 {
self.along_u.max(self.along_v).max(self.between)
}
}
fn greville_midpoints(parameters: &[f64]) -> Vec<f64> {
parameters
.windows(2)
.filter(|pair| pair[1] - pair[0] > 1e-12)
.map(|pair| 0.5 * (pair[0] + pair[1]))
.collect()
}
fn offset_fit_deviation(
fitted: &NurbsSurface,
evaluator: &OffsetEvaluator,
distance: f64,
) -> Result<OffsetFitDeviation, KernelRefusal> {
let parameters_u = greville_parameters(&KnotVector::new(fitted.knots_u.clone(), fitted.degree_u).or_refuse(KernelStage::Refine, "new")?);
let parameters_v = greville_parameters(&KnotVector::new(fitted.knots_v.clone(), fitted.degree_v).or_refuse(KernelStage::Refine, "new")?);
let (middle_u, middle_v) = (greville_midpoints(¶meters_u), greville_midpoints(¶meters_v));
let gap = |u: f64, v: f64| -> Result<f64, KernelRefusal> {
Ok(fitted.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?.sub(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point).length())
};
let mut deviation = OffsetFitDeviation {
along_u: 0.0,
along_v: 0.0,
between: 0.0,
};
for &u in &middle_u {
for &v in ¶meters_v {
deviation.along_u = deviation.along_u.max(gap(u, v)?);
}
for &v in &middle_v {
deviation.between = deviation.between.max(gap(u, v)?);
}
}
for &u in ¶meters_u {
for &v in &middle_v {
deviation.along_v = deviation.along_v.max(gap(u, v)?);
}
}
Ok(deviation)
}
fn raise_linear_directions(
surface: &NurbsSurface,
in_u: bool,
in_v: bool,
) -> Result<Option<NurbsSurface>, KernelRefusal> {
fn raise(knots: &[f64], row: &[Vec4]) -> Option<(Vec<f64>, Vec<Vec4>)> {
let count = row.len();
if knots.len() != count + 2 {
return None;
}
let breaks = &knots[1..=count];
if breaks.windows(2).any(|pair| pair[1] - pair[0] <= 1e-12) {
return None;
}
let mut raised_knots = vec![breaks[0]; 4];
for &interior in &breaks[1..count - 1] {
raised_knots.extend([interior; 3]);
}
raised_knots.extend([breaks[count - 1]; 4]);
let mut points = Vec::with_capacity(3 * (count - 1) + 1);
for pair in row.windows(2) {
let step = pair[1].add(pair[0].scale(-1.0));
points.push(pair[0]);
points.push(pair[0].add(step.scale(1.0 / 3.0)));
points.push(pair[0].add(step.scale(2.0 / 3.0)));
}
points.push(row[count - 1]);
Some((raised_knots, points))
}
let mut knots_u = surface.knots_u.clone();
let mut knots_v = surface.knots_v.clone();
let mut degree_u = surface.degree_u;
let mut degree_v = surface.degree_v;
let mut net = surface.control_points.clone();
if in_v {
if degree_v != 1 {
return Ok(None);
}
let mut rows = Vec::with_capacity(net.len());
for row in &net {
let Some((knots, points)) = raise(&surface.knots_v, row) else {
return Ok(None);
};
knots_v = knots;
rows.push(points);
}
net = rows;
degree_v = 3;
}
if in_u {
if degree_u != 1 {
return Ok(None);
}
let count_v = net[0].len();
let mut columns = Vec::with_capacity(count_v);
for column in 0..count_v {
let row = net.iter().map(|row| row[column]).collect::<Vec<_>>();
let Some((knots, points)) = raise(&surface.knots_u, &row) else {
return Ok(None);
};
knots_u = knots;
columns.push(points);
}
let count_u = columns[0].len();
net = (0..count_u)
.map(|row| columns.iter().map(|column| column[row]).collect())
.collect();
degree_u = 3;
}
Ok(Some(NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")?))
}
fn halve_knot_spans(surface: &NurbsSurface, in_u: bool, in_v: bool) -> Result<NurbsSurface, KernelRefusal> {
fn midpoints(knots: &[f64], degree: usize) -> Vec<f64> {
let last = knots.len() - 1 - degree;
knots[degree..=last]
.windows(2)
.filter(|pair| pair[1] - pair[0] > 1e-12)
.map(|pair| 0.5 * (pair[0] + pair[1]))
.collect()
}
let refine = |degree: usize, knots: &[f64], net: Vec<Vec<Vec4>>| -> Result<(Vec<f64>, Vec<Vec<Vec4>>), KernelRefusal> {
let inserted = midpoints(knots, degree);
let mut refined_knots = knots.to_vec();
let mut rows = Vec::with_capacity(net.len());
for row in net {
let mut curve = NurbsCurve::new(degree, knots.to_vec(), row).or_refuse(KernelStage::Refine, "new")?;
for ¶meter in &inserted {
curve = curve.insert_knot(parameter, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
}
refined_knots = curve.knots.clone();
rows.push(curve.control_points);
}
Ok((refined_knots, rows))
};
let mut knots_u = surface.knots_u.clone();
let mut knots_v = surface.knots_v.clone();
let mut net = surface.control_points.clone();
if in_v {
let (knots, rows) = refine(surface.degree_v, &knots_v, net)?;
knots_v = knots;
net = rows;
}
if in_u {
let count_v = net[0].len();
let columns = (0..count_v)
.map(|column| net.iter().map(|row| row[column]).collect::<Vec<_>>())
.collect::<Vec<_>>();
let (knots, refined) = refine(surface.degree_u, &knots_u, columns)?;
knots_u = knots;
let count_u = refined[0].len();
net = (0..count_u)
.map(|row| refined.iter().map(|column| column[row]).collect())
.collect();
}
NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
}
fn refine_offset_fit(
source: &NurbsSurface,
evaluator: &OffsetEvaluator,
distance: f64,
fitted: NurbsSurface,
) -> Result<NurbsSurface, KernelRefusal> {
let scale = crate::model_scale(
source
.control_points
.iter()
.flatten()
.filter_map(|point| point.point().ok()),
);
let bar = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
let trace = std::env::var("BREP_OFFSET_FIT_TRACE").ok();
let started = web_time::Instant::now();
let enabled = std::env::var("BREP_OFFSET_FIT_REFINE").as_deref() != Ok("0");
let Ok(initial) = offset_fit_deviation(&fitted, evaluator, distance) else {
return Ok(fitted);
};
let mut best = fitted;
let mut best_deviation = initial;
let mut refined_source = source.clone();
let mut rounds = 0usize;
let mut stop = "under bar";
if enabled {
while best_deviation.worst() > bar {
if rounds == OFFSET_FIT_REFINEMENTS {
stop = "refinement limit";
break;
}
let (along_u, along_v) = (best_deviation.along_u > bar, best_deviation.along_v > bar);
let (in_u, in_v) = if along_u || along_v {
(along_u, along_v)
} else {
(true, true)
};
let (raise_u, raise_v) = (in_u && refined_source.degree_u == 1, in_v && refined_source.degree_v == 1);
let raised = if raise_u || raise_v {
raise_linear_directions(&refined_source, raise_u, raise_v)?
} else {
None
};
let (halve_u, halve_v) = match raised {
Some(_) => (in_u && !raise_u, in_v && !raise_v),
None => (in_u, in_v),
};
let raised = raised.unwrap_or_else(|| refined_source.clone());
let candidate_source = if halve_u || halve_v {
halve_knot_spans(&raised, halve_u, halve_v)?
} else {
raised
};
let weights = candidate_source
.control_points
.iter()
.map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
.collect::<Vec<_>>();
let controls = weights.len() * weights[0].len();
let ceiling = if separable_weights(&weights).is_some() {
OFFSET_FIT_MAX_CONTROLS_SEPARABLE
} else {
OFFSET_FIT_MAX_CONTROLS_TENSOR
};
if controls > ceiling {
stop = "net ceiling";
break;
}
let knot_u = KnotVector::new(candidate_source.knots_u.clone(), candidate_source.degree_u).or_refuse(KernelStage::Refine, "new")?;
let knot_v = KnotVector::new(candidate_source.knots_v.clone(), candidate_source.degree_v).or_refuse(KernelStage::Refine, "new")?;
let parameters_u = greville_parameters(&knot_u);
let parameters_v = greville_parameters(&knot_v);
let samples = parameters_u
.iter()
.map(|&u| {
parameters_v
.iter()
.map(|&v| Ok(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
.collect::<Result<Vec<_>, KernelRefusal>>()
})
.collect::<Result<Vec<_>, KernelRefusal>>();
let Ok(samples) = samples else {
stop = "unevaluable sample";
break;
};
let candidate = NurbsSurface::new(
candidate_source.degree_u,
candidate_source.degree_v,
candidate_source.knots_u.clone(),
candidate_source.knots_v.clone(),
interpolate_tensor(&knot_u, &knot_v, ¶meters_u, ¶meters_v, &samples, &weights)?,
).or_refuse(KernelStage::Refine, "new")?;
let Ok(deviation) = offset_fit_deviation(&candidate, evaluator, distance) else {
stop = "unmeasurable round";
break;
};
rounds += 1;
if deviation.worst() >= best_deviation.worst() {
stop = "stopped improving";
break;
}
best = candidate;
best_deviation = deviation;
refined_source = candidate_source;
}
} else if best_deviation.worst() > bar {
stop = "refinement disabled";
}
let band = offset_construction_band(scale);
let refused = enabled && best_deviation.worst() > band;
if let Some(path) = trace {
use std::io::Write;
let caller = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
let thread = std::thread::current().name().unwrap_or("-").to_string();
let line = format!(
"fit deg=({},{}) net=({},{})->({},{}) d={distance:.6} scale={scale:.4} bar={bar:.3e} \
initial=(u {:.3e}, v {:.3e}, uv {:.3e}) final={:.3e} rounds={rounds} stop={stop} \
refused={refused} seconds={:.4} thread={thread} args={caller}\n",
source.degree_u,
source.degree_v,
source.control_points.len(),
source.control_points[0].len(),
best.control_points.len(),
best.control_points[0].len(),
initial.along_u,
initial.along_v,
initial.between,
best_deviation.worst(),
started.elapsed().as_secs_f64(),
);
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
let _ = file.write_all(line.as_bytes());
}
}
if refused {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "offset_carrier_fit", format!(
"offset_surface: the fitted offset carrier stays {:.3e} off its pointwise offset at \
distance {distance} after {rounds} refinement round(s) ({stop}), past the offset \
construction band {band:.3e}",
best_deviation.worst()
)));
}
Ok(best)
}
pub(crate) fn unify_offset_sheet_bases(
first: &NurbsSurface,
second: &NurbsSurface,
) -> Result<(NurbsSurface, NurbsSurface), KernelRefusal> {
fn raise_to(surface: &NurbsSurface, degree_u: usize, degree_v: usize) -> Result<NurbsSurface, KernelRefusal> {
let raise_u = surface.degree_u == 1 && degree_u == 3;
let raise_v = surface.degree_v == 1 && degree_v == 3;
if !raise_u && !raise_v {
return Ok(surface.clone());
}
raise_linear_directions(surface, raise_u, raise_v)?
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Refine,
"offset_raise_linear",
"offset sheets: a linear direction could not be raised",
)
})
}
let degree_u = first.degree_u.max(second.degree_u);
let degree_v = first.degree_v.max(second.degree_v);
let mut first = raise_to(first, degree_u, degree_v)?;
let mut second = raise_to(second, degree_u, degree_v)?;
if first.degree_u != second.degree_u || first.degree_v != second.degree_v {
return Err(KernelRefusal::internal(KernelStage::Refine, "offset_common_basis", format!(
"offset sheets: degrees ({},{}) and ({},{}) have no common basis",
first.degree_u, first.degree_v, second.degree_u, second.degree_v
)));
}
fn missing(from: &[f64], into: &[f64]) -> Vec<f64> {
let mut extra = Vec::new();
let mut index = 0usize;
for &knot in from {
while index < into.len() && into[index] < knot - 1e-12 {
index += 1;
}
if index < into.len() && (into[index] - knot).abs() <= 1e-12 {
index += 1;
} else {
extra.push(knot);
}
}
extra
}
fn insert(surface: &NurbsSurface, knots: &[f64], along_u: bool) -> Result<NurbsSurface, KernelRefusal> {
if knots.is_empty() {
return Ok(surface.clone());
}
let (degree, vector) = if along_u {
(surface.degree_u, &surface.knots_u)
} else {
(surface.degree_v, &surface.knots_v)
};
let lines = if along_u {
(0..surface.control_points[0].len())
.map(|column| surface.control_points.iter().map(|row| row[column]).collect::<Vec<_>>())
.collect::<Vec<_>>()
} else {
surface.control_points.clone()
};
let mut refined_knots = vector.clone();
let mut refined = Vec::with_capacity(lines.len());
for line in lines {
let mut curve = NurbsCurve::new(degree, vector.clone(), line).or_refuse(KernelStage::Refine, "new")?;
for &knot in knots {
curve = curve.insert_knot(knot, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
}
refined_knots = curve.knots.clone();
refined.push(curve.control_points);
}
let net = if along_u {
(0..refined[0].len())
.map(|row| refined.iter().map(|column| column[row]).collect())
.collect()
} else {
refined
};
let (knots_u, knots_v) = if along_u {
(refined_knots, surface.knots_v.clone())
} else {
(surface.knots_u.clone(), refined_knots)
};
NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
}
let (to_first_u, to_second_u) = (
missing(&second.knots_u, &first.knots_u),
missing(&first.knots_u, &second.knots_u),
);
first = insert(&first, &to_first_u, true)?;
second = insert(&second, &to_second_u, true)?;
let (to_first_v, to_second_v) = (
missing(&second.knots_v, &first.knots_v),
missing(&first.knots_v, &second.knots_v),
);
first = insert(&first, &to_first_v, false)?;
second = insert(&second, &to_second_v, false)?;
Ok((first, second))
}
fn carrier_edge_curve(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
points: &[Vec3],
parameters: &[f64],
fit_tolerance: f64,
) -> Result<(NurbsCurve, f64, f64), KernelRefusal> {
let image = if std::env::var("BREP_CARRIER_POLYLINE").as_deref() == Ok("1") {
Err("BREP_CARRIER_POLYLINE set".to_string())
} else {
crate::image_curve::image_curve(surface, pcurve, fit_tolerance, "offset_face_carrier")
};
match image {
Ok(image) if image.t0 <= image.t1 => Ok((image.curve, image.t0, image.t1)),
Ok(image) => {
let [start, end] = image.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let curve = image.curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
Ok((curve, start + end - image.t0, start + end - image.t1))
}
Err(_) => {
let curve = interpolate_curve(points, 1, parameters).or_refuse(KernelStage::Refine, "interpolate_curve")?;
let [t0, t1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
Ok((curve, t0, t1))
}
}
}
fn mapped_pcurve_polyline(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
degenerate: bool,
) -> Result<(Vec<Vec3>, Vec<f64>), KernelRefusal> {
let [start, end] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let evaluate = |fraction: f64| -> Result<Vec3, KernelRefusal> {
let uv = pcurve.evaluate(start + (end - start) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")
};
let first = evaluate(0.0)?;
let last = evaluate(1.0)?;
if degenerate {
return Ok((vec![first, last], vec![0.0, 1.0]));
}
fn append(
evaluate: &impl Fn(f64) -> Result<Vec3, KernelRefusal>,
a_fraction: f64,
a: Vec3,
b_fraction: f64,
b: Vec3,
depth: usize,
parameters: &mut Vec<f64>,
points: &mut Vec<Vec3>,
) -> Result<(), KernelRefusal> {
let fractions =
[0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
let samples = fractions
.map(evaluate)
.into_iter()
.collect::<Result<Vec<_>, KernelRefusal>>()?;
let deviation = samples
.iter()
.enumerate()
.map(|(index, point)| {
point
.sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
.length()
})
.fold(0.0, f64::max);
if deviation <= 5e-4 || depth >= 10 {
parameters.push(b_fraction);
points.push(b);
return Ok(());
}
append(
evaluate,
a_fraction,
a,
fractions[1],
samples[1],
depth + 1,
parameters,
points,
)?;
append(
evaluate,
fractions[1],
samples[1],
b_fraction,
b,
depth + 1,
parameters,
points,
)
}
let mut parameters = vec![0.0];
let mut points = vec![first];
append(
&evaluate,
0.0,
first,
1.0,
last,
0,
&mut parameters,
&mut points,
)?;
Ok((points, parameters))
}
#[derive(Clone, Debug)]
pub struct CarrierDeviation {
pub band: f64,
pub lane: OffsetSurfaceLane,
pub surface: Option<MeasuredTolerance>,
pub edges: Vec<(u64, MeasuredTolerance)>,
pub vertices: Vec<(u64, f64)>,
}
impl CarrierDeviation {
pub fn worst(&self) -> Option<MeasuredTolerance> {
MeasuredTolerance::worst(
self.surface
.into_iter()
.chain(self.edges.iter().map(|(_, measured)| *measured))
.chain(
self.vertices
.iter()
.map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
),
)
}
pub fn exceedances(&self) -> Vec<String> {
let mut out = Vec::new();
if let Some(surface) = self.surface {
if surface.exceeds_band() {
out.push(format!("carrier surface fit {}", surface.describe()));
}
}
for (id, measured) in &self.edges {
if measured.exceeds_band() {
out.push(format!("edge {id} {}", measured.describe()));
}
}
for (id, gap) in &self.vertices {
let measured = MeasuredTolerance::new(*gap, self.band);
if measured.exceeds_band() {
out.push(format!("vertex {id} {}", measured.describe()));
}
}
out
}
}
#[derive(Clone, Debug, Serialize)]
pub struct OffsetFaceCarrier {
pub vertices: Vec<VertexRecord>,
pub edges: Vec<EdgeRecord>,
pub face: FaceRecord,
#[serde(skip)]
pub deviation: Option<CarrierDeviation>,
}
fn claim_vertex_image(
source_id: u64,
point: Vec3,
vertex_images: &mut HashMap<u64, u64>,
vertices: &mut Vec<VertexRecord>,
next_id: &mut u64,
) -> u64 {
if let Some(id) = vertex_images.get(&source_id) {
return *id;
}
let id = *next_id;
*next_id += 1;
vertices.push(VertexRecord { id, point });
vertex_images.insert(source_id, id);
id
}
pub fn offset_face_carrier(
solid: &BrepSolid,
face_id: u64,
distance: f64,
planar_extension: f64,
) -> Result<OffsetFaceCarrier, KernelRefusal> {
offset_face_carrier_impl(
solid,
face_id,
distance,
&CarrierExtension::uniform(planar_extension),
false,
)
}
pub fn offset_face_carrier_sided(
solid: &BrepSolid,
face_id: u64,
distance: f64,
extension: &CarrierExtension,
) -> Result<OffsetFaceCarrier, KernelRefusal> {
offset_face_carrier_impl(solid, face_id, distance, extension, false)
}
pub fn offset_face_carrier_measured(
solid: &BrepSolid,
face_id: u64,
distance: f64,
planar_extension: f64,
) -> Result<OffsetFaceCarrier, KernelRefusal> {
offset_face_carrier_impl(
solid,
face_id,
distance,
&CarrierExtension::uniform(planar_extension),
true,
)
}
fn measure_carrier(
surface: &NurbsSurface,
vertices: &[VertexRecord],
edges: &[EdgeRecord],
loops: &[LoopRecord],
lane: OffsetSurfaceLane,
surface_fit: Option<MeasuredTolerance>,
band: f64,
) -> Result<CarrierDeviation, KernelRefusal> {
let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
for loop_record in loops {
for coedge in &loop_record.coedges {
let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
continue;
};
let measured = measure_edge_against_pcurve_image(
surface,
&coedge.pcurve,
edge,
coedge.forward,
band,
)?;
per_edge
.entry(edge.id)
.and_modify(|existing| *existing = existing.worse_of(measured))
.or_insert(measured);
}
}
let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
measured_edges.sort_by_key(|(id, _)| *id);
let deviation_of: HashMap<u64, f64> = measured_edges
.iter()
.map(|(id, measured)| (*id, measured.deviation()))
.collect();
let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
for edge in edges {
ends_at
.entry(edge.start_vertex_id)
.or_default()
.push((edge, edge.t0));
ends_at
.entry(edge.end_vertex_id)
.or_default()
.push((edge, edge.t1));
}
let mut measured_vertices = Vec::with_capacity(vertices.len());
for vertex in vertices {
let mut gaps = Vec::new();
let mut incident = Vec::new();
for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
gaps.push(vertex_endpoint_gap(
vertex.point,
edge.curve.evaluate(*parameter).or_refuse(KernelStage::Refine, "evaluate")?,
));
incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
}
measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
}
Ok(CarrierDeviation {
band,
lane,
surface: surface_fit,
edges: measured_edges,
vertices: measured_vertices,
})
}
fn offset_face_carrier_impl(
solid: &BrepSolid,
face_id: u64,
distance: f64,
extension: &CarrierExtension,
measure: bool,
) -> Result<OffsetFaceCarrier, KernelRefusal> {
let source = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == face_id)
.ok_or_else(|| format!("offset_face_carrier: missing face {face_id}")).or_refuse(KernelStage::Refine, "offset_missing_face")?;
let band = offset_construction_band(solid_model_scale(solid));
let fit_tolerance =
crate::KernelTolerances::for_scale(solid_model_scale(solid), 1e-7).intersection_fit;
let (surface, lane, surface_fit) = if measure {
let measured = offset_surface_measured_sided(source, distance, extension, band)?;
(measured.surface, measured.lane, measured.fit)
} else {
let (surface, lane) = offset_surface_with_lane(source, distance, extension)?;
(surface, lane, None)
};
let source_edges = solid
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let source_vertices = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex))
.collect::<HashMap<_, _>>();
let mut vertices = Vec::new();
let mut vertex_images = HashMap::default();
let mut edges = Vec::new();
let mut edge_images = HashMap::default();
let mut loops = Vec::new();
let mut next_id = 1u64;
for source_loop in &source.loops {
let mut coedges = Vec::new();
for source_coedge in &source_loop.coedges {
let source_edge = source_edges
.get(&source_coedge.edge_id)
.ok_or_else(|| "offset_face_carrier: missing source edge".to_string()).or_refuse(KernelStage::Refine, "offset_missing_edge")?;
let (source_start, source_end) = if source_coedge.forward {
(source_edge.start_vertex_id, source_edge.end_vertex_id)
} else {
(source_edge.end_vertex_id, source_edge.start_vertex_id)
};
if !source_vertices.contains_key(&source_start)
|| !source_vertices.contains_key(&source_end)
{
return Err(KernelRefusal::internal(KernelStage::Refine, "offset_missing_vertex", "offset_face_carrier: missing source vertex"));
}
let (points, parameters) =
mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
let image_collapsed = points
.iter()
.all(|point| point.sub(points[0]).length() <= collapse_tolerance);
let (edge_id, forward) =
if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
edge_images.get(&source_edge.id)
{
if source_start == source_end {
(*edge_id, source_coedge.forward == *creator_forward)
} else {
(
*edge_id,
vertex_images.get(&source_start) == Some(edge_start_vertex_id),
)
}
} else {
let (curve, t0, t1) = if image_collapsed {
let curve = NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![
Vec4::from_point(points[0], 1.0),
Vec4::from_point(points[0], 1.0),
],
).or_refuse(KernelStage::Refine, "new")?;
(curve, 0.0, 1.0)
} else {
carrier_edge_curve(
&surface,
&source_coedge.pcurve,
&points,
¶meters,
fit_tolerance,
)?
};
let start_vertex_id = claim_vertex_image(
source_start,
points[0],
&mut vertex_images,
&mut vertices,
&mut next_id,
);
let end_vertex_id = claim_vertex_image(
source_end,
points[points.len() - 1],
&mut vertex_images,
&mut vertices,
&mut next_id,
);
let id = next_id;
next_id += 1;
edges.push(EdgeRecord {
id,
curve,
t0,
t1,
start_vertex_id,
end_vertex_id,
degenerate: image_collapsed,
name: source_edge
.name
.as_ref()
.map(|name| format!("{name}_Offset")),
});
edge_images.insert(
source_edge.id,
(id, start_vertex_id, source_coedge.forward),
);
(id, true)
};
let id = next_id;
next_id += 1;
coedges.push(CoedgeRecord {
id,
edge_id,
forward,
pcurve: source_coedge.pcurve.clone(),
});
}
let id = next_id;
next_id += 1;
loops.push(LoopRecord { id, coedges });
}
let deviation = if measure {
Some(measure_carrier(
&surface,
&vertices,
&edges,
&loops,
lane,
surface_fit,
band,
)?)
} else {
None
};
Ok(OffsetFaceCarrier {
vertices,
edges,
face: FaceRecord {
id: next_id,
surface,
same_sense: source.same_sense,
loops,
name: source.name.as_ref().map(|name| format!("{name}_Offset")),
},
deviation,
})
}