use super::*;
const CIRCLE_RESIDUAL_BAR: f64 = 1e-7;
const TANGENT_CHORD_BAR: f64 = 1e-7;
struct FittedCircle {
centre: Vec3,
axis: Vec3,
radius: f64,
residual: f64,
turn: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct SweptEnvelope {
pub centre: Vec3,
pub axis: Vec3,
pub radial: Vec3,
pub major: f64,
pub minor: f64,
pub turn: f64,
pub residual: f64,
}
impl SweptEnvelope {
pub fn ratio(&self) -> f64 {
self.minor / self.major
}
pub fn chord_half_length(&self) -> f64 {
(self.minor * self.minor - self.major * self.major)
.max(0.0)
.sqrt()
}
pub fn tangent(&self) -> bool {
self.chord_half_length() <= TANGENT_CHORD_BAR * self.minor
}
fn cut_moment(&self) -> f64 {
let gap = (self.minor * self.minor - self.major * self.major).max(0.0);
let segment = self.minor * self.minor * (self.major / self.minor).clamp(-1.0, 1.0).acos()
- self.major * gap.sqrt();
self.major * segment - (2.0 / 3.0) * gap.powf(1.5)
}
pub fn apple_volume(&self) -> f64 {
self.turn * (std::f64::consts::PI * self.minor * self.minor * self.major - self.cut_moment())
}
pub fn lemon_volume(&self) -> f64 {
if self.tangent() {
return 0.0;
}
self.turn * -self.cut_moment()
}
}
pub fn swept_envelope_volume(plan: &SweptEnvelope) -> f64 {
if plan.turn >= std::f64::consts::TAU - 1e-9 {
plan.apple_volume()
} else {
plan.apple_volume() + plan.lemon_volume()
}
}
fn run_samples(curves: &[NurbsCurve], per_curve: usize) -> Result<Vec<Vec3>, KernelRefusal> {
let mut samples = Vec::with_capacity(curves.len() * per_curve + 1);
for curve in curves {
let [start, end] = curve.domain().or_refuse(KernelStage::Collect, "domain")?;
for index in 0..per_curve {
samples.push(curve.evaluate(start + (end - start) * index as f64 / per_curve as f64).or_refuse(KernelStage::Collect, "evaluate")?);
}
}
let last = curves.last().ok_or_else(|| {
KernelRefusal::input(KernelStage::Collect, "empty_run", "envelope: empty curve run")
})?;
samples.push(last.evaluate(last.domain().or_refuse(KernelStage::Collect, "domain")?[1]).or_refuse(KernelStage::Collect, "evaluate")?);
Ok(samples)
}
fn fit_circle(samples: &[Vec3]) -> Result<Option<FittedCircle>, KernelRefusal> {
if samples.len() < 5 {
return Ok(None);
}
let first = samples[0];
let middle = samples[samples.len() / 3];
let last = samples[2 * samples.len() / 3];
let Some(centre) = circumcentre(first, middle, last) else {
return Ok(None);
};
let radial = first.sub(centre);
let radius = radial.length();
if radius <= 0.0 || !radius.is_finite() {
return Ok(None);
}
let Ok(axis) = radial.cross(middle.sub(centre)).normalized() else {
return Ok(None);
};
let mut residual: f64 = 0.0;
for sample in samples {
let delta = sample.sub(centre);
residual = residual.max((delta.length() - radius).abs() / radius);
residual = residual.max(delta.dot(axis).abs() / radius);
}
let mut turn = 0.0;
for pair in samples.windows(2) {
let from = pair[0].sub(centre);
let into = pair[1].sub(centre);
let cross = from.cross(into);
let step = cross.length().atan2(from.dot(into));
if cross.dot(axis) < 0.0 && step > CIRCLE_RESIDUAL_BAR {
return Ok(None);
}
turn += step;
}
Ok(Some(FittedCircle {
centre,
axis,
radius,
residual,
turn,
}))
}
fn circumcentre(a: Vec3, b: Vec3, c: Vec3) -> Option<Vec3> {
let ab = b.sub(a);
let ac = c.sub(a);
let normal = ab.cross(ac);
let denominator = 2.0 * normal.dot(normal);
if denominator <= 0.0 || !denominator.is_finite() {
return None;
}
let term = ac
.cross(normal)
.scale(ab.dot(ab))
.add(normal.cross(ab).scale(ac.dot(ac)));
Some(a.add(term.scale(1.0 / denominator)))
}
pub fn recognize_swept_envelope(
profile: &[NurbsCurve],
path: &SweepPath,
placement_mode: SectionPlacement,
) -> Result<Result<SweptEnvelope, String>, KernelRefusal> {
let path_samples = run_samples(&path.curves, 16)?;
let Some(path_circle) = fit_circle(&path_samples)? else {
return Ok(Err("its path is not a circular arc".into()));
};
if path_circle.residual > CIRCLE_RESIDUAL_BAR {
return Ok(Err(format!(
"its path is not a circular arc (its samples sit up to {:.3e} of the fitted radius \
{:.6} off that circle, against a bar of {CIRCLE_RESIDUAL_BAR:.0e})",
path_circle.residual, path_circle.radius
)));
}
let Some(section_circle) = fit_circle(&run_samples(profile, 16)?)? else {
return Ok(Err("its section is not a circle".into()));
};
if section_circle.residual > CIRCLE_RESIDUAL_BAR {
return Ok(Err(format!(
"its section is not a circle (its samples sit up to {:.3e} of the fitted radius \
{:.6} off that circle, against a bar of {CIRCLE_RESIDUAL_BAR:.0e})",
section_circle.residual, section_circle.radius
)));
}
let last = profile.last().ok_or_else(|| {
KernelRefusal::input(KernelStage::Collect, "empty_section", "envelope: empty section")
})?;
let closure = last
.evaluate(last.domain().or_refuse(KernelStage::Classify, "domain")?[1]).or_refuse(KernelStage::Classify, "evaluate")?
.sub(profile[0].evaluate(profile[0].domain().or_refuse(KernelStage::Classify, "domain")?[0]).or_refuse(KernelStage::Classify, "evaluate")?)
.length();
if closure > CIRCLE_RESIDUAL_BAR * section_circle.radius {
return Ok(Err(format!(
"its section is not a closed circle (it ends {closure:.3e} from where it starts)"
)));
}
let major = path_circle.radius;
let minor = section_circle.radius;
if minor < major * (1.0 - CIRCLE_RESIDUAL_BAR) {
return Ok(Err(format!(
"its bend is not tighter than its section (ρ·κ = {:.6} < 1)",
minor / major
)));
}
let start = path_samples[0];
let radial = start.sub(path_circle.centre).normalized().or_refuse(KernelStage::Classify, "normalized")?;
if placement_mode == SectionPlacement::Rigid {
let offset = section_circle.centre.sub(start).length();
if offset > CIRCLE_RESIDUAL_BAR * minor.max(major) {
return Ok(Err(format!(
"its section is not centred on the path (its centre sits {offset:.6} off the \
path's start, and `pathAlign` carries a section from where it was drawn)"
)));
}
let out_of_plane = section_circle
.axis
.dot(path_circle.axis)
.abs()
.max(section_circle.axis.dot(radial).abs());
if out_of_plane > CIRCLE_RESIDUAL_BAR {
return Ok(Err(format!(
"its section is not square to the path (its plane's normal is {:.3e} off the \
path's start tangent)",
out_of_plane
)));
}
}
let turn = if path.closed {
std::f64::consts::TAU
} else {
path_circle.turn
};
if path.closed && (path_circle.turn - std::f64::consts::TAU).abs() > CIRCLE_RESIDUAL_BAR {
return Ok(Err(format!(
"its path closes without going once round its own circle (it turns {:.6} rad)",
path_circle.turn
)));
}
Ok(Ok(SweptEnvelope {
centre: path_circle.centre,
axis: path_circle.axis,
radial,
major,
minor,
turn,
residual: path_circle.residual.max(section_circle.residual),
}))
}
pub const SWEEP_ENVELOPE_REFUSAL: &str = "sweepSolid: the swept envelope";
pub fn build_swept_envelope(plan: &SweptEnvelope) -> Result<Vec<BrepSolid>, KernelRefusal> {
use std::f64::consts::{PI, TAU};
if !(plan.turn > 0.0 && plan.turn <= TAU + 1e-9) {
return Err(KernelRefusal::input(
KernelStage::Collect,
"turn_range",
format!(
"{SWEEP_ENVELOPE_REFUSAL} needs a turn in (0, 2π]; this path turns {:.6} rad",
plan.turn
),
));
}
let full = plan.turn >= TAU - 1e-9;
if full && plan.tangent() {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"horn_torus",
format!(
"{SWEEP_ENVELOPE_REFUSAL} of a section exactly as wide as its bend (ρ·κ = {:.6}, the \
section reaching {:.3e} of its own radius past the axis) carried ALL the way round \
is a HORN torus: the tube touches the axis at one point and the solid is PINCHED \
there, which is not a boundary representation this kernel can hold — its Euler count \
reads one short of a sphere's and no face pair meets along an edge at that point. \
Sweep less than a full turn, where the same section builds, or widen the bend",
plan.ratio(),
plan.chord_half_length() / plan.minor
),
));
}
if !full && plan.turn >= PI && !plan.tangent() {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"lobe_overlap",
format!(
"{SWEEP_ENVELOPE_REFUSAL} of a section that crosses the axis (ρ·κ = {:.6}) sweeps a \
SECOND lobe at the azimuths half a turn from the path, and this path turns {:.6} rad \
— past π that lobe runs back into the first and the union is no longer two disjoint \
revolves. Sweep the run in pieces of less than half a turn, or all the way round",
plan.ratio(),
plan.turn
),
));
}
let turn = if full { TAU } else { plan.turn };
let centre = plan.centre;
let up = plan.axis;
let section_centre = centre.add(plan.radial.scale(plan.major));
let mut bodies = Vec::with_capacity(2);
bodies.push(revolve_lobe(
&near_profile(plan, section_centre)?,
centre,
up,
turn,
)?);
if !full && !plan.tangent() {
bodies.push(revolve_lobe(
&far_profile(plan, section_centre)?,
centre,
up,
turn,
)?);
}
Ok(bodies)
}
fn near_profile(plan: &SweptEnvelope, section_centre: Vec3) -> Result<Vec<NurbsCurve>, KernelRefusal> {
use std::f64::consts::PI;
let (u, w) = (plan.radial, plan.axis);
if plan.tangent() {
return Ok(vec![
make_arc(section_centre, u, w, plan.minor, -PI, 0.0).or_refuse(KernelStage::Fragment, "make_arc")?,
make_arc(section_centre, u, w, plan.minor, 0.0, PI).or_refuse(KernelStage::Fragment, "make_arc")?,
]);
}
let phi = (-plan.major / plan.minor).acos();
let half = plan.chord_half_length();
Ok(vec![
make_arc(section_centre, u, w, plan.minor, -phi, phi).or_refuse(KernelStage::Fragment, "make_arc")?,
make_line(
plan.centre.add(w.scale(half)),
plan.centre.sub(w.scale(half)),
).or_refuse(KernelStage::Fragment, "make_line")?,
])
}
fn far_profile(plan: &SweptEnvelope, section_centre: Vec3) -> Result<Vec<NurbsCurve>, KernelRefusal> {
use std::f64::consts::TAU;
let (u, w) = (plan.radial, plan.axis);
let phi = (-plan.major / plan.minor).acos();
let half = plan.chord_half_length();
Ok(vec![
make_arc(section_centre, u, w, plan.minor, phi, TAU - phi).or_refuse(KernelStage::Fragment, "make_arc")?,
make_line(
plan.centre.sub(w.scale(half)),
plan.centre.add(w.scale(half)),
).or_refuse(KernelStage::Fragment, "make_line")?,
])
}
fn revolve_lobe(
profile: &[NurbsCurve],
centre: Vec3,
axis: Vec3,
turn: f64,
) -> Result<BrepSolid, KernelRefusal> {
let solid = crate::revolve_profile_brep(profile, centre, axis, turn).map_err(|error| {
KernelRefusal::internal(
KernelStage::Fragment,
"revolve",
format!("{SWEEP_ENVELOPE_REFUSAL} could not be revolved: {error}"),
)
})?;
crate::accept_sound(solid, "sweepSolid")
}
pub fn sweep_envelope_bodies(
profile: &[NurbsCurve],
path: &SweepPath,
placement_mode: SectionPlacement,
) -> Result<Vec<BrepSolid>, KernelRefusal> {
match recognize_swept_envelope(profile, path, placement_mode)? {
Ok(plan) => build_swept_envelope(&plan),
Err(reason) => Err(KernelRefusal::unsupported(
KernelStage::Classify,
"envelope_configuration",
format!(
"{SWEEP_ENVELOPE_REFUSAL} trimmed at its own self-intersection is built for a CIRCLE \
carried along one circular planar arc, and this sweep is not that: {reason}. Nothing \
else has a closed form to gate the trim with, so it is refused rather than \
approximated"
),
)),
}
}