use crate::{KernelRefusal, KernelStage, OrRefuse};
use super::*;
pub(crate) const MERIDIAN_RESIDUAL_BAR: f64 = 1e-9;
const COVERAGE_SLACK: f64 = 1e-9;
const WHOLE_FACE_BAR: f64 = 1e-9;
#[derive(Debug, Clone, Copy)]
pub(crate) struct RevolvedBand {
origin: Vec3,
axis: Vec3,
radial: Vec3,
major: f64,
axial: f64,
low: f64,
high: f64,
start: f64,
end: f64,
#[cfg_attr(not(test), allow(dead_code))]
residual: f64,
}
#[cfg_attr(not(test), allow(dead_code))]
impl RevolvedBand {
fn tube_centre(&self) -> Vec3 {
self.origin
.add(self.radial.scale(self.major))
.add(self.axis.scale(self.axial))
}
fn mirror_centre(&self) -> Vec3 {
self.origin
.sub(self.radial.scale(self.major))
.add(self.axis.scale(self.axial))
}
fn fold_angle(&self) -> f64 {
(-self.major / self.high).acos()
}
fn chord_half_length(&self) -> f64 {
(self.high * self.high - self.major * self.major).max(0.0).sqrt()
}
fn radical_rho(&self) -> f64 {
(self.high * self.high - self.low * self.low) / (4.0 * self.major)
}
pub(crate) fn moment(&self) -> f64 {
self.sector_moment() + self.lens_moment()
}
pub(crate) fn volume(&self) -> f64 {
std::f64::consts::TAU * self.moment()
}
fn sector_moment(&self) -> f64 {
let (low, high, major) = (self.low, self.high, self.major);
let span = self.end - self.start;
let sector = span * major * (high * high - low * low) / 2.0
+ (high * high * high - low * low * low) / 3.0 * (self.end.sin() - self.start.sin());
sector - cut_moment(major, high)
}
fn lens_moment(&self) -> f64 {
let Some((rho, _)) = self.lens_crossing() else {
return 0.0;
};
let inner = rho - self.major;
let hole_area = std::f64::consts::PI * self.low * self.low - cap_area(inner, self.low);
let hole = self.major * hole_area
- (2.0 / 3.0) * (self.low * self.low - inner * inner).max(0.0).powf(1.5);
let outer = rho + self.major;
let lemon = -self.major * cap_area(outer, self.high)
+ (2.0 / 3.0) * (self.high * self.high - outer * outer).max(0.0).powf(1.5);
hole + lemon
}
fn lens_crossing(&self) -> Option<(f64, f64)> {
let rho = self.radical_rho();
let height = self.low * self.low - (rho - self.major) * (rho - self.major);
(height > 0.0).then(|| (rho, height.sqrt()))
}
fn meridian_point(&self, s: f64, phi: f64) -> Vec3 {
self.tube_centre()
.add(self.radial.scale(s * phi.cos()))
.add(self.axis.scale(s * phi.sin()))
}
fn rho_at(&self, s: f64, phi: f64) -> f64 {
self.major + s * phi.cos()
}
}
#[cfg_attr(not(test), allow(dead_code))]
fn cut_moment(major: f64, radius: f64) -> f64 {
let gap = (radius * radius - major * major).max(0.0);
let segment = radius * radius * (major / radius).acos() - major * gap.sqrt();
major * segment - (2.0 / 3.0) * gap.powf(1.5)
}
#[cfg_attr(not(test), allow(dead_code))]
fn cap_area(cut: f64, radius: f64) -> f64 {
let clamped = (cut / radius).clamp(-1.0, 1.0);
radius * radius * clamped.acos() - cut * (radius * radius - cut * cut).max(0.0).sqrt()
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Recognized {
Full(RevolvedBand),
Partial(PartialBand),
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PartialBand {
pub(crate) band: RevolvedBand,
pub(crate) sweep: f64,
pub(crate) band_low: f64,
pub(crate) band_high: f64,
pub(crate) touches_start: bool,
pub(crate) touches_end: bool,
}
impl PartialBand {
pub(crate) fn far_moment(&self) -> f64 {
let (major, high) = (self.band.major, self.band.high);
let term = |phi: f64| major * high * high * phi / 2.0 + high * high * high * phi.sin() / 3.0 - major.powi(3) * phi.tan() / 6.0;
term(self.band_high) - term(self.band_low)
}
pub(crate) fn sector_moment(&self) -> f64 {
let (low, high, major) = (self.band.low, self.band.high, self.band.major);
let span = self.band.end - self.band.start;
span * major * (high * high - low * low) / 2.0
+ (high * high * high - low * low * low) / 3.0 * (self.band.end.sin() - self.band.start.sin())
}
pub(crate) fn volumes(&self) -> (f64, f64) {
let far = self.far_moment();
(self.sweep * (self.sector_moment() - far), -self.sweep * far)
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn volume(&self) -> f64 {
let (near, far) = self.volumes();
near + far
}
fn axis_crossing(&self, phi: f64) -> f64 {
-self.band.major / phi.cos()
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn report(&self) -> String {
let (near, far) = self.volumes();
format!(
"R = {:.6}, s ∈ [{:.6}, {:.6}], meridian [{:.6}, {:.6}] rad, sweep {:.6} rad, band inside the range \
[{:.6}, {:.6}] rad (touches start {}, end {}), near body {:.12}, far body {:.12}, V = {:.12}",
self.band.major, self.band.low, self.band.high, self.band.start, self.band.end, self.sweep,
self.band_low, self.band_high, self.touches_start, self.touches_end, near, far, near + far
)
}
}
pub(crate) fn recognize(
surface: &NurbsSurface,
loops: &[Vec<NurbsCurve>],
distance_bottom: f64,
distance_top: f64,
) -> Result<Option<Recognized>, KernelRefusal> {
let Some(crate::AnalyticSurface::Revolution {
frame,
sweep,
generatrix,
..
}) = surface.analytic()
else {
return Ok(None);
};
let samples = generatrix_samples(generatrix)?;
let Some((tube_centre, tube_radius, residual)) = fit_meridian_circle(&samples) else {
return Ok(None);
};
if residual > MERIDIAN_RESIDUAL_BAR {
return Ok(None);
}
let delta = tube_centre.sub(frame.origin);
let axial = delta.dot(frame.axis);
let planar = delta.sub(frame.axis.scale(axial));
let major = planar.length();
if major <= tube_radius {
return Ok(None);
}
let radial = planar.scale(1.0 / major);
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
let middle = surface.evaluate(um, vm).or_refuse(KernelStage::Refine, "evaluate")?;
let Some(outward) = outward_at(middle, frame.origin, frame.axis, major, axial) else {
return Ok(None);
};
let outward_sign = if surface.normal(um, vm).or_refuse(KernelStage::Refine, "normal")?.dot(outward) > 0.0 {
1.0
} else {
-1.0
};
let ends = [
tube_radius + outward_sign * distance_bottom,
tube_radius + outward_sign * distance_top,
];
let low = ends[0].min(ends[1]);
let high = ends[0].max(ends[1]);
if !(low > 0.0) || low >= major || high <= major {
return Ok(None);
}
let (start, end) = meridian_range(&samples, tube_centre, radial, frame.axis)?;
let band = RevolvedBand {
origin: frame.origin,
axis: frame.axis,
radial,
major,
axial,
low,
high,
start,
end,
residual,
};
let fold = band.fold_angle();
let full_turn = (*sweep - std::f64::consts::TAU).abs() <= 1e-9;
if !full_turn {
return recognize_partial(band, *sweep, fold, loops, [u0, u1], [v0, v1]);
}
let Some(band_low) = wrap_into(fold, start, end) else {
return Ok(None);
};
let band_high = band_low + 2.0 * (std::f64::consts::PI - fold);
if band_high >= end {
return Ok(None);
}
if !whole_face(loops, [u0, u1], [v0, v1])? {
return Ok(None);
}
reflected_band_is_covered(&band)?;
Ok(Some(Recognized::Full(band)))
}
fn recognize_partial(
band: RevolvedBand,
sweep: f64,
fold: f64,
loops: &[Vec<NurbsCurve>],
u_domain: [f64; 2],
v_domain: [f64; 2],
) -> Result<Option<Recognized>, KernelRefusal> {
let (start, end) = (band.start, band.end);
let band_width = 2.0 * (std::f64::consts::PI - fold);
let low_inside = wrap_into(fold, start, end);
let high_inside = wrap_into(std::f64::consts::TAU - fold, start, end);
let (band_low, band_high, touches_start, touches_end) = match (low_inside, high_inside) {
(Some(low), Some(high)) if (high - low - band_width).abs() <= 1e-9 => (low, high, false, false),
(Some(_), Some(_)) => return Ok(None),
(Some(low), None) => (low, end, false, true),
(None, Some(high)) => (start, high, true, false),
(None, None) => return Ok(None),
};
if !whole_face(loops, u_domain, v_domain)? {
return Ok(None);
}
if sweep > std::f64::consts::PI + 1e-9 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "thicken_band_partial_overlap", format!(
"thickenSheet: this sheet's offset folds in a BAND and the sheet is revolved {:.6} rad, \
more than half a turn. The segments in the band cross the axis and come out at the \
azimuth half a turn away; at most half a turn that far side lands on azimuths the \
sheet never covers and the thicken is two bodies, and a full turn covers it itself and \
the thicken is one revolve — but between the two the far side OVERLAPS the sheet's own \
sweep over the azimuths [π, {:.6}] rad, where the union is neither: refused rather than \
approximated. Revolve the sheet at most half a turn or a full turn, or thicken it thin \
enough that the offset (now {:.6} from the tube centre, against a major radius of {:.6}) \
does not reach the axis",
sweep, sweep, band.high, band.major
)));
}
Ok(Some(Recognized::Partial(PartialBand {
band,
sweep,
band_low,
band_high,
touches_start,
touches_end,
})))
}
pub(crate) fn outward_at(point: Vec3, origin: Vec3, axis: Vec3, major: f64, axial: f64) -> Option<Vec3> {
let delta = point.sub(origin);
let radial = delta.sub(axis.scale(delta.dot(axis)));
let length = radial.length();
if length <= 0.0 {
return None;
}
let centre = origin
.add(radial.scale(major / length))
.add(axis.scale(axial));
Some(point.sub(centre))
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn diagnose(
surface: &NurbsSurface,
loops: &[Vec<NurbsCurve>],
distance_bottom: f64,
distance_top: f64,
) -> String {
let Some(crate::AnalyticSurface::Revolution {
frame,
sweep,
generatrix,
..
}) = surface.analytic()
else {
return "not a Revolution".into();
};
let samples = match generatrix_samples(generatrix) {
Ok(samples) => samples,
Err(why) => return format!("generatrix samples failed: {why}"),
};
let Some((tube_centre, tube_radius, residual)) = fit_meridian_circle(&samples) else {
return "generatrix is not a circle (collinear)".into();
};
let delta = tube_centre.sub(frame.origin);
let axial = delta.dot(frame.axis);
let planar = delta.sub(frame.axis.scale(axial));
let major = planar.length();
let mut report = format!(
"sweep {sweep:.6}, tube centre {tube_centre:?} r {tube_radius:.6} residual {residual:.3e}, R {major:.6}, axial {axial:.6}"
);
if major <= tube_radius {
return report + " -> the sheet itself crosses the axis";
}
let radial = planar.scale(1.0 / major);
let [u0, u1] = surface.domain_u().unwrap_or([0.0, 1.0]);
let [v0, v1] = surface.domain_v().unwrap_or([0.0, 1.0]);
let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
let middle = match surface.evaluate(um, vm) {
Ok(point) => point,
Err(why) => return report + &format!(" -> evaluate failed: {why}"),
};
let normal = match surface.normal(um, vm) {
Ok(normal) => normal,
Err(why) => return report + &format!(" -> normal failed: {why}"),
};
let Some(outward) = outward_at(middle, frame.origin, frame.axis, major, axial) else {
return report + " -> the mid-domain sample sits on the axis";
};
let outward_sign = if normal.dot(outward) > 0.0 { 1.0 } else { -1.0 };
let ends = [
tube_radius + outward_sign * distance_bottom,
tube_radius + outward_sign * distance_top,
];
let (low, high) = (ends[0].min(ends[1]), ends[0].max(ends[1]));
report += &format!(", outward {outward_sign}, s [{low:.6}, {high:.6}]");
if !(low > 0.0) || low >= major || high <= major {
return report + " -> no fold band in range";
}
let (start, end) = match meridian_range(&samples, tube_centre, radial, frame.axis) {
Ok(range) => range,
Err(why) => return report + &format!(" -> meridian range failed: {why}"),
};
report += &format!(", meridian [{start:.6}, {end:.6}]");
let fold = (-major / high).acos();
report += &format!(", fold {fold:.6}");
let Some(band_low) = wrap_into(fold, start, end) else {
return report + " -> the fold parallel is outside the meridian range";
};
let band_high = band_low + 2.0 * (std::f64::consts::PI - fold);
report += &format!(", band [{band_low:.6}, {band_high:.6}]");
if band_high >= end {
return report + " -> the band is not strictly inside the meridian range";
}
match whole_face(loops, [u0, u1], [v0, v1]) {
Ok(true) => report + ", whole face -> recognized",
Ok(false) => report + " -> the trim is not the whole face",
Err(why) => report + &format!(" -> whole-face check failed: {why}"),
}
}
fn generatrix_samples(generatrix: &NurbsCurve) -> Result<Vec<Vec3>, KernelRefusal> {
let [t0, t1] = generatrix.domain().or_refuse(KernelStage::Refine, "domain")?;
(0..=64)
.map(|index| {
generatrix
.evaluate(t0 + (t1 - t0) * index as f64 / 64.0)
.or_refuse(KernelStage::Refine, "evaluate")
})
.collect()
}
pub(crate) fn fit_meridian_circle(samples: &[Vec3]) -> Option<(Vec3, f64, f64)> {
let (first, middle, last) = (
samples[0],
samples[samples.len() / 3],
samples[2 * samples.len() / 3],
);
let ab = middle.sub(first);
let ac = last.sub(first);
let normal = ab.cross(ac);
let denominator = 2.0 * normal.dot(normal);
if denominator <= 0.0 || !denominator.is_finite() {
return None;
}
let centre = first.add(
ac.cross(normal)
.scale(ab.dot(ab))
.add(normal.cross(ab).scale(ac.dot(ac)))
.scale(1.0 / denominator),
);
let radius = first.sub(centre).length();
if !(radius > 0.0) {
return None;
}
let residual = samples
.iter()
.map(|sample| (sample.sub(centre).length() - radius).abs() / radius)
.fold(0.0_f64, f64::max);
Some((centre, radius, residual))
}
fn meridian_range(
samples: &[Vec3],
tube_centre: Vec3,
radial: Vec3,
axis: Vec3,
) -> Result<(f64, f64), KernelRefusal> {
let angle = |point: Vec3| {
let delta = point.sub(tube_centre);
delta.dot(axis).atan2(delta.dot(radial))
};
let mut previous = angle(samples[0]);
let start = previous;
let mut total = 0.0;
for sample in &samples[1..] {
let current = angle(*sample);
let mut step = current - previous;
while step > std::f64::consts::PI {
step -= std::f64::consts::TAU;
}
while step < -std::f64::consts::PI {
step += std::f64::consts::TAU;
}
total += step;
previous = current;
}
if total < 0.0 {
return Ok((start + total, start));
}
Ok((start, start + total))
}
pub(crate) fn wrap_into(value: f64, low: f64, high: f64) -> Option<f64> {
let mut shifted = value + std::f64::consts::TAU * ((low - value) / std::f64::consts::TAU).floor();
for _ in 0..3 {
if shifted > low && shifted < high {
return Some(shifted);
}
shifted += std::f64::consts::TAU;
}
None
}
pub(crate) fn whole_face(
loops: &[Vec<NurbsCurve>],
[u0, u1]: [f64; 2],
[v0, v1]: [f64; 2],
) -> Result<bool, KernelRefusal> {
if loops.len() != 1 {
return Ok(false);
}
let extent = (u1 - u0).max(v1 - v0);
let mut area = 0.0;
let mut box_u = (f64::INFINITY, f64::NEG_INFINITY);
let mut box_v = (f64::INFINITY, f64::NEG_INFINITY);
for curve in &loops[0] {
let [t0, t1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let mut previous = curve.evaluate(t0).or_refuse(KernelStage::Refine, "evaluate")?;
for index in 1..=32 {
let point = curve.evaluate(t0 + (t1 - t0) * index as f64 / 32.0).or_refuse(KernelStage::Refine, "evaluate")?;
area += 0.5 * (previous.x * point.y - point.x * previous.y);
box_u = (box_u.0.min(point.x), box_u.1.max(point.x));
box_v = (box_v.0.min(point.y), box_v.1.max(point.y));
previous = point;
}
}
let rectangle = (u1 - u0) * (v1 - v0);
Ok((box_u.0 - u0).abs() <= WHOLE_FACE_BAR * extent
&& (box_u.1 - u1).abs() <= WHOLE_FACE_BAR * extent
&& (box_v.0 - v0).abs() <= WHOLE_FACE_BAR * extent
&& (box_v.1 - v1).abs() <= WHOLE_FACE_BAR * extent
&& (area.abs() - rectangle).abs() <= WHOLE_FACE_BAR * rectangle)
}
fn reflected_band_is_covered(band: &RevolvedBand) -> Result<(), KernelRefusal> {
let mirror = band.mirror_centre();
let limit = (band.major / band.high).acos();
let slack = COVERAGE_SLACK * band.high;
for ring in 0..=16 {
let s = band.high * ring as f64 / 16.0;
for step in 0..=32 {
let alpha = -limit + 2.0 * limit * step as f64 / 32.0;
let point = mirror
.add(band.radial.scale(s * alpha.cos()))
.add(band.axis.scale(s * alpha.sin()));
if s * alpha.cos() - band.major < 0.0 {
continue;
}
let delta = point.sub(band.tube_centre());
let distance = delta.length();
if distance < band.low + slack {
continue; }
let phi = delta.dot(band.axis).atan2(delta.dot(band.radial));
if distance <= band.high + slack
&& wrap_into(phi, band.start - COVERAGE_SLACK, band.end + COVERAGE_SLACK).is_some()
{
continue;
}
return Err(KernelRefusal::unsupported(KernelStage::Refine, "thicken_band_unswept_reflection", format!(
"thickenSheet: this sheet's offset folds in a BAND, and the band's reflection \
reaches material the trim never swept — the point at meridian distance {:.6} and \
angle {:.6} rad from the tube centre is in neither the swept sector \
({:.6} rad to {:.6} rad, {:.6} to {:.6} from the centre) nor the tube hole. The \
half-plane union this lane revolves is the sector plus the reflected lemon, and \
that identity is what it just failed",
distance,
phi,
band.start,
band.end,
band.low,
band.high
)));
}
}
Ok(())
}
pub(crate) fn build(band: &RevolvedBand) -> Result<BrepSolid, KernelRefusal> {
let centre = band.tube_centre();
let (radial, axis) = (band.radial, band.axis);
let fold = band.fold_angle();
let band_low = wrap_into(fold, band.start, band.end)
.ok_or_else(|| "thickenSheet: the fold band left the meridian range".to_string()).or_refuse(KernelStage::Refine, "thicken_band_meridian_range")?;
let band_high = band_low + 2.0 * (std::f64::consts::PI - fold);
let mut profile: Vec<NurbsCurve> = Vec::with_capacity(8);
profile.push(make_arc(
centre,
radial,
axis,
band.high,
band.start,
band_low,
).or_refuse(KernelStage::Refine, "make_arc")?);
let half = band.chord_half_length();
let top = band.origin.add(axis.scale(band.axial + half));
let bottom = band.origin.add(axis.scale(band.axial - half));
profile.push(make_line(top, bottom).or_refuse(KernelStage::Refine, "make_line")?);
profile.push(make_arc(
centre,
radial,
axis,
band.high,
band_high,
band.end,
).or_refuse(KernelStage::Refine, "make_arc")?);
profile.push(make_line(
band.meridian_point(band.high, band.end),
band.meridian_point(band.low, band.end),
).or_refuse(KernelStage::Refine, "make_line")?);
match band.lens_crossing() {
Some((rho, height)) => {
let upper = ((rho - band.major) / band.low)
.clamp(-1.0, 1.0)
.acos();
let lower = std::f64::consts::TAU - upper;
let upper = wrap_into(upper, band.start, band_low).ok_or_else(|| {
format!(
"thickenSheet: the reflected band meets the sheet at meridian angle {upper:.6} \
rad, which is outside the regular part of the trim ({:.6} to {:.6})",
band.start, band_low
)
}).or_refuse(KernelStage::Refine, "thicken_band_reflection_range")?;
let lower = wrap_into(lower, band_high, band.end).ok_or_else(|| {
format!(
"thickenSheet: the reflected band meets the sheet at meridian angle {lower:.6} \
rad, which is outside the regular part of the trim ({:.6} to {:.6})",
band_high, band.end
)
}).or_refuse(KernelStage::Refine, "thicken_band_reflection_range")?;
profile.push(make_arc(centre, radial, axis, band.low, lower, band.end).or_refuse(KernelStage::Refine, "make_arc")?.reversed().or_refuse(KernelStage::Refine, "reversed")?);
let mirror = band.mirror_centre();
let alpha = height.atan2(rho + band.major);
profile.push(make_arc(mirror, radial, axis, band.high, -alpha, alpha).or_refuse(KernelStage::Refine, "make_arc")?);
profile.push(make_arc(centre, radial, axis, band.low, band.start, upper).or_refuse(KernelStage::Refine, "make_arc")?.reversed().or_refuse(KernelStage::Refine, "reversed")?);
}
None => {
profile.push(
make_arc(centre, radial, axis, band.low, band.start, band.end).or_refuse(KernelStage::Refine, "make_arc")?.reversed().or_refuse(KernelStage::Refine, "reversed")?,
);
}
}
profile.push(make_line(
band.meridian_point(band.low, band.start),
band.meridian_point(band.high, band.start),
).or_refuse(KernelStage::Refine, "make_line")?);
let solid = crate::revolve_profile_brep(
&profile,
band.origin,
axis,
std::f64::consts::TAU,
)
.map_err(|error| {
KernelRefusal::internal(
KernelStage::Refine,
"revolve_profile_brep",
format!("thickenSheet: the fold band's half-plane union could not be revolved: {error}"),
)
})?;
crate::accept_sound(solid, "thickenSheet")
}
pub(crate) fn build_partial(partial: &PartialBand) -> Result<Vec<BrepSolid>, KernelRefusal> {
let band = &partial.band;
let (radial, axis) = (band.radial, band.axis);
let centre = band.tube_centre();
let (start, end) = (band.start, band.end);
let (band_low, band_high) = (partial.band_low, partial.band_high);
let point = |s: f64, phi: f64| band.meridian_point(s, phi);
let arc = |radius: f64, from: f64, to: f64| make_arc(centre, radial, axis, radius, from, to).or_refuse(KernelStage::Refine, "make_arc");
let line = |from: Vec3, to: Vec3| make_line(from, to).or_refuse(KernelStage::Refine, "make_line");
let reversed = |curve: NurbsCurve| curve.reversed().or_refuse(KernelStage::Refine, "reversed");
let near_start_s = if partial.touches_start { partial.axis_crossing(start) } else { band.high };
let near_end_s = if partial.touches_end { partial.axis_crossing(end) } else { band.high };
let mut near: Vec<NurbsCurve> = Vec::with_capacity(6);
near.push(line(point(band.low, start), point(near_start_s, start))?);
if !partial.touches_start {
near.push(arc(band.high, start, band_low)?);
}
near.push(line(point(near_start_s, band_low), point(near_end_s, band_high))?);
if !partial.touches_end {
near.push(arc(band.high, band_high, end)?);
}
near.push(line(point(near_end_s, end), point(band.low, end))?);
near.push(reversed(arc(band.low, start, end)?)?);
let mut far: Vec<NurbsCurve> = Vec::with_capacity(4);
far.push(arc(band.high, band_low, band_high)?);
if partial.touches_end {
far.push(line(point(band.high, band_high), point(near_end_s, band_high))?);
}
far.push(line(point(near_end_s, band_high), point(near_start_s, band_low))?);
if partial.touches_start {
far.push(line(point(near_start_s, band_low), point(band.high, band_low))?);
}
let mut bodies = Vec::with_capacity(2);
for (label, profile) in [("near", near), ("far", far)] {
let solid = crate::revolve_profile_brep(&profile, band.origin, axis, partial.sweep).map_err(|error| {
KernelRefusal::internal(
KernelStage::Refine,
"revolve_profile_brep",
format!("thickenSheet: the fold band's {label} body could not be revolved: {error}"),
)
})?;
bodies.push(crate::accept_sound(solid, "thickenSheet")?);
}
Ok(bodies)
}
#[cfg_attr(not(test), allow(dead_code))]
impl RevolvedBand {
pub(crate) fn report(&self) -> String {
format!(
"R = {:.6}, s ∈ [{:.6}, {:.6}], meridian [{:.6}, {:.6}] rad, fold band \
[{:.6}, {:.6}] rad, axis chord ±{:.6}, radical ρ* = {:.6}, generatrix residual \
{:.3e}, V = {:.12}",
self.major,
self.low,
self.high,
self.start,
self.end,
self.fold_angle(),
std::f64::consts::TAU - self.fold_angle(),
self.chord_half_length(),
self.radical_rho(),
self.residual,
self.volume(),
)
}
pub(crate) fn end_radii(&self) -> (f64, f64) {
(
self.rho_at(self.high, self.start),
self.rho_at(self.high, self.end),
)
}
}