use energyml_core::value::{FieldValue, Reflect};
use crate::error::{Error, Result};
use crate::points::Point;
use crate::source::ValueSource;
use crate::values;
const TINY: f64 = 1e-12;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Vertical {
Down,
Up,
}
impl Vertical {
fn z(self, depth: f64) -> f64 {
match self {
Vertical::Down => depth,
Vertical::Up => -depth,
}
}
pub fn of(crs: Option<&energyml_domain::crs::Crs>) -> Self {
match crs.map(|crs| crs.vertical_direction) {
Some(energyml_domain::crs::VerticalDirection::Up) => Vertical::Up,
_ => Vertical::Down,
}
}
pub fn read(object: &dyn Reflect, source: &dyn ValueSource) -> Self {
Self::of(energyml_domain::crs::of(object, source).ok().flatten().as_ref())
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
Null,
Vertical,
Linear,
NaturalCubic,
TangentialCubic,
ZLinearCubic,
MinimumCurvature,
}
impl Kind {
fn of(index: i64) -> Option<Self> {
Some(match index {
-1 => Kind::Null,
0 => Kind::Vertical,
1 => Kind::Linear,
2 => Kind::NaturalCubic,
3 => Kind::TangentialCubic,
4 => Kind::ZLinearCubic,
5 => Kind::MinimumCurvature,
_ => return None,
})
}
fn cubic(self) -> bool {
matches!(self, Kind::NaturalCubic | Kind::ZLinearCubic)
}
fn hermite(self) -> bool {
matches!(self, Kind::TangentialCubic | Kind::MinimumCurvature)
}
}
pub fn read(
array: &dyn Reflect,
source: &dyn ValueSource,
at: &str,
vertical: Vertical,
) -> Result<Vec<Point>> {
read_on_lines(array, source, at, None, vertical)
}
pub fn read_on_lines(
array: &dyn Reflect,
source: &dyn ValueSource,
at: &str,
on: Option<&[usize]>,
vertical: Vertical,
) -> Result<Vec<Point>> {
let Some(parameters) = field_object(array, "Parameters") else {
return Err(Error::malformed(at, "a parametric array with no `Parameters`"));
};
let parameters = values::read_doubles(parameters, source, &format!("{at}.Parameters"))?;
let Some(lines) = field_object(array, "ParametricLines") else {
return Err(Error::malformed(
at,
"a parametric array with no `ParametricLines`",
));
};
let lines = Lines::read(lines, source, at, vertical)?;
let explicit = match field_object(array, "ParametricLineIndices") {
Some(indices) => Some(values::read_integers(
indices,
source,
&format!("{at}.ParametricLineIndices"),
)?),
None => None,
};
if let Some(indices) = &explicit
&& indices.len() != parameters.len()
{
return Err(Error::malformed(
at,
format!(
"{} parameter(s) against {} line index/indices",
parameters.len(),
indices.len()
),
));
}
if let Some(on) = on {
if on.is_empty() {
return Err(Error::malformed(at, "a parametric array on no coordinate lines"));
}
if parameters.len() % on.len() != 0 {
return Err(Error::malformed(
at,
format!(
"{} parameter(s) over {} coordinate line(s) is not a whole number of levels",
parameters.len(),
on.len()
),
));
}
}
let count = lines.lines.len().max(1);
let mut points = Vec::with_capacity(parameters.len());
for (index, parameter) in parameters.iter().enumerate() {
let line = match (&explicit, on) {
(Some(indices), _) => indices[index].max(0) as usize,
(None, Some(on)) => on[index % on.len()],
(None, None) => index % count,
};
let Some(line) = lines.lines.get(line) else {
return Err(Error::malformed(
at,
format!("line {line} of {} lines", lines.lines.len()),
));
};
points.push(line.at(*parameter));
}
Ok(points)
}
struct Lines {
lines: Vec<Line>,
}
impl Lines {
fn borrowed(
object: &dyn Reflect,
source: &dyn ValueSource,
at: &str,
vertical: Vertical,
) -> Result<Self> {
let Some(support_uri) = crate::well::reference_of(object, "SupportingRepresentation") else {
return Err(Error::malformed(
at,
"a line lattice over a representation that it does not name",
));
};
let Some(support) = source.object_at(&support_uri) else {
return Err(Error::Domain(energyml_domain::Error::Missing(format!(
"the supporting representation {support_uri}"
))));
};
let support = support.as_reflect();
let Some(array) = find_line_array(support) else {
return Err(Error::malformed(
at,
format!("the support {support_uri} carries no `ParametricLineArray` to borrow from"),
));
};
let lines = Self::read(array, source, &format!("{at}.{support_uri}"), vertical)?;
let shape = pillar_shape(support).unwrap_or_else(|| vec![lines.lines.len()]);
let stated: usize = shape.iter().product();
if stated != lines.lines.len() {
return Err(Error::malformed(
at,
format!(
"the support {support_uri} states a {shape:?} pillar lattice - {stated} line(s) - \
and carries {}",
lines.lines.len()
),
));
}
let Some(lattice) = field_object(object, "LineIndicesOnSupportingRepresentation") else {
return Err(Error::malformed(
at,
"a line lattice with no `LineIndicesOnSupportingRepresentation`",
));
};
let chosen = crate::derived::indices_on(lattice, &shape, at)?;
Ok(Self {
lines: chosen
.into_iter()
.map(|index| lines.lines[index].clone())
.collect(),
})
}
fn read(object: &dyn Reflect, source: &dyn ValueSource, at: &str, vertical: Vertical) -> Result<Self> {
let name = object.type_info().name;
if name == "ParametricLineFromRepresentationLatticeArray" {
return Self::borrowed(object, source, at, vertical);
}
if !name.ends_with("ParametricLineArray") {
return Err(Error::unsupported(leak(name), at));
}
let knots = values::scalar_i64(object, "KnotCount").unwrap_or(0).max(0) as usize;
if knots == 0 {
return Err(Error::malformed(at, "`ParametricLineArray` with no `KnotCount`"));
}
let Some(control) = field_object(object, "ControlPoints") else {
return Err(Error::malformed(
at,
"`ParametricLineArray` with no `ControlPoints`",
));
};
let flat = crate::points::read(control, source, &format!("{at}.ControlPoints"), vertical)?;
if flat.len() % knots != 0 {
return Err(Error::malformed(
at,
format!(
"{} control point(s) is not a multiple of {knots} knot(s)",
flat.len()
),
));
}
let count = flat.len() / knots;
let control = per_line(&flat, count);
let parameters = match field_object(object, "ControlPointParameters") {
Some(array) => {
let flat = values::read_doubles(array, source, &format!("{at}.ControlPointParameters"))?;
if flat.len() != count * knots {
return Err(Error::malformed(
at,
format!(
"{} control point parameter(s) for {count} line(s) of {knots} knot(s)",
flat.len()
),
));
}
Some(per_line(&flat, count))
}
None => None,
};
let tangents = match field_object(object, "TangentVectors") {
Some(array) => {
let flat = crate::points::read(array, source, &format!("{at}.TangentVectors"), vertical)?;
if flat.len() != count * knots {
return Err(Error::malformed(
at,
format!(
"{} tangent vector(s) for {count} line(s) of {knots} knot(s)",
flat.len()
),
));
}
Some(per_line(&flat, count))
}
None => None,
};
let kinds = match field_object(object, "LineKindIndices") {
Some(array) => values::read_integers(array, source, &format!("{at}.LineKindIndices"))?,
None => vec![if knots == 1 { 0 } else { 1 }; count],
};
let mut lines = Vec::with_capacity(count);
for index in 0..count {
let declared = kinds.get(index).copied().unwrap_or(0);
let Some(kind) = Kind::of(declared) else {
return Err(Error::unsupported(
"an unknown kind of parametric line",
format!("{at} line {index}, kind {declared}"),
));
};
lines.push(Line::new(
kind,
control[index].clone(),
parameters.as_ref().map(|p| p[index].clone()),
tangents.as_ref().map(|t| t[index].clone()),
vertical,
));
}
Ok(Self { lines })
}
}
fn per_line<T: Copy>(flat: &[T], count: usize) -> Vec<Vec<T>> {
let count = count.max(1);
let mut out = vec![Vec::with_capacity(flat.len() / count); count];
for (index, value) in flat.iter().enumerate() {
out[index % count].push(*value);
}
out
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Line {
kind: Kind,
control: Vec<Point>,
knots: Vec<f64>,
moments: Vec<Point>,
tangents: Vec<Point>,
parametrised: bool,
vertical: Vertical,
}
impl Line {
fn new(
kind: Kind,
control: Vec<Point>,
knots: Option<Vec<f64>>,
tangents: Option<Vec<Point>>,
vertical: Vertical,
) -> Self {
let mut length = control
.iter()
.position(|p| p.iter().any(|c| !c.is_finite()))
.unwrap_or(control.len());
if let Some(knots) = &knots {
length = length.min(knots.iter().position(|k| !k.is_finite()).unwrap_or(knots.len()));
}
let parametrised = knots.is_some();
let mut control: Vec<Point> = control[..length].to_vec();
let mut knots: Vec<f64> = match knots {
Some(knots) => knots[..length].to_vec(),
None => (0..length).map(|index| index as f64).collect(),
};
let mut tangents: Vec<Point> = match tangents {
Some(tangents) => tangents[..length.min(tangents.len())].to_vec(),
None => Vec::new(),
};
if knots.len() > 1 && knots[knots.len() - 1] < knots[0] {
control.reverse();
knots.reverse();
tangents.reverse();
}
let moments = if kind.cubic() && control.len() > 2 {
moments(&knots, &control)
} else {
Vec::new()
};
let tangents = if kind.hermite() && control.len() > 1 {
if tangents.len() == control.len() {
tangents
} else {
derived_tangents(&knots, &control)
}
} else {
Vec::new()
};
Self {
kind,
control,
knots,
moments,
tangents,
parametrised,
vertical,
}
}
pub(crate) fn stations(&self) -> Vec<Point> {
if !self.parametrised {
return self.control.clone();
}
self.knots.iter().map(|knot| self.at(*knot)).collect()
}
fn borrowed_one(object: &dyn Reflect, source: &dyn ValueSource, at: &str) -> Result<Self> {
let vertical = Vertical::read(object, source);
let Some(support_uri) = crate::well::reference_of(object, "SupportingRepresentation") else {
return Err(Error::malformed(
at,
"a borrowed line over a representation that it does not name",
));
};
let Some(support) = source.object_at(&support_uri) else {
return Err(Error::Domain(energyml_domain::Error::Missing(format!(
"the supporting representation {support_uri}"
))));
};
let support = support.as_reflect();
let Some(array) = find_line_array(support) else {
return Err(Error::malformed(
at,
format!("the support {support_uri} carries no `ParametricLineArray` to borrow from"),
));
};
let lines = Lines::read(array, source, &format!("{at}.{support_uri}"), vertical)?;
let Some(index) = LINE_INDEX
.iter()
.find_map(|field| values::scalar_i64(object, field))
else {
return Err(Error::malformed(
at,
"a borrowed line that does not say which line of the support it is",
));
};
if index < 0 || index as usize >= lines.lines.len() {
return Err(Error::malformed(
at,
format!(
"line {index} of the support {support_uri}, which lends {}",
lines.lines.len()
),
));
}
Ok(lines.lines[index as usize].clone())
}
pub(crate) fn single(geometry: &dyn Reflect, source: &dyn ValueSource, at: &str) -> Result<Self> {
let name = geometry.type_info().name;
if name == "ParametricLineFromRepresentationGeometry" {
return Self::borrowed_one(geometry, source, at);
}
if !name.ends_with("ParametricLineGeometry") {
return Err(Error::unsupported(leak(name), at));
}
let vertical = Vertical::read(geometry, source);
let Some(control) = field_object(geometry, "ControlPoints") else {
return Err(Error::malformed(at, "a parametric line with no `ControlPoints`"));
};
let control = crate::points::read(control, source, &format!("{at}.ControlPoints"), vertical)?;
if control.is_empty() {
return Err(Error::malformed(at, "a parametric line with no control point"));
}
let parameters = match field_object(geometry, "ControlPointParameters") {
Some(array) => {
let read = values::read_doubles(array, source, &format!("{at}.ControlPointParameters"))?;
if read.len() != control.len() {
return Err(Error::malformed(
at,
format!(
"{} control point parameter(s) for {} control point(s)",
read.len(),
control.len()
),
));
}
Some(read)
}
None => None,
};
let tangents = match field_object(geometry, "TangentVectors") {
Some(array) => Some(crate::points::read(
array,
source,
&format!("{at}.TangentVectors"),
vertical,
)?),
None => None,
};
let declared = values::scalar_i64(geometry, "LineKindIndex").unwrap_or(0);
let Some(kind) = Kind::of(declared) else {
return Err(Error::unsupported(
"an unknown kind of parametric line",
format!("{at}, kind {declared}"),
));
};
Ok(Self::new(kind, control, parameters, tangents, vertical))
}
pub(crate) fn at(&self, parameter: f64) -> Point {
const NOWHERE: Point = [f64::NAN; 3];
if self.kind == Kind::Null || self.control.is_empty() {
return NOWHERE;
}
if self.kind == Kind::Vertical || self.control.len() == 1 {
let origin = self.control[0];
return [origin[0], origin[1], self.vertical.z(parameter)];
}
let t = parameter;
match self.kind {
Kind::Linear => self.linear(t),
Kind::NaturalCubic => self.cubic(t),
Kind::ZLinearCubic => {
let curved = self.cubic(t);
let straight = self.linear(t);
[curved[0], curved[1], straight[2]]
}
Kind::TangentialCubic => self.hermite(t),
Kind::MinimumCurvature => self.arc(t),
Kind::Null | Kind::Vertical => NOWHERE,
}
}
fn segment(&self, t: f64) -> usize {
let mut index = 1;
while index + 1 < self.knots.len() && t > self.knots[index] {
index += 1;
}
index
}
fn place(&self, t: f64) -> (usize, f64) {
let index = self.segment(t);
let span = self.knots[index] - self.knots[index - 1];
let fraction = if span.abs() < TINY {
0.0
} else {
(t - self.knots[index - 1]) / span
};
(index, fraction)
}
fn linear(&self, t: f64) -> Point {
let (index, fraction) = self.place(t);
lerp(self.control[index - 1], self.control[index], fraction)
}
fn cubic(&self, t: f64) -> Point {
if self.moments.len() != self.control.len() {
return self.linear(t);
}
let index = self.segment(t);
let span = self.knots[index] - self.knots[index - 1];
if span.abs() < TINY {
return self.control[index];
}
let a = (self.knots[index] - t) / span;
let b = (t - self.knots[index - 1]) / span;
let (p0, p1) = (self.control[index - 1], self.control[index]);
let (m0, m1) = (self.moments[index - 1], self.moments[index]);
let mut out = [0.0; 3];
for axis in 0..3 {
out[axis] = a * p0[axis]
+ b * p1[axis]
+ ((a * a * a - a) * m0[axis] + (b * b * b - b) * m1[axis]) * span * span / 6.0;
}
out
}
fn hermite(&self, t: f64) -> Point {
if self.tangents.len() != self.control.len() {
return self.linear(t);
}
let index = self.segment(t);
let span = self.knots[index] - self.knots[index - 1];
if span.abs() < TINY {
return self.control[index];
}
let s = (t - self.knots[index - 1]) / span;
let (s2, s3) = (s * s, s * s * s);
let (h00, h10) = (2.0 * s3 - 3.0 * s2 + 1.0, s3 - 2.0 * s2 + s);
let (h01, h11) = (-2.0 * s3 + 3.0 * s2, s3 - s2);
let (p0, p1) = (self.control[index - 1], self.control[index]);
let (m0, m1) = (self.tangents[index - 1], self.tangents[index]);
let mut out = [0.0; 3];
for axis in 0..3 {
out[axis] = h00 * p0[axis] + h01 * p1[axis] + (h10 * m0[axis] + h11 * m1[axis]) * span;
}
out
}
fn arc(&self, t: f64) -> Point {
if self.tangents.len() != self.control.len() {
return self.linear(t);
}
let (index, fraction) = self.place(t);
let (p0, p1) = (self.control[index - 1], self.control[index]);
let (Some(t0), Some(t1)) = (unit(self.tangents[index - 1]), unit(self.tangents[index])) else {
return lerp(p0, p1, fraction);
};
let alpha = dot(t0, t1).clamp(-1.0, 1.0).acos();
if alpha < STRAIGHT || (std::f64::consts::PI - alpha) < 1e-9 {
return lerp(p0, p1, fraction);
}
let scale = 1.0 / (alpha * alpha.sin());
let cos = alpha.cos();
let integral = |f: f64| -> Point {
add(
mul(t0, scale * ((1.0 - f) * alpha).cos() - scale * cos),
mul(t1, scale * (1.0 - (f * alpha).cos())),
)
};
let whole = integral(1.0);
let length = norm(whole);
if length < TINY {
return lerp(p0, p1, fraction);
}
let arc_length = norm(sub(p1, p0)) / length;
let closing = sub(sub(p1, p0), mul(whole, arc_length));
add(
add(p0, mul(integral(fraction), arc_length)),
mul(closing, fraction),
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Path(Line);
impl Path {
pub fn of(geometry: &dyn Reflect, source: &dyn ValueSource) -> Result<Path> {
Line::single(geometry, source, "ParametricLineGeometry").map(Path)
}
pub fn new(
kind: i64,
control: Vec<Point>,
parameters: Vec<f64>,
tangents: Option<Vec<Point>>,
vertical: Vertical,
) -> Result<Path> {
let at = "a parametric line given as values";
let Some(kind) = Kind::of(kind) else {
return Err(Error::unsupported(
"an unknown kind of parametric line",
format!("{at}, kind {kind}"),
));
};
if control.is_empty() {
return Err(Error::malformed(at, "no control point"));
}
if parameters.len() != control.len() {
return Err(Error::malformed(
at,
format!(
"{} parameter(s) for {} control point(s)",
parameters.len(),
control.len()
),
));
}
if let Some(tangents) = &tangents
&& tangents.len() != control.len()
{
return Err(Error::malformed(
at,
format!(
"{} tangent(s) for {} control point(s)",
tangents.len(),
control.len()
),
));
}
Ok(Path(Line::new(
kind,
control,
Some(parameters),
tangents,
vertical,
)))
}
pub fn at(&self, parameter: f64) -> Point {
self.0.at(parameter)
}
pub fn knots(&self) -> Option<&[f64]> {
self.0.parametrised.then_some(self.0.knots.as_slice())
}
}
const STRAIGHT: f64 = 1e-6;
fn moments(knots: &[f64], control: &[Point]) -> Vec<Point> {
let n = control.len();
let mut solved = vec![[0.0f64; 3]; n];
if n < 3 || knots.len() != n {
return solved;
}
let spacing: Vec<f64> = (1..n).map(|index| knots[index] - knots[index - 1]).collect();
if spacing.iter().any(|h| h.abs() < TINY) {
return Vec::new();
}
let mut super_diagonal = vec![0.0f64; n];
let mut right = vec![[0.0f64; 3]; n];
for index in 1..n - 1 {
let (left, right_span) = (spacing[index - 1], spacing[index]);
let denominator = 2.0 * (left + right_span) - left * super_diagonal[index - 1];
super_diagonal[index] = right_span / denominator;
for axis in 0..3 {
let slope_after = (control[index + 1][axis] - control[index][axis]) / right_span;
let slope_before = (control[index][axis] - control[index - 1][axis]) / left;
right[index][axis] =
(6.0 * (slope_after - slope_before) - left * right[index - 1][axis]) / denominator;
}
}
for index in (1..n - 1).rev() {
for axis in 0..3 {
solved[index][axis] = right[index][axis] - super_diagonal[index] * solved[index + 1][axis];
}
}
solved
}
fn derived_tangents(knots: &[f64], control: &[Point]) -> Vec<Point> {
let n = control.len();
let mut out = vec![[0.0f64; 3]; n];
if n < 2 || knots.len() != n {
return out;
}
let slope = |from: usize, to: usize| -> Option<Point> {
let span = knots[to] - knots[from];
(span.abs() >= TINY).then(|| mul(sub(control[to], control[from]), 1.0 / span))
};
if n == 2 {
let Some(slope) = slope(0, 1) else {
return out;
};
return vec![slope, slope];
}
for index in 1..n - 1 {
let before = knots[index] - knots[index - 1];
let after = knots[index + 1] - knots[index];
if before.abs() < TINY || after.abs() < TINY {
continue;
}
for axis in 0..3 {
out[index][axis] = -after / (before * (before + after)) * control[index - 1][axis]
+ (after - before) / (before * after) * control[index][axis]
+ before / (after * (before + after)) * control[index + 1][axis];
}
}
if let Some(slope) = slope(0, 1) {
out[0] = sub(mul(slope, 1.5), mul(out[1], 0.5));
}
if let Some(slope) = slope(n - 2, n - 1) {
out[n - 1] = sub(mul(slope, 1.5), mul(out[n - 2], 0.5));
}
out
}
fn field_object<'a>(object: &'a dyn Reflect, field: &str) -> Option<&'a dyn Reflect> {
match object.field(field)? {
FieldValue::Object(child) => Some(child),
_ => None,
}
}
fn leak(key: &str) -> &'static str {
Box::leak(key.to_string().into_boxed_str())
}
fn add(a: Point, b: Point) -> Point {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn sub(a: Point, b: Point) -> Point {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn mul(a: Point, by: f64) -> Point {
[a[0] * by, a[1] * by, a[2] * by]
}
fn dot(a: Point, b: Point) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn norm(a: Point) -> f64 {
dot(a, a).sqrt()
}
fn unit(a: Point) -> Option<Point> {
let length = norm(a);
(length.is_finite() && length > TINY).then(|| mul(a, 1.0 / length))
}
fn lerp(a: Point, b: Point, fraction: f64) -> Point {
add(a, mul(sub(b, a), fraction))
}
const LINE_INDEX: &[&str] = &[
"LineIndexOnSupportingRepresentation",
"LineIndiexOnSupportingRepresentation",
];
fn find_line_array(object: &dyn Reflect) -> Option<&dyn Reflect> {
if object.type_info().name == "ParametricLineArray" {
return Some(object);
}
for field in object.type_info().fields {
match object.field(field.name) {
Some(FieldValue::Object(child)) => {
if let Some(found) = find_line_array(child) {
return Some(found);
}
}
Some(FieldValue::List(items)) => {
for item in items {
if let FieldValue::Object(child) = item
&& let Some(found) = find_line_array(child)
{
return Some(found);
}
}
}
_ => {}
}
}
None
}
fn pillar_shape(support: &dyn Reflect) -> Option<Vec<usize>> {
let axis = |name: &str| {
values::scalar_i64(support, name)
.filter(|n| *n >= 0)
.map(|n| n as usize)
};
match (axis("Nj"), axis("Ni")) {
(Some(nj), Some(ni)) => Some(vec![nj + 1, ni + 1]),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn line(kind: Kind, control: Vec<Point>, knots: Option<Vec<f64>>) -> Line {
Line::new(kind, control, knots, None, Vertical::Down)
}
#[test]
fn a_vertical_line_turns_its_depth_into_z_through_the_crs() {
let control = vec![[275.0, 75.0, 0.0]];
let knots = Some(vec![0.0, 500.0, 1000.0]);
let down = Line::new(
Kind::Vertical,
control.clone(),
knots.clone(),
None,
Vertical::Down,
);
assert_eq!(
down.at(500.0),
[275.0, 75.0, 500.0],
"a depth system stores the depth"
);
let up = Line::new(Kind::Vertical, control, knots, None, Vertical::Up);
assert_eq!(
up.at(500.0),
[275.0, 75.0, -500.0],
"an elevation system stores its negation"
);
}
#[test]
fn a_single_control_point_line_follows_the_same_rule() {
let control = vec![[10.0, 20.0, 0.0]];
let knots = Some(vec![300.0]);
let down = Line::new(Kind::Linear, control.clone(), knots.clone(), None, Vertical::Down);
let up = Line::new(Kind::Linear, control, knots, None, Vertical::Up);
assert_eq!(down.at(300.0)[2], 300.0);
assert_eq!(up.at(300.0)[2], -300.0);
}
#[test]
fn the_curved_kinds_keep_the_z_their_control_points_carry() {
let control = vec![[0.0, 0.0, 100.0], [0.0, 0.0, 300.0]];
let knots = Some(vec![0.0, 200.0]);
let down = Line::new(Kind::Linear, control.clone(), knots.clone(), None, Vertical::Down);
let up = Line::new(Kind::Linear, control, knots, None, Vertical::Up);
assert_eq!(down.at(100.0), up.at(100.0));
assert_eq!(down.at(100.0)[2], 200.0);
}
#[test]
fn the_reading_comes_from_the_crs_and_defaults_to_the_files_own_numbers() {
use energyml_domain::crs::{Crs, CrsKind, VerticalDirection, VerticalQuantity};
let crs = |direction| Crs {
uuid: String::new(),
kind: "LocalDepth3dCrs",
x_offset: 0.0,
y_offset: 0.0,
z_offset: 0.0,
areal_rotation: 0.0,
vertical_direction: direction,
vertical_quantity: VerticalQuantity::Length,
horizontal_uom: None,
vertical_uom: None,
time_uom: None,
projected: CrsKind::Absent,
vertical: CrsKind::Absent,
};
assert_eq!(Vertical::of(Some(&crs(VerticalDirection::Down))), Vertical::Down);
assert_eq!(Vertical::of(Some(&crs(VerticalDirection::Up))), Vertical::Up);
assert_eq!(Vertical::of(None), Vertical::Down);
}
fn close(a: Point, b: Point) -> bool {
within(a, b, 1e-6)
}
fn within(a: Point, b: Point, tolerance: f64) -> bool {
(0..3).all(|axis| (a[axis] - b[axis]).abs() < tolerance)
}
#[test]
fn the_kind_table_is_the_schemas_own() {
assert_eq!(Kind::of(-1), Some(Kind::Null));
assert_eq!(Kind::of(0), Some(Kind::Vertical));
assert_eq!(Kind::of(1), Some(Kind::Linear));
assert_eq!(Kind::of(2), Some(Kind::NaturalCubic));
assert_eq!(Kind::of(3), Some(Kind::TangentialCubic));
assert_eq!(Kind::of(4), Some(Kind::ZLinearCubic));
assert_eq!(Kind::of(5), Some(Kind::MinimumCurvature));
assert_eq!(Kind::of(6), None, "an index the standard does not define");
}
#[test]
fn a_single_knot_is_a_vertical_line() {
let line = line(Kind::Vertical, vec![[10.0, 20.0, 300.0]], None);
assert_eq!(line.at(450.0), [10.0, 20.0, 450.0]);
}
#[test]
fn a_null_line_has_no_points_rather_than_wrong_ones() {
let line = line(Kind::Null, vec![[10.0, 20.0, 300.0]], None);
assert!(line.at(450.0).iter().all(|c| c.is_nan()));
}
#[test]
fn a_linear_line_interpolates_between_its_knots() {
let line = line(
Kind::Linear,
vec![[0.0, 0.0, 0.0], [100.0, 0.0, 1000.0]],
Some(vec![0.0, 1000.0]),
);
assert!(close(line.at(500.0), [50.0, 0.0, 500.0]));
}
#[test]
fn a_parameter_past_the_end_extends_the_last_interval() {
let line = line(
Kind::Linear,
vec![[0.0, 0.0, 0.0], [100.0, 0.0, 1000.0]],
Some(vec![0.0, 1000.0]),
);
assert!(
close(line.at(5000.0), [500.0, 0.0, 5000.0]),
"{:?}",
line.at(5000.0)
);
assert!(
close(line.at(-5000.0), [-500.0, 0.0, -5000.0]),
"{:?}",
line.at(-5000.0)
);
}
#[test]
fn the_extension_follows_the_interval_it_leaves_by() {
let line = line(
Kind::Linear,
vec![[0.0, 0.0, 0.0], [0.0, 0.0, 100.0], [100.0, 0.0, 200.0]],
Some(vec![0.0, 100.0, 200.0]),
);
assert!(close(line.at(300.0), [200.0, 0.0, 300.0]), "{:?}", line.at(300.0));
assert!(
close(line.at(-100.0), [0.0, 0.0, -100.0]),
"{:?}",
line.at(-100.0)
);
}
#[test]
fn descending_knots_interpolate_the_same_way() {
let line = line(
Kind::Linear,
vec![[0.0, 0.0, 1000.0], [0.0, 0.0, 0.0]],
Some(vec![1000.0, 0.0]),
);
assert!((line.at(500.0)[2] - 500.0).abs() < 1e-9);
}
#[test]
fn nan_padding_is_not_data() {
let line = line(
Kind::NaturalCubic,
vec![[0.0, 0.0, 0.0], [10.0, 0.0, 100.0], [f64::NAN; 3]],
Some(vec![0.0, 100.0, f64::NAN]),
);
assert_eq!(line.control.len(), 2, "the padded knot is not a knot");
assert!(close(line.at(50.0), [5.0, 0.0, 50.0]));
}
#[test]
fn a_natural_cubic_passes_through_its_knots() {
let control = vec![
[0.0, 0.0, 0.0],
[1.0, 2.0, 1.0],
[2.0, -1.0, 4.0],
[3.0, 3.0, 9.0],
];
let line = line(
Kind::NaturalCubic,
control.clone(),
Some(vec![0.0, 1.0, 2.0, 3.0]),
);
for (index, knot) in [0.0, 1.0, 2.0, 3.0].iter().enumerate() {
assert!(
close(line.at(*knot), control[index]),
"at {knot}: {:?} vs {:?}",
line.at(*knot),
control[index]
);
}
}
#[test]
fn a_natural_cubic_has_no_curvature_at_its_ends() {
let line = line(
Kind::NaturalCubic,
vec![
[0.0, 0.0, 0.0],
[1.0, 2.0, 1.0],
[2.0, -1.0, 4.0],
[3.0, 3.0, 9.0],
],
Some(vec![0.0, 1.0, 2.0, 3.0]),
);
let step = 1e-5;
let second = |at: f64, inward: f64| -> Point {
let (a, b, c) = (
line.at(at),
line.at(at + inward * step),
line.at(at + inward * 2.0 * step),
);
let mut out = [0.0; 3];
for axis in 0..3 {
out[axis] = (a[axis] - 2.0 * b[axis] + c[axis]) / (step * step);
}
out
};
for (end, inward) in [(0.0, 1.0), (3.0, -1.0)] {
let measured = second(end, inward);
assert!(
within(measured, [0.0; 3], 1e-2),
"at {end}: {measured:?} - a natural spline has none"
);
}
let inside = second(1.5, 1.0);
assert!(
inside.iter().any(|c| c.abs() > 1.0),
"the middle of this spline is curved, or the test proves nothing: {inside:?}"
);
}
#[test]
fn a_natural_cubic_of_two_knots_is_a_straight_segment() {
let line = line(
Kind::NaturalCubic,
vec![[0.0, 0.0, 0.0], [10.0, 20.0, 30.0]],
Some(vec![0.0, 1.0]),
);
assert!(close(line.at(0.25), [2.5, 5.0, 7.5]));
}
#[test]
fn a_z_linear_cubic_curves_in_x_and_y_and_not_in_z() {
let control = vec![[0.0, 0.0, 0.0], [1.0, 3.0, 1.0], [2.0, 0.0, 8.0]];
let knots = vec![0.0, 1.0, 2.0];
let curved = line(Kind::NaturalCubic, control.clone(), Some(knots.clone()));
let mixed = line(Kind::ZLinearCubic, control, Some(knots));
let (a, b) = (curved.at(0.5), mixed.at(0.5));
assert!((a[0] - b[0]).abs() < 1e-12 && (a[1] - b[1]).abs() < 1e-12);
assert!((b[2] - 0.5).abs() < 1e-12, "z is linear in the parameter: {b:?}");
assert!((a[2] - b[2]).abs() > 1e-3, "the z of a full cubic differs: {a:?}");
}
#[test]
fn a_minimum_curvature_segment_is_the_circle_it_describes() {
let line = Line::new(
Kind::MinimumCurvature,
vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
Some(vec![0.0, 1.0]),
Some(vec![[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]]),
Vertical::Down,
);
let half = std::f64::consts::FRAC_1_SQRT_2;
assert!(close(line.at(0.5), [half, half, 0.0]), "{:?}", line.at(0.5));
for step in 0..=10 {
let point = line.at(step as f64 / 10.0);
let radius = (point[0] * point[0] + point[1] * point[1]).sqrt();
assert!((radius - 1.0).abs() < 1e-9, "at {step}/10: radius {radius}");
}
}
#[test]
fn a_minimum_curvature_segment_with_no_dogleg_is_straight() {
let line = Line::new(
Kind::MinimumCurvature,
vec![[0.0, 0.0, 0.0], [0.0, 0.0, 100.0]],
Some(vec![0.0, 100.0]),
Some(vec![[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]),
Vertical::Down,
);
assert!(close(line.at(25.0), [0.0, 0.0, 25.0]));
}
#[test]
fn a_curve_still_reaches_its_far_knot_when_the_arc_would_miss_it() {
let line = Line::new(
Kind::MinimumCurvature,
vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
Some(vec![0.0, 1.0]),
Some(vec![[0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]),
Vertical::Down,
);
assert!(close(line.at(1.0), [10.0, 0.0, 0.0]), "{:?}", line.at(1.0));
assert!(close(line.at(0.0), [0.0, 0.0, 0.0]));
}
#[test]
fn a_tangential_cubic_honours_the_tangents_it_is_given() {
let line = Line::new(
Kind::TangentialCubic,
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
Some(vec![0.0, 1.0]),
Some(vec![[0.0, 2.0, 0.0], [0.0, -2.0, 0.0]]),
Vertical::Down,
);
let step = 1e-5;
let derivative = mul(sub(line.at(step), line.at(0.0)), 1.0 / step);
assert!(
within(derivative, [0.0, 2.0, 0.0], 1e-3),
"{derivative:?} - a forward difference carries `h·f″/2` of error"
);
}
#[test]
fn a_tangential_cubic_with_no_tangents_falls_back_to_the_natural_spline() {
let control = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
let knots = vec![0.0, 1.0, 2.0];
let hermite = line(Kind::TangentialCubic, control.clone(), Some(knots.clone()));
let natural = line(Kind::NaturalCubic, control, Some(knots));
for step in 0..=8 {
let t = step as f64 / 4.0;
assert!(close(hermite.at(t), natural.at(t)), "at {t}");
}
}
#[test]
fn an_undefined_kind_is_refused_rather_than_guessed() {
assert_eq!(Kind::of(9), None);
}
}