use std::f64::consts::{FRAC_PI_2, PI, TAU};
use axiolid_curve::{CurvatureLaw, Harmonic};
use ifc_model::EntityId;
use crate::error::GeometryResult;
use crate::lower::session::LoweringSession;
pub(crate) const STANDALONE_SPIRAL: &str =
"an IfcSpiral is unbounded (-inf < u < inf) and the neutral intrinsic curve needs a \
finite arc length; it lowers exactly as the ParentCurve of an IfcCurveSegment";
pub(crate) const STANDALONE_HARMONIC_SPIRAL: &str =
"an IfcCosineSpiral or IfcSineSpiral law depends on the length L of the IfcCurveSegment \
using it; it lowers exactly only as the ParentCurve of an IfcCurveSegment";
pub(crate) fn is_spiral(kind: &str) -> bool {
matches!(
kind,
"IFCCLOTHOID"
| "IFCCOSINESPIRAL"
| "IFCSINESPIRAL"
| "IFCSECONDORDERPOLYNOMIALSPIRAL"
| "IFCTHIRDORDERPOLYNOMIALSPIRAL"
| "IFCSEVENTHORDERPOLYNOMIALSPIRAL"
)
}
pub(crate) fn standalone_reason(kind: &str) -> &'static str {
match kind {
"IFCCOSINESPIRAL" | "IFCSINESPIRAL" => STANDALONE_HARMONIC_SPIRAL,
_ => STANDALONE_SPIRAL,
}
}
pub(crate) fn spiral_law(
session: &LoweringSession<'_>,
id: EntityId,
kind: &str,
segment_length: f64,
) -> GeometryResult<CurvatureLaw> {
let slots = session.slots(id)?;
let term = |index: usize, name: &'static str, required: bool| -> GeometryResult<Option<f64>> {
let raw = if required {
Some(slots.req_f64(index, name)?)
} else {
slots.opt_f64(index)
};
let Some(raw) = raw else { return Ok(None) };
let metres = session.units().length(raw);
if !metres.is_finite() || metres == 0.0 {
return Err(session.degenerate(
id,
kind,
format!("{name} must be finite and non-zero: a zero term is an infinite coefficient, not an absent one"),
));
}
Ok(Some(metres))
};
let polynomial = |terms: &[(usize, &'static str, bool)]| -> GeometryResult<CurvatureLaw> {
let degree = terms.len() - 1;
let mut coefficients = vec![0.0; degree + 1];
for (offset, (index, name, required)) in terms.iter().enumerate() {
if let Some(a) = term(*index, name, *required)? {
let power = degree - offset;
coefficients[power] = polynomial_term(a, power);
}
}
Ok(CurvatureLaw::Polynomial { coefficients })
};
let law = match kind {
"IFCCLOTHOID" => {
let a = term(1, "ClothoidConstant", true)?.unwrap_or(f64::NAN);
CurvatureLaw::Polynomial {
coefficients: vec![0.0, polynomial_term(a, 1)],
}
}
"IFCSECONDORDERPOLYNOMIALSPIRAL" => polynomial(&[
(1, "QuadraticTerm", true),
(2, "LinearTerm", false),
(3, "ConstantTerm", false),
])?,
"IFCTHIRDORDERPOLYNOMIALSPIRAL" => polynomial(&[
(1, "CubicTerm", true),
(2, "QuadraticTerm", false),
(3, "LinearTerm", false),
(4, "ConstantTerm", false),
])?,
"IFCSEVENTHORDERPOLYNOMIALSPIRAL" => polynomial(&[
(1, "SepticTerm", true),
(2, "SexticTerm", false),
(3, "QuinticTerm", false),
(4, "QuarticTerm", false),
(5, "CubicTerm", false),
(6, "QuadraticTerm", false),
(7, "LinearTerm", false),
(8, "ConstantTerm", false),
])?,
"IFCCOSINESPIRAL" => {
let cosine = term(1, "CosineTerm", true)?.unwrap_or(f64::NAN);
let constant = term(2, "ConstantTerm", false)?;
harmonic_law(
session,
id,
kind,
segment_length,
vec![constant.map_or(0.0, |a| polynomial_term(a, 0))],
1.0 / cosine,
PI,
FRAC_PI_2,
)?
}
"IFCSINESPIRAL" => {
let sine = term(1, "SineTerm", true)?.unwrap_or(f64::NAN);
let linear = term(2, "LinearTerm", false)?;
let constant = term(3, "ConstantTerm", false)?;
harmonic_law(
session,
id,
kind,
segment_length,
vec![
constant.map_or(0.0, |a| polynomial_term(a, 0)),
linear.map_or(0.0, |a| polynomial_term(a, 1)),
],
1.0 / sine,
TAU,
0.0,
)?
}
_ => unreachable!("spiral_law is called only for IfcSpiral subtypes"),
};
if !law_is_finite(&law) {
return Err(session.degenerate(id, kind, "the curvature law's coefficients overflow f64"));
}
Ok(law)
}
fn polynomial_term(a: f64, power: usize) -> f64 {
let exponent = i32::try_from(power + 1).unwrap_or(i32::MAX);
a.signum() / a.abs().powi(exponent)
}
#[allow(clippy::too_many_arguments)]
fn harmonic_law(
session: &LoweringSession<'_>,
id: EntityId,
kind: &str,
segment_length: f64,
polynomial: Vec<f64>,
amplitude: f64,
turn: f64,
phase: f64,
) -> GeometryResult<CurvatureLaw> {
if !(segment_length.is_finite() && segment_length > 0.0) {
return Err(session.degenerate(
id,
kind,
"the using IfcCurveSegment must have a finite, non-zero length L",
));
}
Ok(CurvatureLaw::Composite {
polynomial,
harmonics: vec![Harmonic {
amplitude,
angular_frequency: turn / segment_length,
phase,
}],
})
}
fn law_is_finite(law: &CurvatureLaw) -> bool {
match law {
CurvatureLaw::Polynomial { coefficients } => coefficients.iter().all(|c| c.is_finite()),
CurvatureLaw::Composite {
polynomial,
harmonics,
} => {
polynomial.iter().all(|c| c.is_finite())
&& harmonics.iter().all(|h| {
h.amplitude.is_finite()
&& h.angular_frequency.is_finite()
&& h.phase.is_finite()
})
}
_ => false,
}
}
pub(crate) fn piece_law(law: &CurvatureLaw, start: f64, length: f64) -> Option<CurvatureLaw> {
if length >= 0.0 {
return law.shifted(start);
}
Some(reflected(law)?.shifted(-start)?.reversed_orientation())
}
fn reflected(law: &CurvatureLaw) -> Option<CurvatureLaw> {
let odd = |power: usize, c: f64| if power % 2 == 1 { -c } else { c };
let harmonic = |h: &Harmonic| Harmonic {
amplitude: -h.amplitude,
angular_frequency: h.angular_frequency,
phase: -h.phase,
};
match law {
CurvatureLaw::Constant { curvature } => Some(CurvatureLaw::Constant {
curvature: *curvature,
}),
CurvatureLaw::Polynomial { coefficients } => Some(CurvatureLaw::Polynomial {
coefficients: coefficients
.iter()
.enumerate()
.map(|(power, c)| odd(power, *c))
.collect(),
}),
CurvatureLaw::Composite {
polynomial,
harmonics,
} => Some(CurvatureLaw::Composite {
polynomial: polynomial
.iter()
.enumerate()
.map(|(power, c)| odd(power, *c))
.collect(),
harmonics: harmonics.iter().map(harmonic).collect(),
}),
_ => None,
}
}
#[cfg(test)]
mod tests;